Java Adding field and method to compiled class and reload using class loader

676 Views Asked by At

I would like to add field along with its getter/setter in compiled Java classes which is loaded in a Spring boot application. I was able to modify the class using JavaAssist and ASM. But the problem is it is not letting me reload the class after modification, since this is already been loaded. I tried to write a class extending java.lang.ClassLoader but custom classloader is not getting called. Also, I checked java's Instrumentation API which clearly states

The retransformation may change method bodies, the constant pool and attributes. The retransformation must not add, remove or rename fields or methods, change the signatures of methods, or change inheritance. These restrictions maybe be lifted in future versions. The class file bytes are not checked, verified and installed until after the transformations have been applied, if the resultant bytes are in error this method will throw an exception.

Could you please let me know how to achieve this? I am open to runtime vs compile time modification. If you can share some examples that will be great. Subclassing may not be an option because this class will be used by third-party jars on which we don't have any control and this jar will us the class from the class pool. Also, could you please let me know how to use custom class loader?

Technology

Java - JDK 8
Spring Boot - 2.x
Spring 5
Bytecode manipulation - ASM or JavaAssist

I would like to achieve below

From

class A {
 Integer num;
}

To

class A {
    Integer num;
    //Newly added field
    private String numModified; 
    //Newly added method
    public String getNumModified(){}
    public String setNumModified(String numModified){}
}

When trying to load the class using below methods

private static Class loadClass(byte[] b,String className) {
        // Override defineClass (as it is protected) and define the class.
        Class clazz = null;
        try {
            ClassLoader loader = ClassLoader.getSystemClassLoader();
            Class cls = Class.forName("java.lang.ClassLoader");
            java.lang.reflect.Method method =
                    cls.getDeclaredMethod(
                            "defineClass", 
                            new Class[] { String.class, byte[].class, int.class, int.class });

            // Protected method invocation.
            method.setAccessible(true);
            try {
                Object[] args = 
                        new Object[] { className, b, new Integer(0), new Integer(b.length)};
                clazz = (Class) method.invoke(loader, args);
            } finally {
                method.setAccessible(false);
            }
        } catch (Exception e) {
            e.printStackTrace();
            //System.exit(1);
        }
        return clazz;
    }

Exception

java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
    at java.lang.reflect.Method.invoke(Unknown Source)
    at com.test.pii.mask.util.ClassModifier.loadClass(ClassModifier.java:110)
    at com.test.pii.mask.util.ClassModifier.modifyClass(ClassModifier.java:85)
    at com.test.pii.mask.util.ClassModifier.main(ClassModifier.java:200)
Caused by: java.lang.LinkageError: loader (instance of  sun/misc/Launcher$AppClassLoader): attempted  duplicate class definition for name: "com/test/pii/web/dto/SomeOther"
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClass(Unknown Source)
    at java.lang.ClassLoader.defineClass(Unknown Source)
    ... 7 more

Custom Class Loader which is not getting called

public class PIIClassLoader extends ClassLoader {

    static {
        ClassLoader.registerAsParallelCapable();
    }

    /**
     * 
     */
    public PIIClassLoader() {
        super();
    }

    /**
     * @param parent
     */
    public PIIClassLoader(ClassLoader parent) {
        super(parent);
    }


    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }

    @Override
    protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException {
        // respect the java.* packages.
        if( name.startsWith("java.")) {
            return super.loadClass(name, resolve);
        }
        else {
            // see if we have already loaded the class.
            if(Foo.class.getName().equals(name)) {
                return null;
            }

            Class<?> c = findLoadedClass(name);
            if( c != null ) return c;
        }
        return null;
    }
}
0

There are 0 best solutions below