Подтвердить что ты не робот

Java: Как переопределить метод класса динамически (класс в конечном итоге НЕ в 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;
    }

  });
}

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

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

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

4b9b3361

Ответ 1

Узнайте, что этот класс доступен (во время выполнения)
Поместите использование в блок 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);
   }
}

Ответ 3

Следующий код должен решить вашу проблему. Класс 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, производный от dummy класс. Обратите внимание, что поскольку он расширяет A, он должен импортировать packageA.A, а class 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:

Nimbus Look and Feel

Ответ 4

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

то есть:.

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

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

Ответ 5

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