Java: How do I override a method of a class dynamically (class is eventually NOT in classpath)?

14,455

Solution 1

Get to know it that class is available (at runtime)
Put the usage in a try block ...

If it's not the case, skip the whole thing
... and leave the catch block empty (code smell?!).

How do I manage to override a method of a dynamically-loaded class
Just do it and make sure the compile-time dependency is satisfied. You are mixing things up here. Overriding takes place at compile time while class loading is a runtime thing.

For completeness, every class you write is dynamically loaded by the runtime environment when it is required.

So your code may look like:

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);
   }
}

Solution 2

Use BCEL to generate your dynamic subclass on the fly.

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

Solution 3

The follow code should solve your problem. The Main class simulates your main class. Class A simulates the base class you want to extend (and you have no control of). Class B is the derived class of class A. Interface C simulates "function pointer" functionality that Java does not have. Let's see the code first...

The following is class A, the class you want to extend, but have no control of:


/* 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);
    }
}

The following is class B, the dummy derived class. Notice that, since it extends A, it must import packageA.A and class A must be available at the compile time of class B. A constructor with parameter C is essential, but implementing interface C is optional. If B implements C, you gain the convenience to call the method(s) on an instance of B directly (without reflection). In B.doSomething(), calling super.doSomething() is optional and depends on whether you want so, but calling c.doSomething() is essential (explained below):


/* 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);
    }
}

The following is the tricky interface C. Just put all the methods you want to override into this interface:


/* src/packageC/C.java */

package packageC;

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

The following is the main class:


/* 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");
        }
    }
}

Why does it compile and run?
You can see that in the Main class, only packageC.C is imported, and there is no reference to packageA.A or packageB.B. If there is any, the class loader will throw an exception on platforms that don't have packageA.A when it tries to load one of them.

How does it work?
In the first Class.forName(), it checks whether class A is available on the platform. If it is, ask the class loader to load class B, and store the resulting Class object in classB. Otherwise, ClassNotFoundException is thrown by Class.forName(), and the program goes without class A.

Then, if classB is not null, get the constructor of class B that accepts a single C object as parameter. Store the Constructor object in constructorB.

Then, if constructorB is not null, invoke constructorB.newInstance() to create a B object. Since there is a C object as parameter, you can create an anonymous class that implements interface C and pass the instance as the parameter value. This is just like what you do when you create an anonymous MouseListener.

(In fact, you don't have to separate the above try blocks. It is done so to make it clear what I am doing.)

If you made B implements C, you can cast the B object as a C reference at this time, and then you can call the overridden methods directly (without reflection).

What if class A does not have a "no parameter constructor"?
Just add the required parameters to class B, like public B(int extraParam, C c), and call super(extraParam) instead of super(). When creating the constructorB, also add the extra parameter, like classB.getConstructor(Integer.TYPE, C.class).

What happens to String s and String t?
t is used by the anonymous class directly. When objectB.doSomething("World"); is called, "World" is the s supplied to class B. Since super can't be used in the anonymous class (for obvious reasons), all the code that use super are placed in class B.

What if I want to refer to super multiple times?
Just write a template in B.doSomething() like this:


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

Of course, you have to modify interface C to include doSomethingAfter1() and doSomethingAfter2().

How to compile and run the code?

$ 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

In the first run, the class packageB.B is not compiled (since Main.java does not have any reference to it). In the second run, the class is explicitly compiled, and thus you get the result you expected.

To help you fitting my solution to your problem, here is a link to the correct way to set the Nimbus Look and Feel:

Nimbus Look and Feel

Share:
14,455
java.is.for.desktop
Author by

java.is.for.desktop

Updated on July 20, 2022

Comments

  • java.is.for.desktop
    java.is.for.desktop almost 2 years

    How do I call a method of a class dynamically + conditionally?
    (Class is eventually not in classpath)

    Let's say, I need the class NimbusLookAndFeel, but on some systems it's not available (i.e. OpenJDK-6).

    So I must be able to:

    • Get to know it that class is available (at runtime),
    • If it's not the case, skip the whole thing.
    • How do I manage to override a method of a dynamically-loaded class
      (thus creating an anonymous inner sub-class of it)?

    Code example

    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:
    Now I edited my code, as it was suggested, to intercept NoClassDefFoundError using try-catch. It fails. I don't know, if it's OpenJDK's fault. I get InvocationTargetException, caused by NoClassDefFoundError. Funny, that I can't catch InvocationTargetException: It's thrown anyway.

    EDIT2::
    Cause found: I was wrapping SwingUtilities.invokeAndWait(...) around the tested method, and that very invokeAndWait call throws NoClassDefFoundError when loading Nimbus fails.

    EDIT3::
    Can anyone please clarify where NoClassDefFoundError can occur at all? Because it seems that it's always the calling method, not the actual method which uses the non-existing class.

  • java.is.for.desktop
    java.is.for.desktop almost 14 years
    I was thinking about that, but how do I manage overriding a method of a dynamically-loaded class (thus creating an anonymous inner sub-class of it)?
  • java.is.for.desktop
    java.is.for.desktop almost 14 years
    Good idea, but isn't it possible in a more simple way?
  • Kirk Woll
    Kirk Woll almost 14 years
    I don't think so. The closest thing built into the JDK is the Proxy class, but that won't be a subclass so doesn't help. Ultimately, a new Class has to be created on the fly, which means generating the bytecode of that class, and loading it through a ClassLoader. Java doesn't provide many options for this without a third party library such as BCEL.
  • java.is.for.desktop
    java.is.for.desktop almost 14 years
    Good idea! I was not only executing, but also compiling the code under OpenJDK 6, so I got confused by compile-time errors.
  • java.is.for.desktop
    java.is.for.desktop almost 14 years
    Yes, proxies are sadly only there for implementing interfaces.
  • java.is.for.desktop
    java.is.for.desktop almost 14 years
    ... of course, the optimal solution would enable me to compile the eventually non-existing class optionally.
  • Devanshu Mevada
    Devanshu Mevada almost 14 years
    Assuming this code has been compiled, it won't throw a ClassNotFoundException but a NoClassDefFoundError if NimbusLookAndFeel is not there at runtime.
  • java.is.for.desktop
    java.is.for.desktop almost 14 years
    @Pascal Thivent: Right! "exception ClassNotFoundException is never thrown in body of corresponding try statement". But are you sure that NoClassDefFoundError is thrown inside that method? (Not, for instance, at class' ctor, static ctor, ... or what ever...)
  • whiskeysierra
    whiskeysierra almost 14 years
    @java.is.for.desktop Did u try it?
  • java.is.for.desktop
    java.is.for.desktop almost 14 years
    Yes, as mentioned in the edited question, something seems to be wrong with OpenJDK-6: I get InvocationTargetException (caused by NoClassDefFoundError), which, strangely, can't be caught.
  • whiskeysierra
    whiskeysierra almost 14 years
    Ah, didn't see the new part in the question. Does the stacktrace indicate where the exception is coming from?
  • java.is.for.desktop
    java.is.for.desktop almost 14 years
    Now, found the cause, added to question (EDIT2).