自定义 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")) 下一篇计划:深入对比这两种方案,讲清楚什么场景用哪个。

相关推荐
有来技术1 天前
Spring Boot 4 + Vue3 企业级多租户 SaaS:从共享 Schema 架构到商业化套餐设计
java·vue.js·spring boot·后端
东东5161 天前
学院个人信息管理系统 (springboot+vue)
vue.js·spring boot·后端·个人开发·毕设
三水不滴1 天前
Redis缓存更新策略
数据库·经验分享·redis·笔记·后端·缓存
sxgzzn1 天前
能源行业智能监测产品与技术架构解析
架构·数字孪生·无人机巡检
xiaoxue..1 天前
React 手写实现的 KeepAlive 组件
前端·javascript·react.js·面试
快乐非自愿1 天前
【面试题】MySQL 的索引类型有哪些?
数据库·mysql·面试
小邓吖1 天前
自己做了一个工具网站
前端·分布式·后端·中间件·架构·golang
南风知我意9571 天前
【前端面试2】基础面试(杂项)
前端·面试·职场和发展
大爱编程♡1 天前
SpringBoot统一功能处理
java·spring boot·后端
Java烘焙师1 天前
架构师必备:灰度方案汇总
架构·数仓