【问题标题】:Java: How do I override a method of a class dynamically (class is eventually NOT in classpath)?Java:如何动态覆盖类的方法(类最终不在类路径中)?
【发布时间】:2011-03-26 19:07:15
【问题描述】:

如何动态 + 有条件地调用类的方法?
(类最终不在在类路径中)

假设我需要 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;
    }

  });
}

编辑
现在我按照建议编辑了我的代码,以使用 try-catch 拦截 NoClassDefFoundError。它失败。我不知道,这是否是 OpenJDK 的错。我得到InvocationTargetException,由NoClassDefFoundError 引起。有趣的是,我无法抓住InvocationTargetException:反正它被抛出了。

EDIT2::
找到原因:我将 SwingUtilities.invokeAndWait(...) 包裹在测试方法周围,当加载 Nimbus 失败时,非常 invokeAndWait 调用会抛出 NoClassDefFoundError

EDIT3::
任何人都可以澄清在哪里 NoClassDefFoundError 可以发生吗?因为它似乎总是调用方法,而不是使用不存在的类的实际方法。

【问题讨论】:

  • NoClassDefFoundError 发生在加载编译时类路径中存在但运行时类路径中不存在的类期间。 ClassNotFoundException 发生在加载运行时类路径中不存在但编译时类路径中不需要存在的类期间。
  • @BalusC:我在 EDIT3 中的问题与 EDIT2 相关:是在某处指定 NoClassDefFoundError 发生,比方说,在类的构造 试图调用另一个不存在的类,还是仅在调用不存在的类的方法 被调用时才发生,... 更一般化:何时加载某个类的指定位置在哪里?

标签: java reflection classpath openjdk nimbus


【解决方案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);
   }
}

【讨论】:

  • 好主意!我不仅在执行,还编译OpenJDK 6下的代码,所以我被编译时错误弄糊涂了。
  • ...当然,最佳解决方案将使我能够选择性地编译最终不存在的类。
  • 假设这段代码已经编译,如果NimbusLookAndFeel在运行时不存在,它不会抛出ClassNotFoundException,但会抛出NoClassDefFoundError
  • @Pascal Thivent:对! “在相应的 try 语句的主体中永远不会抛出异常 ClassNotFoundException”。但是你确定NoClassDefFoundError 被抛出在那个方法中吗? (不是,例如,在课堂上的 ctor、静态 ctor、......或其他任何东西......)
  • @java.is.for.desktop 你试过了吗?
【解决方案2】:

使用 BCEL 动态生成动态子类。

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

【讨论】:

  • 好主意,但不是更简单吗?
  • 我不这么认为。 JDK 中内置的最接近的东西是 Proxy 类,但这不会是子类,所以没有帮助。最终,必须动态创建一个新类,这意味着生成该类的字节码,并通过 ClassLoader 加载它。如果没有 BCEL 等第三方库,Java 不会为此提供很多选项。
  • 是的,可悲的是,代理仅用于实现接口。
【解决方案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,虚拟派生类。请注意,由于它扩展了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.ApackageB.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)

String s 和 String t 会发生什么情况?
t 被匿名类直接使用。当调用objectB.doSomething("World"); 时,"World" 是提供给类Bs。由于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 类 $ $ $ $ javac -cp src -d 类 src/Main.java $ java -cp 类主要 未找到 packageA.A。没有它! $ $ $ $ javac -cp src -d 类 src/packageB/B.java $ java -cp 类主要 这是来自 packageA.A: World 这是来自匿名内部类:你好

在第一次运行中,类packageB.B 没有被编译(因为Main.java 没有任何对它的引用)。在第二次运行中,该类被显式编译,因此您得到了预期的结果。

为了帮助您适应我的解决方案来解决您的问题,这里是设置 Nimbus 外观和感觉的正确方法的链接:

Nimbus Look and Feel

【讨论】:

    【解决方案4】:

    您可以使用Class 类来做到这一点。

    IE:

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

    如果在当前类路径中找不到,上面的语句将抛出 ClassNotFoundException。如果没有抛出异常,则可以使用c 中的newInstance() 方法创建your.package.YourClass 类的对象。如果需要调用特定的构造函数,可以使用getConstructors方法获取一个并使用它来创建一个新的实例。

    【讨论】:

    • 我在考虑这个问题,但是如何管理重写动态加载类的方法(从而创建它的匿名内部子类)?
    【解决方案5】:

    呃,你不能把你想扩展的类放到编译时类路径中,像往常一样写你的子类,在运行时,显式触发加载子类,并处理链接器抛出的任何异常,表明超类不见了?

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-02-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-07-29
      相关资源
      最近更新 更多