1

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;
    }
}
Debopam
  • 3,198
  • 6
  • 41
  • 72
  • This seems impossible, but maybe there's some way it can be done that I don't understand. If you are adding or removing fields, what happens to existing objects of this type? In my opinion, if you need this kind of functionality, the way to do it would be to use JavaScript / ScriptEngine. It's a snap in JavaScript – ControlAltDel May 19 '20 at 23:53
  • 2
    The restriction in the Instrumentation API exists because the JVM does not support such changes. The `ClassLoader` does not support altering the definition of an already loaded class at all. If it did, we wouldn’t need the Instrumentation API… – Holger May 20 '20 at 09:04
  • Is there any way to prevent the class loader to load the files? That way code can modify the class and load it. – Debopam May 20 '20 at 17:05
  • 1
    If you're using an Instrumentation (e.g. javaagent), the bytecode of any class will be passed to you before it gets initially loaded. You can alter it as you want and return your modified bytecode that gets loaded instead. (see https://docs.oracle.com/javase/7/docs/api/java/lang/instrument/Instrumentation.html#addTransformer(java.lang.instrument.ClassFileTransformer) and implement the ClassFileTransformer interface) – Aki May 27 '20 at 06:12

0 Answers0