Java:我如何动态地覆盖一个类的方法(该类最终不在类路径中)?

10

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

假设我需要使用类 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的问题。我得到了由NoClassDefFoundError引起的InvocationTargetException。有趣的是,我无法捕获InvocationTargetException:它无论如何都会被抛出。

编辑2::
原因已找到:我在测试方法周围包装了SwingUtilities.invokeAndWait(...),那个invokeAndWait调用会在加载Nimbus失败时抛出NoClassDefFoundError

编辑3::
能否请有人澄清一下NoClassDefFoundError到底可以发生在哪里?因为似乎总是在调用方法而不是实际使用不存在类的方法时发生。


NoClassDefFoundError 在加载编译时类路径中存在但在运行时类路径中不存在的类时发生。ClassNotFoundException 在加载运行时类路径中不存在但不需要出现在编译时类路径中的类时发生。 - BalusC
@BalusC: 我在 EDIT3 中的问题与 EDIT2 有关:是否有地方指定 NoClassDefFoundError 发生在类的构造期间,该类试图调用另一个不存在的类,或者仅在调用调用不存在的类的 方法 时才发生,... 更一般地说:何时指定某个类被加载? - java.is.for.desktop
5个回答

4

了解类是可用的(在运行时)
把使用放在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);
   }
}

当然,最优解决方案应该让我有选择地编译最终不存在的类。 - java.is.for.desktop
3
假设这段代码已经编译过,如果在运行时缺少NimbusLookAndFeel,它将不会抛出ClassNotFoundException而是会抛出NoClassDefFoundError - Pascal Thivent
@Pascal Thivent: 没错!"在相应的try语句体中,不会抛出ClassNotFoundException异常"。 但你确定NoClassDefFoundError在该方法内部抛出的吗?(而不是在类的构造函数、静态构造函数等地方...) - java.is.for.desktop
是的,正如编辑后的问题中所提到的,OpenJDK-6似乎有些问题:我遇到了InvocationTargetException(由NoClassDefFoundError引起),而奇怪的是,它无法被捕获。 - java.is.for.desktop
现在,找到了原因,并添加到问题中(编辑2)。 - java.is.for.desktop
显示剩余3条评论

1

以下代码应该可以解决您的问题。 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,并将结果存储在classB中。否则,Class.forName()会抛出ClassNotFoundException,程序将不使用类A

然后,如果classB不为空,请获取接受单个C对象作为参数的类B的构造函数。将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)

字符串 s 和字符串 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 classes
$
$
$
$ javac -cp src -d classes src/Main.java
$ java -cp classes Main
packageA.A未找到。继续执行!
$
$
$
$ javac -cp src -d classes src/packageB/B.java
$ java -cp classes Main
这是来自packageA.A的内容:World
这是来自匿名内部类的内容:Hello

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

为了帮助您将我的解决方案适应于您的问题,这里有一个链接,指导正确设置Nimbus外观:

Nimbus外观


1

好主意,但是有没有更简单的方法呢? - java.is.for.desktop
1
我不这么认为。JDK内置的最接近的东西是Proxy类,但它不会成为子类,因此没有帮助。最终,必须动态创建一个新类,这意味着生成该类的字节码,并通过ClassLoader加载它。Java在没有第三方库(如BCEL)的情况下提供的选项不多。 - Kirk Woll
是的,代理只能用于接口的实现。 - java.is.for.desktop

0
你可以使用 Class 类来实现这个功能。

I.E.:

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

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


1
我正在考虑这个问题,但是我该如何管理动态加载类的方法覆盖(从而创建一个匿名内部子类)? - java.is.for.desktop

-1
嗯,你不能将想要扩展的类放入编译时类路径中,像平常一样编写你的子类,然后在运行时显式地触发加载子类,并处理链接器抛出的指示超类丢失的任何异常吗?

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接