动态编译 Java 源码为 Class

  • 一.背景
  • 1.Jdk 版本
  • 2.需求
  • 二.Java 源码动态编译实现
  • 1.Maven 依赖
  • 2.源码包装类
  • 3.Java 文件对象封装类
  • 4.文件管理器封装类
  • 5.类加载器
  • 6.类编译器
  • 三.动态编译测试
  • 1.普通测试类
  • 2.接口实现类
  • 3.测试
  • 四.用动态编译 Class 替换 SpringBoot 的 Bean(未完)


一.背景

1.Jdk 版本

版本查看命令:java -version

java 动态调jar包 java动态编译java文件_JavaCompiler

2.需求

本来想看下项目热部署的实现,比如 SpringBoot 不停机热加载 Jar 实现功能修改;后来看到 Jdk 支持源码动态编译,如果可以实现,那么就可以在线直接修改代码,再利用 SpringBoot 管理起来,替换旧的 Bean,实现功能修改。可能实际应用场景不多,可以做应急修改,线上服务最终还是需要把修改后的代码重新部署更为稳妥。

其实动态修改代码还可以通过 Arthas 实现,包括反编译、编译等更多功能

二.Java 源码动态编译实现

源码编译需要用到的关键类:


说明

JavaCompiler

编译器 ToolProvider.getSystemJavaCompiler();

SimpleJavaFileObject

文件对象类,可以表示源码、类文件

ClassLoader

顶层类加载器,抽象类

ForwardingJavaFileManager

文件管理器

项目结构如图

java 动态调jar包 java动态编译java文件_jdk_02

1.Maven 依赖

暂时只是一个 Maven 项目,未引入其他依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>DynamicDemo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>19</maven.compiler.source>
        <maven.compiler.target>19</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

</project>

2.源码包装类

基于 SimpleJavaFileObject 扩展,用于封装类名、源码信息

package org.example.demo.util;

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

/**
 * @author moon
 * @date 2023-02-15 20:32
 * @since 1.8
 */
public class CustomSourceCode extends SimpleJavaFileObject {

    /**
     * 类名称
     */
    private String className;

    /**
     * 类源码
     */
    private String contents;

    /**
     * 源码初始化
     * @param className
     * @param contents
     */
    public CustomSourceCode(String className, String contents) {
        super(URI.create("string:///" + className.replace('.', '/')
                + Kind.SOURCE.extension), Kind.SOURCE);
        this.contents = contents;
        this.className = className;
    }

    /**
     * 获取类名
     * @return
     */
    public String getClassName() {
        return className;
    }

    /**
     * 源码字符序列
     * @param ignoreEncodingErrors ignore encoding errors if true
     * @return
     * @throws IOException
     */
    public CharSequence getCharContent(boolean ignoreEncodingErrors)
            throws IOException {
        return contents;
    }
}

3.Java 文件对象封装类

基于 SimpleJavaFileObject 实现,封装了类名、类字节输出流信息

package org.example.demo.util;

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

/**
 * @author moon
 * @date 2023-02-15 20:52
 * @since 1.8
 */
public class CustomJavaFileObject extends SimpleJavaFileObject {

    /**
     * 类名称
     */
    private String className;

    /**
     * 输出的字节码流
     */
    private ByteArrayOutputStream toByteArray = new ByteArrayOutputStream();

    /**
     * Construct a SimpleJavaFileObject of the given kind and with the
     * given URI.
     *
     * @param className
     */
    public CustomJavaFileObject(String className) throws URISyntaxException {
        super(new URI(className), Kind.CLASS);
        this.className = className;
    }

    @Override
    public OutputStream openOutputStream() throws IOException {
        return toByteArray;
    }

    /**
     * 获取类名
     * @return
     */
    public String getClassName() {
        return className;
    }

    /**
     * 获取字节信息
     * @return
     */
    public byte[] getByteCode() {
        return toByteArray.toByteArray();
    }
}

4.文件管理器封装类

package org.example.demo.util;

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

/**
 * @author moon
 * @date 2023-02-15 20:00
 * @since 1.8
 */
public class CustomJavaFileManager extends ForwardingJavaFileManager<JavaFileManager> {

    /**
     * 自定义类加载器
     */
    private CustomClassLoader loader;

    /**
     * 初始化
     * @param fileManager
     * @param loader
     */
    protected CustomJavaFileManager(JavaFileManager fileManager, CustomClassLoader loader) {
        super(fileManager);
        this.loader = loader;
    }

    @Override
    public JavaFileObject getJavaFileForOutput(
            JavaFileManager.Location location, String className,
            JavaFileObject.Kind kind, FileObject sibling) {

        try {
            CustomJavaFileObject innerClass = new CustomJavaFileObject(className);
            loader.addJavaCode(innerClass);
            return innerClass;
        } catch (Exception e) {
            throw new RuntimeException("exception when creating in-memory output stream for  " + className, e);
        }
    }

    @Override
    public ClassLoader getClassLoader(JavaFileManager.Location location) {
        return loader;
    }

}

5.类加载器

用于从 CustomJavaFileObject 获取字节流,并通过 ClassLoader.defineClass 生成类

package org.example.demo.util;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author moon
 * @date 2023-02-15 20:50
 * @since 1.8
 */
public class CustomClassLoader extends ClassLoader{

    /**
     * 缓存源代码对象
     */
    private Map<String, CustomJavaFileObject> fileCacheMap = new ConcurrentHashMap<>(16);

    /**
     * 初始化类加载器
     * @param parent
     */
    public CustomClassLoader(ClassLoader parent) {
        super(parent);
    }

    /**
     * 添加源码缓存
     * @param obj
     */
    public void addJavaCode(CustomJavaFileObject obj) {
        fileCacheMap.put(obj.getName(), obj);
    }

    /**
     * 获取类
     * @param className
     *          The <a href="#binary-name">binary name</a> of the class
     *
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> findClass(String className) throws ClassNotFoundException {
        if (fileCacheMap.containsKey(className)){
            byte[] byteCode = fileCacheMap.get(className).getByteCode();
            return defineClass(className, byteCode, 0, byteCode.length);
        } else {
            return super.findClass(className);
        }
    }
}

6.类编译器

简要说明一下调用流程:读取源码 -> 编译 -> 加载为 Class => 构建对象及使用

package org.example.demo.util;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author moon
 * @date 2023-02-15 20:50
 * @since 1.8
 */
public class CustomClassLoader extends ClassLoader{

    /**
     * 缓存源代码对象
     */
    private Map<String, CustomJavaFileObject> fileCacheMap = new ConcurrentHashMap<>(16);

    /**
     * 初始化类加载器
     * @param parent
     */
    public CustomClassLoader(ClassLoader parent) {
        super(parent);
    }

    /**
     * 添加源码缓存
     * @param obj
     */
    public void addJavaCode(CustomJavaFileObject obj) {
        fileCacheMap.put(obj.getName(), obj);
    }

    /**
     * 获取类
     * @param className
     *          The <a href="#binary-name">binary name</a> of the class
     *
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> findClass(String className) throws ClassNotFoundException {
        if (fileCacheMap.containsKey(className)){
            byte[] byteCode = fileCacheMap.get(className).getByteCode();
            return defineClass(className, byteCode, 0, byteCode.length);
        } else {
            return super.findClass(className);
        }
    }
}

三.动态编译测试

1.普通测试类

定义一个普通测试类,包含:有、无参构造初始化,有、无参方法调用

用户积分器

package org.example.demo.common;

public class UserSort {

    private String name;
    private int score;

    public UserSort (){
    }

    public UserSort (String name, int sort){
        this.name = name;
        this.score = sort;
    }

    public void reset(){
        this.score = 0;
        System.out.println("姓名: " + this.name + " 积分重置: " + this.score);
    }

    public void insert(int score){
        this.score += score;
        System.out.println("姓名: " + this.name + " 加分结果: " + this.score);
    }

    public void reduce(int score){
        this.score -= score;
        System.out.println("姓名: " + this.name + " 减分结果: " + this.score);
    }
}

封装一个静态方法:

public static void commonClass(CustomClassCompiler compiler) throws Exception{

        String sourceCode = "package org.example.demo.common;\n" +
                "\n" +
                "public class UserSort {\n" +
                "\n" +
                "    private String name;\n" +
                "    private int score;\n" +
                "\n" +
                "    public UserSort (){\n" +
                "    }\n" +
                "\n" +
                "    public UserSort (String name, int sort){\n" +
                "        this.name = name;\n" +
                "        this.score = sort;\n" +
                "    }\n" +
                "\n" +
                "    public void reset(){\n" +
                "        this.score = 0;\n" +
                "        System.out.println(\"姓名: \" + this.name + \" 积分重置: \" + this.score);\n" +
                "    }\n" +
                "\n" +
                "    public void insert(int score){\n" +
                "        this.score += score;\n" +
                "        System.out.println(\"姓名: \" + this.name + \" 加分结果: \" + this.score);\n" +
                "    }\n" +
                "\n" +
                "    public void reduce(int score){\n" +
                "        this.score -= score;\n" +
                "        System.out.println(\"姓名: \" + this.name + \" 减分结果: \" + this.score);\n" +
                "    }\n" +
                "}";

        String className = "org.example.demo.common.UserSort";

        compiler.addSource(className, sourceCode);

        compiler.compile(className);

        Class<?> clazz = compiler.getClassByName(className);

        System.out.println("无参构造及重置分数-----------------------");

        Object object = clazz.getDeclaredConstructor().newInstance();

        Method method = clazz.getDeclaredMethod("reset");

        method.invoke(object);

        System.out.println("有参构造及重置分数-----------------------");

        object = clazz.getDeclaredConstructor(String.class,int.class).newInstance("张三",0);

        method.invoke(object);

        System.out.println("加分-----------------------------------");

        method = clazz.getDeclaredMethod("insert",int.class);

        method.invoke(object,10);

        System.out.println("减分-----------------------------------");

        method = clazz.getDeclaredMethod("reduce",int.class);

        method.invoke(object,2);
    }

2.接口实现类

定义一个处理器接口,用于处理数据

package org.example.demo.handler;

/**
 * @author moon
 * @date 2023-02-15 20:55
 * @since 1.8
 */
public interface BaseHandler {

    /**
     * 处理器
     * @param content
     */
    void deal(String content);
}

封装一个静态方法,实现类不再单独贴出,简单加了个打印,输出【春江花月夜】

public static void interfaceClass(CustomClassCompiler compiler) throws Exception {

        String sourceCode = "package com.demo.handler;\n" +
                "import org.example.demo.handler.BaseHandler;\n" +
                "public class DynamicHandler implements BaseHandler {\n" +
                "    \n" +
                "    @Override\n" +
                "    public void deal(String content) {\n" +
                "        System.out.println(content);\n" +
                "    }\n" +
                "}";

        String className = "com.demo.handler.DynamicHandler";

        compiler.addSource(className, sourceCode);

        compiler.compile(className);

        BaseHandler handler = (BaseHandler) compiler.getClassByName(className).getDeclaredConstructor().newInstance();

        handler.deal("春江花月夜");
    }

3.测试

在 App 类内直接定义一个 main 方法,调用上面两个静态方法

package org.example.demo;

import org.example.demo.handler.BaseHandler;
import org.example.demo.util.CustomClassCompiler;

import java.lang.reflect.Method;

/**
 * @author moon
 * @date 2023-02-15 20:42
 * @since 1.8
 */
public class App {

    public static void main(String[] args) throws Exception {

        CustomClassCompiler compiler = CustomClassCompiler.newInstance(null);

        commonClass(compiler);

        System.out.println("\n--------------------------------------------------\n");
        
        interfaceClass(compiler);
        
    }

	//TODO 静态方法 commonClass
	
	//TODO 静态方法 interfaceClass
	
}

调用效果如下:

java 动态调jar包 java动态编译java文件_JavaCompiler_03

四.用动态编译 Class 替换 SpringBoot 的 Bean(未完)

未完待续 . . .