一、java代码动态注入

在本文中,我们将研究如何将Java代码动态加载到正在运行的jvm中。 该代码可能是全新的,或者我们可能想更改程序中某些现有代码的功能。

(在开始之前,您可能想知道为什么到底有人会这样做。显而易见的示例是规则引擎之类的东西。规则引擎希望为用户提供添加或更改规则的能力,而不必重新启动规则。您可以通过将DSL脚本作为规则注入规则库来执行此操作,这种方法的真正问题在于,必须对DSL脚本进行解释,使其运行起来极其缓慢。然后可以像程序中的任何其他代码一样编译和运行该程序,效率将提高几个数量级。

在《纪事报》中,我们在新的微秒微服务/算法容器的核心中使用了这个想法。

我们将要使用的库是Chronicle开源库Java-Runtime-Compiler 。

从下面的代码中您将看到,该库的使用极其简单-实际上,它实际上只需要几行。 创建一个CachedCompiler,然后调用loadFromJava。 (有关实际最简单的用例,请参见此处的文档。)

下面列出的程序执行以下操作:

创建一个线程,该线程每秒调用一次Strategy。 该战略的投入为10和20。
加载将两个数字相加的策略
等待3秒
加载从另一个数中减去一个数的策略
这是完整的代码清单:

package test;
 
import net.openhft.compiler.CachedCompiler;
 
/**
 * Loads the addingStrategy and then after 3s replaces it with the 
 * subtractingStrategy.
 */
public class DynamicJavaClassLoading {
 
    private final static String className = "test.MyClass";
    private final static String addingStrategy = "package test;\n" +
            "import test.DynamicJavaClassLoading.Strategy;\n" +
            "public class MyClass implements Strategy {\n" +
            "    public int compute(int a, int b) {\n" +
            "        return a+b;\n" +
            "    }\n" +
            "}\n";
 
    private final static String subtractingStrategy = "package test;\n" +
            "import test.DynamicJavaClassLoading.Strategy;\n" +
            "public class MyClass implements Strategy {\n" +
            "    public int compute(int a, int b) {\n" +
            "        return a-b;\n" +
            "    }\n" +
            "}\n";
    
    public static void main(String[] args) throws Exception {
        StrategyProxy strategy = new StrategyProxy();
 
        //Thread calling the strategy once a second
        Thread t = new Thread(() -> {
            while (true) {
                System.out.println(strategy.compute(10,20));
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
 
        {
            ClassLoader cl = new ClassLoader() {
            };
            CachedCompiler cc = new CachedCompiler(null, null);
            Class aClass = cc.loadFromJava(cl, className, addingStrategy);
 
            Strategy runner = (Strategy) aClass.newInstance();
            strategy.setStratgey(runner);
        }
 
        Thread.sleep(3000);
 
        {
            ClassLoader cl = new ClassLoader() {
            };
            CachedCompiler cc = new CachedCompiler(null, null);
            Class aClass = cc.loadFromJava(cl, className, subtractingStrategy);
 
            Strategy runner = (Strategy) aClass.newInstance();
            strategy.setStratgey(runner);
        }
    }
 
    public interface Strategy{
        int compute(int a, int b);
    }
 
    public static class StrategyProxy implements Strategy{
        private volatile Strategy underlying;
 
        public void setStratgey(Strategy underlying){
            this.underlying = underlying;
        }
 
        public int compute(int a, int b){
            Strategy underlying = this.underlying;
            return underlying == null ? Integer.MIN_VALUE : underlying.compute(a, b);
        }
    }
}

这是输出(蓝色注释):

The strategy has not been loaded yet. underlying in the StrategyProxy is null so Integer.MIN_VALUE is returned
-2 1 4 7 4 8 3 6 4 8
The adding strategy has been loaded 10+20=30
30
30
30
After 3s the subtracting strategy is loaded. It replaces the adding strategy. 10-20=-10
-10
-10
-10
-10
 
-10

请注意,在每次加载策略时,在代码中我们都创建了一个新的ClassLoader和一个CachedCompiler。 这样做的原因是,ClassLoader一次只能加载一个特定类的一个实例。

如果仅使用该库来加载新代码,则可以这样做,而无需创建ClassLoader(即使用默认的ClassLoader)和CachedCompiler。

二、class热加载

还有一种需求,就是将已编译好的class文件,让jvm热加载,供其他模块使用,这个过程也是要求程序不可以重启;
此时需要我们自定义类加载器,新建MyClassLoads类,让其继承ClassLoader类,然后重写defineClass类:
策略: 上传class文件–>后台接收后类加载进行加载–>完成加载后放在一个map中(类全路径为key,class为value)–>调用者从map中获取class,利用反射调用其方法
代码:

package com.zf.load.classloadwar.hotload;

import com.zf.load.classloadwar.controller.LoadController;

import java.io.*;

/**
 * 类加载器
 */
public class MyClassLoads extends ClassLoader {

    private static MyClassLoads instance = null;
    static byte[] byteArray = null;

    public MyClassLoads(ClassLoader parent) {
        super(parent); //这个很重要解决类加载作用域问题
    }

    //获取MyClassLoads的唯一方法
    public static MyClassLoads GetInstance(byte[] content, ClassLoader parent) {
        try {
            instance = new MyClassLoads(parent);
            MyClassLoads.byteArray = content;
        } catch (Exception e) {
            System.out.println(e.toString());
        }
        return instance;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            Class<?> myclass = findLoadedClass(name);
            if (myclass == null) {
                myclass = defineClass(null, MyClassLoads.byteArray, 0, MyClassLoads.byteArray.length);
                return myclass;
            } else {
                return myclass;
            }
        } catch (Exception e) {
            System.out.println(e.toString());
            e.printStackTrace();
            return null;
        }
    }

    public Class<?> addLoadClassToMap(String className) {
        Class<?> abc = null;
        try {
            abc = MyClassLoads.GetInstance(byteArray, this.getClass().getClassLoader()).findClass(className);
			LoadController.classMap.put(className,abc);
			System.err.println(abc.getName()+ "成功被热加载");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return abc;
    }


    /**
     * @param classPaht
     * @return Object
     * 热加载新的Class类
     */
    public Object FindNewClass(String classPaht) {
        try {
            byte[] b = getBytes(classPaht);
            return defineClass(null, b, 0, b.length).newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 热加载新的Class类
     */
    public Object loadNewClass(byte[] content) {
        try {
            return defineClass(null, content, 0, content.length).newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }


    /**
     * @param o
     * @return T
     * 包装返回类对象
     */
    public static <T> T ReLoadClass(Object o) {
        return (T) o;
    }


    /**
     * @param filename
     * @return Byte[]
     * @throws IOException 返回class文件的Byte
     */
    private byte[] getBytes(String filename) throws IOException {
        File file = new File(filename);
        byte raw[] = new byte[(int) file.length()];
        FileInputStream fin = new FileInputStream(file);
        fin.read(raw);
        fin.close();
        return raw;
    }
}
package com.zf.load.classloadwar.hotload;

/**
 *  类热加载后回调接口
 */
public interface RefChange {
	void ReLoadClass(Object j);
}
package com.zf.load.classloadwar.hotload;

import com.zf.load.classloadwar.controller.LoadController;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Component
public class ReloadClass implements RefChange {

    @Override
    public void ReLoadClass(Object j) {
        String name = j.getClass().getName();
        System.out.println(name);
        Object o = MyClassLoads.ReLoadClass(j);
        System.err.println(name+ "成功被热加载");
        Class clazz1 = null;
        try {
            clazz1 = o.getClass();
            LoadController.classMap.put(name,clazz1);

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

然后是我们用来测试使用的父类和子类:

package com.zf.load.classloadwar.hotload;

public class FatherManager {

    public String getName(){
        return "haha";
    }
}
package com.zf.load.classloadwar.hotload;

public class MyManager extends FatherManager{

    public String getName(){
        return "nihao";
    }
}

主要controller类:

package com.zf.load.classloadwar.controller;


import com.zf.load.classloadwar.hotload.MyClassLoads;
import com.zf.load.classloadwar.hotload.MyManager;
import com.zf.load.classloadwar.hotload.ReloadClass;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("file")
public class LoadController {

    private Object ClassTemp;

    @Autowired
    private ReloadClass reloadClass;

    public static Map<String, Class<?>> classMap = new HashMap<String, Class<?>>();

    @PostMapping(value = "/fileUpload")
    public String fileUpload(@RequestParam(value = "file") MultipartFile file, String className) {
        if (file.isEmpty()) {
            System.out.println("文件为空");
        }
        String fileName = file.getOriginalFilename(); // 文件名
        try {
            byte[] bytes = file.getBytes();
            Class<?> aClass =
                    MyClassLoads.GetInstance(bytes, this.getClass().getClassLoader()).addLoadClassToMap(className);
            System.out.println(aClass.getName());

        } catch (Exception e) {
            e.printStackTrace();
        }
        return fileName;
    }


    @GetMapping(value = "/getName")
    public String getName() {
        Class<?> aClass = classMap.get("com.zf.load.classloadwar.hotload.MyManager");
        Method method = null;
        String result = "";
        try {
            method = aClass.getMethod("getName");
            method.setAccessible(true);
            Object obj1 = method.invoke(aClass.newInstance());
            result = String.valueOf(obj1);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        return result;
    }

    @GetMapping(value = "/getTestName")
    public String getTestName() {
        MyManager myManager = new MyManager();
        String name = myManager.getName();
        return name;
    }

    @GetMapping(value = "/testWar")
    public String testWar() {
        return  "nihao";
    }


}

下面我们测试一下:

首先我们准备一个编译好的class文件,在本程序中测试类为MyManager;

java 动态添加类成员 java动态加载class文件_System


将此类编译为class文件(clean complie一下),作为准备;再将此类在程序中修改为

java 动态添加类成员 java动态加载class文件_加载_02

,然后启动程序:

1)postman执行“localhost:8081/load/file/getTestName”:

得到:

java 动态添加类成员 java动态加载class文件_java_03


2)然后上传我们准备好的class文件,再执行“localhost:8081/load/file/getName”;

java 动态添加类成员 java动态加载class文件_加载_04


java 动态添加类成员 java动态加载class文件_java_05


可以看到,我们上传的class文件成功被加载了;

注意: 编写自定义类加载时,一定要继承一下父类的类加载器,不然当程序打包为war包放在tomcat运行时(jar包运行无误),热加载会失败,因为在tomcat中类加载不遵循双亲委派机制;