目标

通过Java字节码技术,实现对代码的动态修改,不需要重启服务或者热替换,即可实现业务功能的逻辑修改!

自定义类加载器

将字节数组转换为类class的实例,根据指定的字节数据创建指定名称的Class对象

/**
 * 自定义类加载器
 *
 * @author huxiang
 */
public class BizClassLoader extends ClassLoader {

    /**
     * 重写类加载器方法
     *
     * @param fullName 全限定类名
     * @param javaClassObject class对象
     * @return
     */
    public Class loadClass(String fullName, BizJavaClassFileObject javaClassObject) {
        byte[] classData = javaClassObject.getBytes();
        return this.defineClass(fullName, classData, 0, classData.length);
    }
}

自定义字节码文件类

主要是为了重写openOutStream()方法,不输出字节码文件到文件,而是直接保存在一个字节输出流中

import javax.tools.JavaFileObject;
import javax.tools.SimpleJavaFileObject;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;

/**
 * 自定义字节码文件类,重写openOutStream()方法,不输出字节码文件到文件,而是直接保存在一个输出流中
 *
 * @author huxiang
 */
public class BizJavaClassFileObject extends SimpleJavaFileObject {
    private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

    public BizJavaClassFileObject(String name, JavaFileObject.Kind kind) {
        super(URI.create("string:///" + name.replace('.', '/') + kind.extension), kind);
    }

    public byte[] getBytes() {
        return outputStream.toByteArray();
    }

    //编译时候会调用openOutputStream获取输出流,并输出数据
    @Override
    public OutputStream openOutputStream() throws IOException {
        return outputStream;
    }
}

自定义文件管理器

用于操作Java源码文件和类文件的抽象文件工具

import javax.tools.FileObject;
import javax.tools.ForwardingJavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;

/**
 * 自定义文件管理器,重写getJavaFileOutput()方法,输出我们自定义的Java字节码对象。
 *
 * @author huxiang
 */
public class BizFileManager extends ForwardingJavaFileManager {

    private BizJavaClassFileObject javaClassObject;

    protected BizFileManager(StandardJavaFileManager standardJavaFileManager) {
        super(standardJavaFileManager);
    }

    public BizJavaClassFileObject getJavaClassObject() {
        return javaClassObject;
    }

    @Override
    public JavaFileObject getJavaFileForOutput(Location location, String className,
                                               JavaFileObject.Kind kind, FileObject sibling) {
        //生命周期内的Java编译对象,尽量复用,不要直接new
        if (this.javaClassObject == null) {
            this.javaClassObject = new BizJavaClassFileObject(className, kind);
        }
        return javaClassObject;
    }
}

自定义源码文件类

实现SimpleJavaFileObject 方法,SimpleJavaFileObject 为JavaFileObject中的大多数方法提供简单的实现,虽然JavaFileObject实现的源码文件基类很多,但是一般情况下使用SimpleJavaFileObject 足以处理90%以上业务的功能

import javax.tools.SimpleJavaFileObject;
import java.io.IOException;
import java.net.URI;

/**
 * 自定义源码文件类,输出我们自己写的Java字节码文件类
 *
 * @author huxiang
 */
public class BizSimpleJavaFileObject extends SimpleJavaFileObject {

    /**
     * 源码文件内容
     */
    private String contents = null;

    /**
     * 全限定类名
     */
    private String className;

    /**
     * Kind.SOURCE表示要创建到内存中的源码文件为.java
     *
     * @param className
     * @param contents
     */
    public BizSimpleJavaFileObject(String className, String contents) {
        super(URI.create("string:///" + className.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE);
        this.className = className;
        this.contents = contents;
    }

    public CharSequence getCharContent(boolean ignoredEncodingErrors) throws IOException {
        return contents;
    }

    public String getClassName() {
        return className;
    }
}

自定义动态编译器

对外入口,一般建议加载到Spring容器中,实现方法的invoke

import com.paratera.console.common.BizException;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import javax.tools.*;
import java.io.*;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * 自定义动态编译器,invoke为入口,包括解析代码字符串,将代码字符串转换为Class,Object可执行对象,然后反射调用目标方法
 *
 * @author huxiang
 */
@Component
@Order(value = 2)
public class DynamicCompiler {
    /**
     * 编译出Class对象并加载到JVM内存
     *
     * @param fullClassName 全路径的类名
     * @param javaCode      java代码
     * @return 目标类
     */
    private Class<?> compileToClass(String fullClassName, String javaCode) throws Exception {
        //获取环境Java编译器
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        //编译文件仓库对象
        DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
        //编码设置
        List<String> options = new ArrayList<>();
        options.add("-encoding");
        options.add("UTF-8");
        //文件管理器对象,将文件仓库管理起来
        BizFileManager fileManager = new BizFileManager(compiler.getStandardFileManager(diagnostics, null, null));
        //需要编译成.java的文件对象,如果有多个,需要多次添加,我们这边就一个,并且限定了全路径类名和Java代码字符串
        List<JavaFileObject> jfiles = new ArrayList<>();
        jfiles.add(new BizSimpleJavaFileObject(fullClassName, javaCode));
        //编译任务开启,正常这一步是JVM虚拟机调度线程自动执行
        JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, diagnostics, options, null, jfiles);
        //编译任务执行
        boolean success = task.call();
        if (success) {
            //编译成功,加载class对象至内存(而不是生成实实在在的.class文件到磁盘)
            BizJavaClassFileObject javaClassObject = fileManager.getJavaClassObject();
            BizClassLoader dynamicClassLoader = new BizClassLoader();
            return dynamicClassLoader.loadClass(fullClassName, javaClassObject);
        } else {
            //编译出错(比如字符串非标准Java格式时异常)
            for (Diagnostic diagnostic : diagnostics.getDiagnostics()) {
                String error = compileError(diagnostic);
                throw new RuntimeException(error);
            }
            throw new RuntimeException("编译失败!");
        }
    }

    /**
     * 编译错误信息异常报告
     *
     * @param diagnostic
     * @return
     */
    private String compileError(Diagnostic diagnostic) {
        StringBuilder res = new StringBuilder();
        res.append("LineNumber:[").append(diagnostic.getLineNumber()).append("]\n");
        res.append("ColumnNumber:[").append(diagnostic.getColumnNumber()).append("]\n");
        res.append("Message:[").append(diagnostic.getMessage(null)).append("]\n");
        return res.toString();
    }

    /**
     * 目标方法反射执行
     *
     * @param methodName
     * @param args
     * @return
     */
    public Object invoke(String methodName, String... args) throws BizException {
        //获取字符串代码内容
        StringBuilder file = getFileInfo();
        Class<?> clazz = null;
        Object obj = null;
        try {
            //字节码编译处理,得到Class对象和执行对象
            clazz = this.compileToClass("com.paratera.console.biz.handler.compiler.Compiler", file.toString());
            obj = clazz.getDeclaredConstructor().newInstance();
        } catch (Exception e) {
            throw new Exception("反射获取对象实例异常:" + (StringUtils.isBlank(e.getMessage()) ? ((InvocationTargetException) e).getTargetException().getMessage() : e.getMessage()));
        }
        //反射调用目标方法
        Method[] test = clazz.getDeclaredMethods();
        List<Method> methods = Arrays.stream(test).filter(app ->
                StringUtils.equals(app.getName(), methodName)).toList();
        for (Method method : methods) {
            Parameter[] parameters = method.getParameters();
            //无参方法执行
            Boolean noArgs = (parameters == null && args == null) || (parameters.length == 0 && args.length == 0);
            if(noArgs){
                try {
                    return method.invoke(obj, args);
                }catch (Exception e){
                    throw new Exception("反射调用对象方法失败:" + (StringUtils.isBlank(e.getMessage())?((InvocationTargetException) e).getTargetException().getMessage():e.getMessage()));
                }
            }
            //参数个数不相同,退出当前循环
            if(parameters.length != args.length){
                continue;
            }
            //如果参数个数相同,匹配参数类型
            Boolean flag = true;
            for (int i = 0; i < args.length; i++) {
                //如果传参类型和method参数类型有一个不匹配,即退出
                if(!StringUtils.equals(parameters[i].getType().getName(),args[i].getClass().getName())){
                    flag = false;
                    break;
                }
            }
            if(flag){
                try {
                    return method.invoke(obj, args);
                }catch (Exception e){
                    throw new Exception("反射调用对象方法失败:" + (StringUtils.isBlank(e.getMessage())?((InvocationTargetException) e).getTargetException().getMessage():e.getMessage()));
                }
            }
        }
        return "反射调用对象没有任何可执行的目标方法,请检查动态编译类!";
    }

    /**
     * 读取文件内容
     *
     * @return
     */
    @NotNull
    private StringBuilder getFileInfo() throws BizException {
        StringBuilder file = new StringBuilder();
        InputStream is = this.getClass().getResourceAsStream("/compiler.txt");
        InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8);
        BufferedReader br = new BufferedReader(isr);
        String line;
        while (true) {
            try {
                if (!((line = br.readLine()) != null)) {
                    break;
                }
            } catch (IOException e) {
                throw new BizException("IO流异常-1:" + e.getMessage());
            }
            file.append(line).append("\n");
        }
        try {
            br.close();
            isr.close();
            is.close();
        } catch (IOException e) {
            throw new BizException("IO流异常-2:" + e.getMessage());
        }
        return file;
    }
}

代码文件

将可执行程序内容复制到resources/compiler.txt文件中,注意一定要是Java标准格式,比如”//“注释不要出现

import lombok.extern.slf4j.Slf4j;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Slf4j
public class Compiler {

    /**
     * 解析1
     *
     * @param message
     * @return
     */
    public List<Map> getQuota1(String message) {
        List<Map> lstMap = new ArrayList<>();
        String[] splits = message.split("\n");
        for (int i = 0; i < splits.length; i++) {
            if (i == 0 || i == 1) {
                continue;
            }
            String mess = splits[i];
            String[] splitss = mess.trim().split("\\s+");
            Map map = new HashMap();
            map.put("name", splitss[0]);
            map.put("limit", splitss[3]);
            map.put("used", splitss[1]);
            lstMap.add(map);
        }
        return lstMap;
    }

    /**
     * 解析2
     *
     * @param message
     * @return
     */
    public List<Map> getQuota2(String message) {
        List<Map> lstMap = new ArrayList<>();
        String[] splits = message.split("\n");
        String unit = "";
        for (int i = 0; i < splits.length; i++) {
            if (i == 0 || i == 1) {
                continue;
            }
            String[] splitss = splits[i].trim().split("\\s+");
            Map map = new HashMap();
            map.put("name", splitss[0]);
            map.put("limit", splitss[4]);
            map.put("used", splitss[2]);
            lstMap.add(map);
        }
        return lstMap;
    }

    /**
     * 解析3
     *
     * @param message
     * @return
     */
    public List<Map> getQuota3(String message) {
        List<Map> lstMap = new ArrayList<>();
        String[] splits = message.split("\n");
        for (int i = 0; i < splits.length; i++) {
            if (i == 0 || i == 1) {
                continue;
            }
            String[] splitss = splits[i].trim().split("\\s+");
            Map map = new HashMap();
            map.put("name", splitss[0]);
            map.put("limit", splitss[4]);
            map.put("used", splitss[3]);
            lstMap.add(map);
        }
        return lstMap;
    }

    /**
     * 解析4
     *
     * @param message
     * @return
     */
    public List<Map> getQuota4(String message) {
        List<Map> lstMap = new ArrayList<>();
        String[] splits = message.split("\n");
        DecimalFormat df = new DecimalFormat("0");
        for (int i = 0; i < splits.length; i++) {
            String[] splitss = splits[i].trim().split("\\s+");
            Map map = new HashMap();
            map.put("name", splitss[0]);
            String limitStr = splitss[4];
            String limit = limitStr.substring(limitStr.indexOf(":") + 1, limitStr.length() - 2);
            map.put("limit", df.format(Double.valueOf(limit) * 1024 * 1024));
            String usedStr = splitss[3];
            String used = usedStr.substring(1, usedStr.length() - 2);
            map.put("used", df.format(Double.valueOf(used) * 1024 * 1024));
            lstMap.add(map);
        }
        return lstMap;
    }
}

单元测试用例

dynamicCompiler.invoke传入需要动态执行的方法名以及参数即可,参数可多个,根据目标方法来即可

import com.paratera.console.biz.Application;
import com.paratera.console.biz.handler.compiler.DynamicCompiler;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest(
        classes = Application.class,
        args = {"--spring.config.import=configserver:http://localhost:20010", "--spring.profiles.active=local"},
        webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
)
public class TestCompiler {

    @Autowired
    private DynamicCompiler dynamicCompiler;

   @Test
    public void getQuota1() throws Exception {
        Object obj = dynamicCompiler.invoke("getQuota1", """
                Disk quotas for user pp824 (uid 6555):
                                 Filesystem    used   quota   limit   grace   files   quota   limit   grace
                                /PARA/pp824  101500000     102400000  204800000       - 61739953       0       0       -
                """);
        System.out.println(obj);
    }

    @Test
    public void getQuota2() throws Exception {
        Object obj = dynamicCompiler.invoke("getQuota2", """
                                         Block Limits                                    |     File Limits
                Filesystem type             KB      quota      limit   in_doubt    grace |    files   quota    limit in_doubt    grace  Remarks
                ssd        USR               0 1048576000 2097152000          0     none |        1 9000000 10000000        0     none njugpfs_ssd.ssd02
                """);
        System.out.println(obj);
    }

    @Test
    public void getQuota3() throws Exception {
        Object obj = dynamicCompiler.invoke("getQuota3", """
                Block Limits                                               |     File Limits
                Filesystem Fileset    type             KB      quota      limit   in_doubt    grace |    files   quota    limit in_doubt    grace  Remarks
                dssg       root       USR       435054592 1048576000 1048576000          0     none |  1729156       0        0        0     none\s
                """);
        System.out.println(obj);
    }

    @Test
    public void getQuota4() throws Exception {
        Object obj = dynamicCompiler.invoke("getQuota4", """
                Home   DiskUsage  :  [150G]             (Limit:300G)
                Work1  DiskUsage  :  [377G]             (Limit:6.0T)
                """);
        System.out.println(obj);
    }
}