Java: Как я могу переопределить метод класса динамически (класс в конечном итоге НЕ находится в пути к классам)? - PullRequest
10 голосов
/ 07 августа 2010

Как вызвать метод класса динамически + условно?
(класс в конечном итоге не в пути к классам)

Допустим, мне нужен класс NimbusLookAndFeel, но в некоторых системах он недоступен (т.е. OpenJDK-6).

Так что я должен уметь:

  • Узнайте, что класс доступен (во время выполнения),
  • Если это не так, пропустить все это.
  • Как мне удается переопределить метод динамически загруженного класса
    (таким образом создавая его анонимный внутренний подкласс)?

Пример кода

public static void setNimbusUI(final IMethod<UIDefaults> method)
    throws UnsupportedLookAndFeelException {

  // NimbusLookAndFeel may be now available
  UIManager.setLookAndFeel(new NimbusLookAndFeel() {

    @Override
    public UIDefaults getDefaults() {
      UIDefaults ret = super.getDefaults();
      method.perform(ret);
      return ret;
    }

  });
}

EDIT
Теперь я отредактировал свой код, как было предложено, для перехвата NoClassDefFoundError с помощью try-catch. Это не удается. Я не знаю, если это вина OpenJDK. Я получаю InvocationTargetException, вызванное NoClassDefFoundError. Забавно, что я не могу поймать InvocationTargetException: он все равно брошен.

EDIT2:
Обнаружена причина: я обернул SwingUtilities.invokeAndWait(...) вокруг тестируемого метода, и тот самый invokeAndWait вызов выдает NoClassDefFoundError при сбое загрузки Nimbus.

EDIT3:
Может кто-нибудь уточнить, , где NoClassDefFoundError вообще может происходить? Потому что кажется, что это всегда вызывающий метод, а не фактический метод, который использует несуществующий класс.

Ответы [ 5 ]

4 голосов
/ 08 августа 2010

Узнайте, что класс доступен (во время выполнения)
Поместите использование в блок try ...

Если это не так, пропуститевсе это
... и оставьте блок catch пустым (запах кода!!).

Как мне удается переопределить метод динамически загруженного класса
Просто сделайте это и убедитесь, что зависимость времени компиляции удовлетворена.Вы смешиваете вещи здесь.Переопределение происходит во время компиляции, в то время как загрузка класса - это вещь времени выполнения.

Для полноты каждый класс, который вы пишете, динамически загружается средой выполнения, когда это требуется.

Так что ваш код может выглядеть так:

public static void setNimbusUI(final IMethod<UIDefaults> method)
    throws UnsupportedLookAndFeelException {

    try {
        // NimbusLookAndFeel may be now available
        UIManager.setLookAndFeel(new NimbusLookAndFeel() {

            @Override
            public UIDefaults getDefaults() {
                final UIDefaults defaults = super.getDefaults();
                method.perform(defaults);
                return defaults;
            }

        });
   } catch (NoClassDefFoundError e) {
       throw new UnsupportedLookAndFeelException(e);
   }
}
1 голос
/ 11 августа 2010

Следующий код должен решить вашу проблему. Класс Main имитирует ваш основной класс. Класс A имитирует базовый класс, который вы хотите расширить (и у вас нет контроля над ним). Класс B является производным классом класса A. Интерфейс C имитирует функциональность «указатель функции», которой нет в Java. Давайте сначала посмотрим код ...

Ниже приведен класс A, класс, который вы хотите расширить, но не можете его контролировать:


/* src/packageA/A.java */

package packageA;

public class A {
    public A() {
    }

    public void doSomething(String s) {
        System.out.println("This is from packageA.A: " + s);
    }
}

Ниже приведен класс B, фиктивный производный класс. Обратите внимание, что, поскольку он расширяет A, он должен импортировать packageA.A, а класс A должен быть доступен во время компиляции класса B. Конструктор с параметром C необходим, но реализация интерфейса C является необязательной. Если B реализует C, вы получаете удобство для вызова метода (ов) для экземпляра B напрямую (без отражения). В B.doSomething() вызов super.doSomething() является необязательным и зависит от того, хотите ли вы этого, но необходим c.doSomething() (поясняется ниже):


/* src/packageB/B.java */

package packageB;

import packageA.A;
import packageC.C;

public class B extends A implements C {
    private C c;

    public B(C c) {
        super();
        this.c = c;
    }

    @Override
    public void doSomething(String s) {
        super.doSomething(s);
        c.doSomething(s);
    }
}

Ниже приведен хитрый интерфейс C. Просто поместите все методы, которые вы хотите переопределить в этот интерфейс:


/* src/packageC/C.java */

package packageC;

public interface C {
    public void doSomething(String s);
}

Ниже приведен основной класс:


/* src/Main.java */

import packageC.C;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class Main {
    public static void main(String[] args) {
        doSomethingWithB("Hello");
    }

    public static void doSomethingWithB(final String t) {
        Class classB = null;
        try {
            Class classA = Class.forName("packageA.A");
            classB = Class.forName("packageB.B");
        } catch (ClassNotFoundException e) {
            System.out.println("packageA.A not found. Go without it!");
        }

        Constructor constructorB = null;
        if (classB != null) {
            try {
                constructorB = classB.getConstructor(C.class);
            } catch (NoSuchMethodException e) {
                throw new RuntimeException(e);
            }
        }

        C objectB = null;
        if (constructorB != null) {
            try {
                objectB = (C) constructorB.newInstance(new C() {
                    public void doSomething(String s) {
                        System.out.println("This is from anonymous inner class: " + t);
                    }
                });
            } catch (ClassCastException e) {
                throw new RuntimeException(e);
            } catch (InstantiationException e) {
                throw new RuntimeException(e);
            } catch (IllegalAccessException e) {
                throw new RuntimeException(e);
            } catch (InvocationTargetException e) {
                throw new RuntimeException(e);
            }
        }

        if (objectB != null) {
            objectB.doSomething("World");
        }
    }
}

Почему он компилируется и запускается?
Вы можете видеть, что в классе Main импортируется только packageC.C, и нет ссылки на packageA.A или packageB.B. Если таковые имеются, загрузчик классов сгенерирует исключение на платформах, у которых нет packageA.A, когда он попытается загрузить одну из них.

Как это работает?
В первом Class.forName() он проверяет, доступен ли класс A на платформе. Если это так, попросите загрузчик классов загрузить класс B и сохранить полученный объект Class в classB. В противном случае ClassNotFoundException выбрасывается Class.forName(), и программа работает без класса A.

Тогда, если classB не равно нулю, получите конструктор класса B, который принимает один объект C в качестве параметра. Сохраните объект Constructor в constructorB.

Затем, если constructorB не равно нулю, вызовите constructorB.newInstance(), чтобы создать B объект. Поскольку в качестве параметра имеется объект C, вы можете создать анонимный класс, который реализует интерфейс C, и передать экземпляр в качестве значения параметра. Это похоже на то, что вы делаете, когда создаете анонимный MouseListener.

(На самом деле вам не нужно разделять вышеуказанные блоки try. Это сделано для того, чтобы было понятно, что я делаю.)

Если вы сделали B реализации C, вы можете привести объект B в качестве ссылки C в это время, а затем вы можете вызвать переопределенные методы напрямую (без отражения).

Что если класс A не имеет "конструктора без параметров"?
Просто добавьте необходимые параметры в класс B, например public B(int extraParam, C c), и вызовите super(extraParam) вместо super(). При создании constructorB также добавьте дополнительный параметр, например classB.getConstructor(Integer.TYPE, C.class).

Что происходит со строкой s и строкой t?
t используется анонимным классом напрямую. Когда вызывается objectB.doSomething("World");, "World" - это s, предоставляемый классу B. Поскольку super нельзя использовать в анонимном классе (по понятным причинам), весь код, который использует super, помещается в класс B.

Что если я хочу сослаться на super несколько раз?
Просто напишите шаблон в B.doSomething() так:


    @Override
    public void doSomething(String s) {
        super.doSomething1(s);
        c.doSomethingAfter1(s);
        super.doSomething2(s);
        c.doSomethingAfter2(s);
    }

Конечно, вы должны изменить интерфейс C, чтобы включить doSomethingAfter1() и doSomethingAfter2().

Как скомпилировать и запустить код?

$ mkdir classes
$
$
$
$ javac -cp src -d classes src/Main.java
$ java -cp classes Main
packageA.A not found. Go without it!
$
$
$
$ javac -cp src -d classes src/packageB/B.java
$ java -cp classes Main
This is from packageA.A: World
This is from anonymous inner class: Hello

При первом запуске класс packageB.B не компилируется (поскольку Main.java не имеет на него никаких ссылок). Во втором запуске класс явно скомпилирован, и, таким образом, вы получите ожидаемый результат.

Чтобы помочь вам найти подходящее решение для вашей проблемы, вот ссылка на правильный способ настройки Nimbus Look and Feel:

Нимб Смотри и чувствуй

1 голос
/ 08 августа 2010

Используйте BCEL для создания динамического подкласса на лету.

http://jakarta.apache.org/bcel/manual.html

0 голосов
/ 08 августа 2010

Эмм, разве вы не можете поместить класс, который вы хотите расширить, в путь класса времени компиляции, написать свой подкласс как обычно, а во время выполнения явно инициировать загрузку подкласса и обработать любое исключение, выдаваемое компоновщиком, который указывает, чтосуперкласс отсутствует?

0 голосов
/ 07 августа 2010

Вы можете использовать Class class, чтобы сделать это.

т.е:.

Class c = Class.forName("your.package.YourClass");

Приведенное выше предложение вызовет исключение ClassNotFoundException, если оно не найдено в текущем пути к классам. Если исключение не выдается, вы можете использовать метод newInstance() в c для создания объектов класса your.package.YourClass . Если вам нужно вызвать определенный конструктор, вы можете использовать метод getConstructors для его получения и использовать его для создания нового экземпляра.

...