自定义 ClassLoader 动态加载:不重启就能加载新代码?

本文将带你用 300 行代码,亲手实现 JVM 级别的动态代码加载能力,并看清它的代价。

全程只使用 javax.tools.JavaCompiler 和标准 ClassLoader,零第三方依赖,JDK 8+ 开箱即用。


一、同一个问题,不同的解法

项目中经常有这样的需求:运营配置一段规则脚本,系统能直接执行。

java 复制代码
String rule = "if (amount > 100) amount * 0.8";
Object result = engine.execute(rule);  // 怎么让字符串变成可执行代码?

上一篇文章《QLExpress 如何做到不编译就能执行?》讲了解释执行的方案:把脚本解析成 AST 树,用 Java 代码遍历执行,不生成 class 文件。

这一篇换个思路:能不能把脚本编译成真正的 class 文件,然后加载执行?

听起来很酷,但有三个问题:

java 复制代码
String code = "public class Script { ... }";

// 问题1:怎么编译?Java 源码变成字节码
byte[] classBytes = compile(code);

// 问题2:怎么加载?字节码变成 Class 对象
Class<?> clazz = load(classBytes);

// 问题3:怎么执行?调用 Class 的方法
Object result = clazz.execute();

花了一天手写 300 行代码验证,这个思路完全可行。后来发现:这就是 Groovy 的做法。

Groovy 是什么? 一门运行在 JVM 上的动态语言,最常见的场景是 Jenkins Pipeline 和 Gradle 构建脚本。它的核心能力就是动态编译执行。

本文手写的实现称为 DynamicScriptRunner,展示 Groovy 等脚本引擎的底层原理,全程只用 JDK 自带工具,零依赖。

二、整体流程

把脚本执行分成五个阶段:

graph LR A[脚本字符串] -->|1.包装| B[Java源码] B -->|2.编译| C[内存字节码] C -->|3.加载| D[Class对象] D -->|4.反射| E[执行结果] style A fill:#e1f5ff,stroke:#01579b,stroke-width:2px style B fill:#fff9c4,stroke:#f57f17,stroke-width:2px style C fill:#f3e5f5,stroke:#4a148c,stroke-width:2px style D fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px style E fill:#ffebee,stroke:#b71c1c,stroke-width:2px

举个例子

java 复制代码
// 输入脚本
"if (amount > 100) MyMath.discount(amount, 0.8)"

// 上下文
amount = 150
MyMath = MyMath.class

// 执行流程
脚本字符串 → 包装成Java类 → 编译成字节码 → 加载成Class → 反射执行 → 返回120.0

三、关键技术点详解

技术点一:脚本包装成 Java 源码

原始脚本

java 复制代码
if (amount > 100) MyMath.discount(amount, 0.8)

包装后的 Java 源码

java 复制代码
public class Script_1702345678 {
    public static Object execute(
        java.util.Map<String, Object> context,
        java.util.Map<String, Class<?>> functions
    ) throws Exception {
        // 步骤1:从 context 提取变量并声明
        Double amount = (Double) context.get("amount");
        
        // 步骤2:从 functions 提取函数类
        Class<?> MyMath = functions.get("MyMath");
        
        // 步骤3:插入脚本(需要转换)
        return (amount > 100) ? 
            (Double) MyMath.getMethod("discount", double.class, double.class)
                           .invoke(null, amount, 0.8)
            : null;
    }
}

为什么要包装?

复制代码
脚本片段不能直接编译
必须包装成完整的类
才能交给 JavaCompiler

包装三步走

graph TD A[原始脚本] --> B[提取变量] A --> C[提取函数] A --> D[转换调用] B --> E[生成Java源码] C --> E D --> E style A fill:#e1f5ff,stroke:#01579b,stroke-width:2px style B fill:#fff9c4,stroke:#f57f17,stroke-width:2px style C fill:#fff9c4,stroke:#f57f17,stroke-width:2px style D fill:#fff9c4,stroke:#f57f17,stroke-width:2px style E fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px

详细说明

  1. 提取变量:从 context 中提取 amount,生成 Double amount = ...
  2. 提取函数:从 functions 中提取 MyMath,生成 Class<?> MyMath = ...
  3. 转换调用:把 MyMath.discount(...) 转换成反射调用

关键代码

java 复制代码
private String wrapScript(String className, String script) {
    StringBuilder sb = new StringBuilder();
    
    // 类定义
    sb.append("public class ").append(className).append(" {\n");
    sb.append("    public static Object execute(\n");
    sb.append("        java.util.Map<String, Object> context,\n");
    sb.append("        java.util.Map<String, Class<?>> functions\n");
    sb.append("    ) throws Exception {\n");
    
    // 注入变量
    for (String varName : context.getVariables().keySet()) {
        Object value = context.getVariables().get(varName);
        String type = getJavaType(value);  // Double, Integer, String
        sb.append("        ").append(type).append(" ").append(varName)
          .append(" = (").append(type).append(") context.get(\"")
          .append(varName).append("\");\n");
    }
    
    // 注入函数类
    for (String funcName : context.getFunctions().keySet()) {
        sb.append("        Class<?> ").append(funcName)
          .append(" = functions.get(\"").append(funcName).append("\");\n");
    }
    
    // 转换并插入脚本
    String transformedScript = transformScript(script);
    sb.append("        return ").append(transformedScript).append(";\n");
    
    sb.append("    }\n");
    sb.append("}\n");
    
    return sb.toString();
}

技术点二:函数调用转换

问题:脚本里怎么调用 MyMath.discount?

java 复制代码
// 脚本中
MyMath.discount(100, 0.8)

// 但此时 MyMath 是 Class<?> 类型
Class<?> MyMath = functions.get("MyMath");

// 不能直接调用方法
MyMath.discount(100, 0.8);  // 编译错误!

解决方案:转换成反射调用

java 复制代码
// 转换后
(Double) MyMath.getMethod("discount", double.class, double.class)
               .invoke(null, 100.0, 0.8)

转换逻辑

java 复制代码
private String transformFunctionCalls(String script) {
    // 正则匹配:ClassName.methodName(args)
    String pattern = "(\\w+)\\.(\\w+)\\(([^)]*)\\)";
    Pattern p = Pattern.compile(pattern);
    Matcher m = p.matcher(script);
    
    StringBuffer result = new StringBuffer();
    while (m.find()) {
        String className = m.group(1);   // MyMath
        String methodName = m.group(2);  // discount
        String args = m.group(3);        // 100, 0.8
        
        String[] argArray = args.split(",");
        
        // 构造反射调用
        StringBuilder repl = new StringBuilder();
        repl.append("(Double) ").append(className)
            .append(".getMethod(\"").append(methodName).append("\"");
        
        // 添加参数类型
        for (int i = 0; i < argArray.length; i++) {
            repl.append(", double.class");
        }
        
        repl.append(").invoke(null, ").append(args).append(")");
        
        m.appendReplacement(result, repl.toString());
    }
    m.appendTail(result);
    
    return result.toString();
}

技术点三:JavaCompiler 动态编译

核心 API

java 复制代码
// 获取系统编译器(需要 JDK,JRE 没有)
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

if (compiler == null) {
    throw new RuntimeException("需要 JDK,不能用 JRE");
}

编译四步骤

graph LR A[Java源码] -->|包装| B[JavaFileObject] B -->|编译| C[内存字节码] C -->|检查| D{是否成功} D -->|是| E[返回字节码] D -->|否| F[抛异常] style A fill:#fff9c4,stroke:#f57f17,stroke-width:2px style B fill:#e1f5ff,stroke:#01579b,stroke-width:2px style C fill:#f3e5f5,stroke:#4a148c,stroke-width:2px style D fill:#ffccbc,stroke:#e64a19,stroke-width:2px style E fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px style F fill:#ffebee,stroke:#b71c1c,stroke-width:2px

关键代码

java 复制代码
public byte[] compile(String className, String javaSource) throws Exception {
    // 1. 获取编译器
    JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
    
    // 2. 诊断收集器(收集编译错误)
    DiagnosticCollector<JavaFileObject> diagnostics = 
        new DiagnosticCollector<>();
    
    // 3. 文件管理器(拦截输出到内存)
    InMemoryFileManager fileManager = new InMemoryFileManager(
        compiler.getStandardFileManager(diagnostics, null, null)
    );
    
    // 4. 源码对象
    JavaFileObject sourceFile = 
        new StringSourceJavaFileObject(className, javaSource);
    
    // 5. 编译任务
    Iterable<? extends JavaFileObject> compilationUnits = 
        Arrays.asList(sourceFile);
    
    JavaCompiler.CompilationTask task = compiler.getTask(
        null,                    // 输出(null = 不输出)
        fileManager,             // 文件管理器
        diagnostics,             // 诊断收集器
        null,                    // 编译选项
        null,                    // 注解处理器
        compilationUnits         // 源文件
    );
    
    // 6. 执行编译
    boolean success = task.call();
    
    if (!success) {
        // 收集错误
        StringBuilder errors = new StringBuilder();
        for (Diagnostic<?> d : diagnostics.getDiagnostics()) {
            errors.append(d.getMessage(null)).append("\n");
        }
        throw new RuntimeException("编译失败:\n" + errors);
    }
    
    // 7. 获取字节码
    return fileManager.getCompiledClass(className);
}

技术点四:内存编译(不写磁盘)

问题:JavaCompiler 默认会把 .class 写到磁盘

bash 复制代码
# 默认行为
javac Script_123.java
# 生成 Script_123.class 文件

目标:编译后的字节码保存在内存,不写磁盘

解决方案:自定义 FileManager 拦截输出

graph LR A[编译器要写.class] -->|拦截| B[FileManager] B -->|改写到| C[内存对象] C -->|保存| D[ByteArrayOutputStream] style A fill:#ffccbc,stroke:#e64a19,stroke-width:2px style B fill:#fff9c4,stroke:#f57f17,stroke-width:3px style C fill:#e1f5ff,stroke:#01579b,stroke-width:2px style D fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px

核心原理

编译器调用 getJavaFileForOutput() 时,我们返回自己的内存对象,而不是真实文件。编译器会把字节码写到这个内存对象里。

关键代码

java 复制代码
public class InMemoryFileManager 
    extends ForwardingJavaFileManager<JavaFileManager> {
    
    // 保存编译后的字节码
    private Map<String, ByteArrayJavaFileObject> compiledClasses = 
        new HashMap<>();
    
    @Override
    public JavaFileObject getJavaFileForOutput(
        Location location,
        String className,
        JavaFileObject.Kind kind,
        FileObject sibling
    ) throws IOException {
        
        if (kind == JavaFileObject.Kind.CLASS) {
            // 拦截 .class 输出
            ByteArrayJavaFileObject fileObject = 
                new ByteArrayJavaFileObject(className);
            compiledClasses.put(className, fileObject);
            return fileObject;  // 返回内存对象
        }
        
        // 其他类型交给父类处理
        return super.getJavaFileForOutput(location, className, kind, sibling);
    }
    
    public byte[] getCompiledClass(String className) {
        ByteArrayJavaFileObject fileObject = compiledClasses.get(className);
        return fileObject != null ? fileObject.getBytes() : null;
    }
}

ByteArrayJavaFileObject

java 复制代码
public class ByteArrayJavaFileObject extends SimpleJavaFileObject {
    
    private ByteArrayOutputStream baos = new ByteArrayOutputStream();
    
    public ByteArrayJavaFileObject(String className) {
        super(
            URI.create("bytes:///" + className.replace('.', '/') + ".class"),
            Kind.CLASS
        );
    }
    
    @Override
    public OutputStream openOutputStream() {
        return baos;  // 编译器写入这里,从内存加载字节码,绕过磁盘IO
    }
    
    public byte[] getBytes() {
        return baos.toByteArray();  // 获取字节码
    }
}

技术点五:自定义 ClassLoader 加载

问题:有了字节码,怎么加载成 Class 对象?

java 复制代码
byte[] classBytes = compiler.compile(className, javaSource);
// 怎么变成 Class<?> 对象?

答案:自定义 ClassLoader

核心方法

java 复制代码
// defineClass:字节码数组 → Class 对象
Class<?> clazz = defineClass(name, classBytes, 0, classBytes.length);

双亲委派机制

vbnet 复制代码
先问父类能不能加载
  ↓ 不能
再调用自己的 findClass
  ↓
从内存字节码数组加载
  ↓
返回 Class 对象

关键代码

java 复制代码
public class ScriptClassLoader extends ClassLoader {
    
    // 存储字节码
    private Map<String, byte[]> classBytesMap = new HashMap<>();
    
    public ScriptClassLoader() {
        super(ScriptClassLoader.class.getClassLoader());  // 设置父类加载器
    }
    
    // 注册字节码
    public void defineClass(String className, byte[] classBytes) {
        classBytesMap.put(className, classBytes);
    }
    
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 从内存获取字节码(JDK 编译器 SPI 接口生成的)
        byte[] classBytes = classBytesMap.get(name);
        
        if (classBytes != null) {
            // defineClass:从内存字节数组加载,绕过磁盘IO
            return defineClass(name, classBytes, 0, classBytes.length);
        }
        
        throw new ClassNotFoundException(name);
    }
}

为什么每次生成新类名?

java 复制代码
String className = "Script_" + System.currentTimeMillis();

原因

markdown 复制代码
1. 同一个 ClassLoader 不能加载同名类
2. 重复加载会报错:LinkageError

解决方案:
- 方案1:每次生成新类名(当前做法)
- 方案2:每次创建新 ClassLoader
- 方案3:缓存(相同脚本复用 Class)

技术点六:反射执行

问题:有了 Class 对象,怎么执行?

java 复制代码
Class<?> scriptClass = loader.loadClass("Script_1702345678");
// 怎么调用 execute 方法?

答案:反射

java 复制代码
// 1. 获取 execute 方法
Method executeMethod = scriptClass.getMethod(
    "execute", 
    Map.class,    // 第一个参数:context
    Map.class     // 第二个参数:functions
);

// 2. 准备参数
Map<String, Object> context = new HashMap<>();
context.put("amount", 150.0);

Map<String, Class<?>> functions = new HashMap<>();
functions.put("MyMath", MyMath.class);

// 3. 调用(静态方法,第一个参数传 null)
Object result = executeMethod.invoke(
    null,        // 静态方法
    context,     // 第一个参数
    functions    // 第二个参数
);

// 4. 返回结果
return result;  // 120.0

四、完整执行流程示例

输入

java 复制代码
String script = "if (amount > 100) MyMath.discount(amount, 0.8)";
Context:
  amount = 150.0
  MyMath = MyMath.class

执行流程

graph TD A[脚本字符串] --> B[包装成Java源码] B --> C[JavaCompiler编译] C --> D[得到内存字节码] D --> E[ClassLoader加载] E --> F[反射执行] F --> G[返回结果] style A fill:#e1f5ff,stroke:#01579b,stroke-width:2px style B fill:#fff9c4,stroke:#f57f17,stroke-width:2px style C fill:#f3e5f5,stroke:#4a148c,stroke-width:2px style D fill:#ffccbc,stroke:#e64a19,stroke-width:2px style E fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px style F fill:#ffebee,stroke:#c62828,stroke-width:2px style G fill:#e8f5e9,stroke:#1b5e20,stroke-width:3px

生成的 Java 源码

java 复制代码
public class Script_1702345678 {
    public static Object execute(
        java.util.Map<String, Object> context,
        java.util.Map<String, Class<?>> functions
    ) throws Exception {
        Double amount = (Double) context.get("amount");
        Class<?> MyMath = functions.get("MyMath");
        
        return (amount > 100) ? 
            (Double) MyMath.getMethod("discount", double.class, double.class)
                           .invoke(null, amount, 0.8)
            : null;
    }
}

编译后的字节码指令(伪代码):

vbnet 复制代码
execute方法:
  ALOAD context
  LDC "amount"
  INVOKEINTERFACE get
  CHECKCAST Double
  ASTORE amount
  
  ALOAD amount
  INVOKEVIRTUAL doubleValue
  LDC 100.0
  DCMPG
  IFLE label_else       ← 真正的 JVM if 指令!
  
  ALOAD MyMath
  LDC "discount"
  ...
  INVOKEVIRTUAL invoke
  GOTO label_end
  
label_else:
  ACONST_NULL
  
label_end:
  ARETURN

关键对比

javascript 复制代码
QLExpress:
  用 Java 的 if 包装:if ((Boolean) cond) { ... }
  
MiniGroovy:
  生成 JVM 的 if 指令:IFLE label_else

五、与 QLExpress 的对比

不深入对比(下一篇文章的事),简单说几点差异:

维度 QLExpress DynamicScriptRunner
if 语句 Java 代码模拟 JVM 字节码指令
生成 class 不生成 生成
启动速度 慢(编译耗时)
运行速度 快(JIT 优化)
内存占用 高(每次生成新类)

核心差异

arduino 复制代码
QLExpress:
  脚本 → AST → 用 Java 代码遍历执行
  
DynamicScriptRunner:
  脚本 → Java 源码 → 编译成 class → JVM 执行

六、黑科技与警告

黑科技:动态加载 Controller

理论上,可以用这个技术动态加载 Controller:

java 复制代码
// 写一个 HTTP 接口接收代码
@PostMapping("/dynamicController")
public String addController(@RequestBody String code) {
    // 编译代码
    byte[] classBytes = compiler.compile("DynamicController", code);
    
    // 加载类
    Class<?> clazz = loader.loadClass("DynamicController");
    
    // 注册到 Spring
    // ...(省略注册逻辑)
    
    return "Controller 已加载,无需重启!";
}

想象一下

复制代码
不重启服务器
发送一段代码
新功能立刻生效

听起来很酷?

警告:千万别在线上用

为什么不能用?

风险一:类泄漏导致 OOM

复制代码
每次加载生成新类
不释放会占满 Metaspace
最终 OutOfMemoryError

真实案例:某团队在线上使用类似机制热更新业务规则,因类加载器泄漏未及时清理,72 小时后 Metaspace 占满,Full GC 频发,服务雪崩。

监控

bash 复制代码
jmap -clstats <pid> | grep Script_

输出:
Script_1702345678
Script_1702345679
Script_1702345680
...
Script_1702399999  # 几万个类!

风险二:安全漏洞

java 复制代码
// 用户提交恶意代码
String maliciousCode = "System.exit(0)";
// 或
String maliciousCode = "Runtime.getRuntime().exec(\"rm -rf /\")";

// 你的服务:直接挂了

风险三:编译耗时影响性能

复制代码
JavaCompiler 编译一次:100-200ms
高并发下:所有请求都在等编译
服务响应变慢

风险四:难以追踪和调试

复制代码
线上出了问题
看日志:Script_1702345678 报错
去哪找这个类的代码?
已经不在了,只在内存里存在过

对线上要有敬畏之心

diff 复制代码
动态编译很强大
但也很危险

线上环境:
- 稳定压倒一切
- 可追溯压倒一切
- 安全压倒一切

这种黑科技:
- 学习可以
- 本地玩可以
- 线上绝对不行

替代方案

diff 复制代码
需要动态能力?
- 用配置中心(Apollo、Nacos)
- 用规则引擎(Drools、QLExpress)
- 用脚本引擎(但要沙箱隔离)
- 用插件系统(OSGi、JPMS)

需要热部署?
- 用灰度发布
- 用蓝绿部署
- 用滚动更新

安全使用场景

这项技术并非一无是处,在可控环境下有其价值:

diff 复制代码
适合使用:
- 本地调试脚本
- 规则测试沙箱
- 教学演示系统
- 低频配置热加载(配合严格白名单)
- 内部工具平台(非核心业务)

前提条件:
- 严格的权限控制
- 完善的监控告警
- 清晰的生命周期管理
- 定期的类加载器清理

七、完整测试

测试代码

java 复制代码
public class Main {
    public static void main(String[] args) throws Exception {
        DynamicScriptRunner runner = new DynamicScriptRunner();
        runner.register("MyMath", MyMath.class);
        runner.getContext().put("amount", 150.0);
        
        test(runner, "2 + 3 * 4");
        test(runner, "if (amount > 100) amount * 0.8");
        test(runner, "MyMath.discount(100, 0.8)");
        test(runner, "if (amount > 100) MyMath.discount(amount, 0.8)");
    }
    
    static void test(DynamicScriptRunner runner, String script) throws Exception {
        Object result = runner.execute(script);
        System.out.println("Result: " + result);
    }
}

测试输出

makefile 复制代码
Result: 14.0
Result: 120.0
Result: 80.0
Result: 120.0

验证

bash 复制代码
# 运行时查看加载的类
jps
jmap -clstats <pid> | grep Script_

# 应该看到
Script_1702345678
Script_1702345679
Script_1702345680
Script_1702345681

# 每次执行生成一个新类

八、核心代码展示

DynamicScriptRunner 主入口

java 复制代码
public class DynamicScriptRunner {
    
    private Context context = new Context();
    private InMemoryCompiler compiler = new InMemoryCompiler();
    
    public void register(String name, Class<?> clazz) {
        context.registerFunction(name, clazz);
    }
    
    public Object execute(String script) throws Exception {
        // 1. 生成唯一类名
        String className = "Script_" + System.currentTimeMillis();
        
        // 2. 包装成 Java 源码
        String javaSource = wrapScript(className, script);
        
        // 3. 动态编译
        byte[] classBytes = compiler.compile(className, javaSource);
        
        // 4. 加载类
        ScriptClassLoader loader = new ScriptClassLoader();
        loader.defineClass(className, classBytes);
        Class<?> scriptClass = loader.loadClass(className);
        
        // 5. 反射执行
        Method executeMethod = scriptClass.getMethod("execute", Map.class, Map.class);
        return executeMethod.invoke(null, context.getVariables(), context.getFunctions());
    }
    
    private String wrapScript(String className, String script) {
        // 包装逻辑(前面已展示)
    }
}

InMemoryCompiler 编译器

java 复制代码
public class InMemoryCompiler {
    
    public byte[] compile(String className, String javaSource) throws Exception {
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        
        DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
        InMemoryFileManager fileManager = new InMemoryFileManager(
            compiler.getStandardFileManager(diagnostics, null, null)
        );
        
        JavaFileObject sourceFile = new StringSourceJavaFileObject(className, javaSource);
        
        JavaCompiler.CompilationTask task = compiler.getTask(
            null, fileManager, diagnostics, null, null, Arrays.asList(sourceFile)
        );
        
        boolean success = task.call();
        
        if (!success) {
            StringBuilder errors = new StringBuilder();
            for (Diagnostic<?> d : diagnostics.getDiagnostics()) {
                errors.append(d.getMessage(null)).append("\n");
            }
            throw new RuntimeException("编译失败:\n" + errors);
        }
        
        return fileManager.getCompiledClass(className);
    }
}

ScriptClassLoader 类加载器

java 复制代码
public class ScriptClassLoader extends ClassLoader {
    
    private Map<String, byte[]> classBytesMap = new HashMap<>();
    
    public void defineClass(String className, byte[] classBytes) {
        classBytesMap.put(className, classBytes);
    }
    
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classBytes = classBytesMap.get(name);
        if (classBytes != null) {
            return defineClass(name, classBytes, 0, classBytes.length);
        }
        throw new ClassNotFoundException(name);
    }
}

九、总结

核心机制

动态编译执行的本质是:

markdown 复制代码
1. 脚本包装成完整 Java 类
2. JavaCompiler 动态编译(JDK 原生能力)
3. InMemoryFileManager 拦截输出到内存
4. ScriptClassLoader 从内存加载字节码
5. 反射调用执行

零依赖实现 :全程只使用 JDK 自带的 javax.tools.JavaCompiler 和标准 ClassLoader,无需引入任何第三方包。

关键技术点

markdown 复制代码
1. JavaCompiler API:JDK 自带的动态编译工具
2. FileManager 机制:拦截编译输出(JDK 编译器 SPI 接口)
3. ClassLoader 机制:从内存加载字节码
4. defineClass 方法:字节码数组 → Class 对象
5. 反射调用:执行生成的类

与 QLExpress 的核心差异

kotlin 复制代码
QLExpress:
  用 Java 代码模拟 if
  不生成 class
  
MiniGroovy:
  生成真正的 JVM if 指令
  会生成 class(在内存里)

学到了什么

markdown 复制代码
1. 动态编译不神秘,JavaCompiler 就能做
2. class 文件可以只在内存里存在
3. ClassLoader 可以从内存加载字节码
4. 强大的技术要谨慎使用
5. 对线上要有敬畏之心

写在最后

拆了两个轮子:

上一篇 QLExpress

  • 不生成 class
  • 用 Java 代码模拟执行
  • 轻量、快速

本篇动态编译方案

  • 动态编译生成 class
  • 真正的 JVM 字节码
  • 强大、灵活

这套机制就是 Groovy、JSR-223 ScriptEngine 等工具的底层原理。理解了它,就理解了 Java 世界里动态能力的本质。

本文代码\] ([gitee.com/sh_wangwanb...](https://link.juejin.cn?target=https%3A%2F%2Fgitee.com%2Fsh_wangwanbao%2Fql-express-mini-groovy "https://gitee.com/sh_wangwanbao/ql-express-mini-groovy")) 下一篇计划:深入对比这两种方案,讲清楚什么场景用哪个。

相关推荐
lomocode5 小时前
改一个需求动 23 处代码?你可能踩进了这个坑
后端·设计模式
踏浪无痕5 小时前
别重蹈我们的覆辙:脚本引擎选错的两年代价
后端·面试·架构
TT哇5 小时前
【每日八股】面经常考
java·面试
何中应5 小时前
【面试题-4】JVM
java·jvm·后端·面试题
Oneslide5 小时前
如何在Kubernetes搭建RabbitMQ集群 部署篇
后端
VX:Fegn08955 小时前
计算机毕业设计|基于springboot + vue非遗传承文化管理系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
Warren985 小时前
面试和投简历闲聊
网络·学习·docker·面试·职场和发展·eureka·ansible
测试人社区-千羽5 小时前
Apple自动化测试基础设施(XCTest/XCUITest)面试深度解析
运维·人工智能·测试工具·面试·职场和发展·自动化·开源软件
tonydf5 小时前
从零开始玩转 Microsoft Agent Framework:我的 MAF 实践之旅
后端·aigc