Как я могу вызвать метод класса динамически + условно?
(Class в конечном итоге не в classpath)
Скажем, мне нужен класс 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. 1278] EDIT3: :
Кто-нибудь может уточнить , где NoClassDefFoundError
вообще может происходить? Потому что кажется, что это всегда вызывающий метод, а не фактический метод, который использует несуществующий класс.
Узнай, что класс доступен (во время выполнения)
Поместите использование в блок попытки ...
Если это не так, пропустите все это
... и оставьте блок 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);
}
}
Для этого вы можете использовать класс Class .
I.E .:
Class c = Class.forName("your.package.YourClass");
Приведенное выше предложение вызовет исключение ClassNotFoundException, если оно не будет найдено в текущем пути к классам. Если исключение не возникло, вы можете использовать метод newInstance ()
в c
для создания объектов класса your.package.YourClass . Если вам нужно вызвать конкретный конструктор, вы можете использовать метод getConstructors
, чтобы получить его и использовать для создания нового экземпляра.
Эмм, не могли бы вы поместить класс, который хотите расширить, в путь класса времени компиляции, написать свой подкласс как обычно, а во время выполнения явно запустить загрузку подкласса и обработать любое исключение, созданное компоновщиком, которое указывает, что суперкласс отсутствует?
Используйте BCEL для создания динамического подкласса на лету.
Следующий код должен решить вашу проблему. Класс 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
не имеет значения NULL, вызовите 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)
.
Что происходит с String s
и String 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: