Jdk19 动态编译 Java 源码为 Class 文件(一)

一.背景

1.Jdk 版本

版本查看命令:java -version

在这里插入图片描述

2.需求

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

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

二.Java 源码动态编译实现

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

说明
JavaCompiler编译器 ToolProvider.getSystemJavaCompiler();
SimpleJavaFileObject文件对象类,可以表示源码、类文件
ClassLoader顶层类加载器,抽象类
ForwardingJavaFileManager文件管理器

项目结构如图

在这里插入图片描述

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 javax.tools.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 类编译器
 *
 * @author moon
 * @date 2023-02-15 20:09
 * @since 1.8
 */
public class CustomClassCompiler {

    private JavaCompiler javac;
    private CustomClassLoader classLoader;
    private Iterable<String> options;
    boolean ignoreWarnings = false;

    /**
     * 缓存待编译的源码
     */
    private Map<String, CustomSourceCode> sourceCodes = new ConcurrentHashMap<>(16);

    /**
     * 缓存生成的类
     */
    private Map<String, Class<?>> classMap = new ConcurrentHashMap<>(16);

    /**
     * 单例编译器
     */
    private static volatile CustomClassCompiler compiler;

    /**
     * 获取实例
     * @return
     */
    public static CustomClassCompiler newInstance(ClassLoader parent) {
        if ( null==compiler ){
            synchronized (CustomClassCompiler.class){
                if (null==compiler){
                    compiler = new CustomClassCompiler();
                    if (null!=parent){
                        compiler.useParentClassLoader(parent);
                    }
                }
            }
        }
        return compiler;
    }

    /**
     * 默认类加载器 (私有化构造)
     */
    private CustomClassCompiler() {
        this.javac = ToolProvider.getSystemJavaCompiler();
        this.classLoader = new CustomClassLoader(ClassLoader.getSystemClassLoader());
    }

    /**
     * 使用父类加载器
     * @param parent
     * @return
     */
    private CustomClassCompiler useParentClassLoader(ClassLoader parent) {
        this.classLoader = new CustomClassLoader(parent);
        return this;
    }

    /**
     * @return the class loader used internally by the compiler
     */
    public ClassLoader getClassloader() {
        return classLoader;
    }

    /**
     * Options used by the compiler, e.g. '-Xlint:unchecked'.
     *
     * @param options
     * @return
     */
    public void useOptions(String... options) {
        this.options = Arrays.asList(options);
    }

    /**
     * 忽略警告信息
     */
    public void ignoreWarnings() {
        ignoreWarnings = true;
    }

    /**
     * 向编译器添加源码
     * @param className
     * @param sourceCode
     * @return
     * @throws Exception
     */
    public CustomClassCompiler addSource(String className, String sourceCode) {
        sourceCodes.put(className, new CustomSourceCode(className, sourceCode));
        return this;
    }

    /**
     * 编译源码
     *
     * @param classNames
     * @return
     */
    public boolean compile(String ... classNames) {
        try {
            compileByNames(Arrays.asList(classNames));
            return true;
        } catch (Exception e) {
            System.out.println("Compile Exception:" + e.getMessage());
            return false;
        }
    }

    /**
     * 获取类
     * @param className
     * @return
     */
    public Class<?> getClassByName(String className){
        return classMap.get(className);
    }

    /**
     * 编译源码
     *
     * @return Map containing instances of all compiled classes
     * @throws Exception
     */
    private void compileByNames(List<String> classNames) throws Exception {

        if (sourceCodes.size() == 0) {
            throw new RuntimeException("No source code to compile");
        }

        //获取待编译类源码
        Collection<CustomSourceCode> compilationUnits;
        Set<String> keyTemp = null;
        if (null != classNames && classNames.size() > 0){
            compilationUnits = new ArrayList<>(classNames.size());
            keyTemp = new HashSet<>(classNames.size());
            for (String key:classNames){
                if (sourceCodes.containsKey(key)){
                    keyTemp.add(key);
                    compilationUnits.add(sourceCodes.get(key));
                }
            }
        } else {
            keyTemp = sourceCodes.keySet();
            compilationUnits = sourceCodes.values();
        }

        //检测源码是否全部存在
        if (keyTemp.size() < classNames.size()){
            throw new RuntimeException("Some source code not exist");
        }

        //定义警告和错误信息输出集合
        DiagnosticCollector<JavaFileObject> collector = new DiagnosticCollector<>();
        CustomJavaFileManager fileManager = new CustomJavaFileManager(javac.getStandardFileManager(null, null, StandardCharsets.UTF_8), classLoader);
        JavaCompiler.CompilationTask task = javac.getTask(null, fileManager, collector, options, null, compilationUnits);
        //编译
        boolean result = task.call();
        //编译结果
        if (!result || collector.getDiagnostics().size() > 0) {
            StringBuffer exceptionMsg = new StringBuffer();
            exceptionMsg.append("Unable to compile the source");
            boolean hasWarnings = false;
            boolean hasErrors = false;
            for (Diagnostic<? extends JavaFileObject> d : collector.getDiagnostics()) {
                switch (d.getKind()) {
                    case NOTE:
                    case MANDATORY_WARNING:
                    case WARNING:
                        hasWarnings = true;
                        break;
                    case OTHER:
                    case ERROR:
                        hasErrors = true;
                        break;
                    default:
                        break;
                }
                exceptionMsg.append("\n").append("[kind=").append(d.getKind());
                exceptionMsg.append(", ").append("line=").append(d.getLineNumber());
                exceptionMsg.append(", ").append("message=").append(d.getMessage(Locale.US)).append("]");
            }
            //是否忽略警告
            if ((hasWarnings && !ignoreWarnings ) || hasErrors) {
                throw new RuntimeException(exceptionMsg.toString());
            }
        }

        //遍历并缓存编译后的源码
        for (String className : keyTemp) {
            classMap.put(className, classLoader.loadClass(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
	
}

调用效果如下:

在这里插入图片描述

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

未完待续 . . .

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

猪悟道

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值