How do I replace a class with new one with Java instrumentation?

倖福魔咒の 提交于 2019-12-05 15:46:35

Another Approach to your problem is using endorsed dirs. Keep classes that you want to be loaded in a library and provide -Djava.endorsed.dirs=<directory_path> as JVM argument to the program.

While loading classes, JVM first checks the class availability in this directory and if not found then it will check the application classes. This works perfectly fine without any issue and without any coding.

  1. Your UrlClassLoader should contain the jar file from which you want to load the class.
  2. If the class is already loaded, then it cant be reloaded.
  3. Instrumentation, and redefining of class works only when the class is being loaded to JVM.

The code looks good, but you need to double check if the urlClassLoader contains the jar file from which you want to load the class and jar has the required class.

You can debug the application to ensure the above conditions.

I managed to fix the issue. In case someone has the same issue here was the problem:

I was using ClassReader and ClassWriter. For some reason, ClassWriter were stuffing the byte code, perhaps it was my mistake to pass already compiled class to class writer but anyway the following code:

    ClassReader reader = new ClassReader(is);
    ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
    byte[] content = writer.toByteArray();

Replaced with:

        InputStream is = urlClassLoader.getResourceAsStream(className + ".class");

        byte[] content = new byte[is.available()];
        is.read(content);

        System.out.println ("Original class version: " + ((classfileBuffer[6]&0xff)<<8 | (classfileBuffer[7]&0xff)));
        System.out.println ("Redefined class version: " + ((content[6]&0xff)<<8 | (content[7]&0xff)));

As you can see I am using InputStream to retrieve the byte code directly. That fixed the issue and in case you are interested, Spring detected the difference nicely and refreshed the context.

==== EDIT ====

I noticed using URLClassLoader here is not reliable as for some reason, it may return the already loaded class in the application itself and not the class inside the JAR file. It was random, sometimes returns the class inside the jar and sometimes the original class so I have decided to remove the URLClassLoader and instead get the class file as InputStream while traversing the jar file. This is the final code of my transformer for anyone who needs it:

public class JarFileClassTransformer implements ClassFileTransformer {

private String jarFileName = null;
protected Map<String, InputStream> classNames = new HashMap<>();
static Instrumentation instrumentation = null;

/**
 * Constructor.
 * @param jarFileName
 */
public JarFileClassTransformer(String jarFileName) {
    this.jarFileName = jarFileName;

    File file  = new File(jarFileName);
    System.out.println("Jar file '" + this.jarFileName + "' " + (file.exists() ? "exists" : "doesn't exists!"));

    if(file.exists()) {
        try {
            JarFile jarFile = new JarFile(file);
            Enumeration e = jarFile.entries();

            while (e.hasMoreElements()) {
                JarEntry je = (JarEntry) e.nextElement();
                if(je.isDirectory() || !je.getName().endsWith(".class")){
                    continue;
                }
                // -6 because of .class
                String jarClassName = je.getName().substring(0,je.getName().length()-6);
                jarClassName = jarClassName.replace('/', '.');
                System.out.println("Adding class " + jarClassName);
                this.classNames.put(jarClassName, jarFile.getInputStream(je));

            }
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

}

@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {

    if(classNames.containsKey(className.replace("/", "."))) {
        System.out.format("\n==> Found %s to replace with the existing version\n", className);
        try {

            Class c = loader.loadClass(className.replace("/", "."));
            System.out.println("Existing class: " + c);
            InputStream is = classNames.get(className.replace("/", "."));

            byte[] content = new byte[is.available()];
            is.read(content);

            System.out.println("Original class version: " + ((classfileBuffer[6]&0xff)<<8 | (classfileBuffer[7]&0xff)));
            System.out.println("Redefined class version: " + ((content[6]&0xff)<<8 | (content[7]&0xff)));

            System.out.println("Original bytecode: " + new String(classfileBuffer));
            System.out.println("Redefined byte code: " + new String(content));
            ClassDefinition cd = new ClassDefinition(c, content);
            instrumentation.redefineClasses(cd);

            return content;
        } catch (Throwable e) {
            e.printStackTrace();

        }

    }
    return classfileBuffer;
}

}

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!