「不用发版也能改代码」—— dynamo-spring:Java 运行时动态编译与 Bean 管理工具包
想象这几个场景
- 线上 Service 有个逻辑 bug,改一行代码却要跑完「打包 → 审批 → 灰度 → 全量」的完整发版流程;
- SaaS 平台要给每个租户注册不同的 Controller 路由,每次新增租户都要重启应用;
- 规则引擎的条件分支用 Java 写死,每次调整规则都要重新部署;
- 想给现有服务加一段临时监控逻辑,发版成本比写代码本身还高。
这些场景有一个共同诉求:在运行时动态加载代码,最好不用重启。
Java 本身支持运行时编译(javax.tools.JavaCompiler),Spring 也开放了 Bean 注册的入口。但要把这两件事做到生产可用——内存字节码管理、ClassLoader 隔离、Spring MVC 路由热注册、生命周期回收——需要填的坑不少。
dynamo-spring 就是为这个诉求而生的。
痛点:一个改一行的需求,要走一天的流程
我们都经历过:线上某个 Service 的判断逻辑有 bug ,影响面不大,但修复代码只有两三行。按公司规范,要走完整发版流程——本地跑测试、打 fat jar、提交变更单、等审批、灰度 10%、全量发布、观察监控。一个「改一行」的需求,从提交代码到线上生效,快的半天,慢的隔天。
有没有可能跳过发版流程,直接在运行时把新类加载进去?
答案是 Java 本身就能做到。JDK 自带的 JavaCompiler 可以在运行时编译源码,配合自定义 ClassLoader 把字节码加载到内存。Spring 的 BeanFactory 也支持运行时注册 Bean。理论上,一条链路就能实现「改完即生效」。
dynamo-spring 把这条链路做成了开箱即用的工具库。
dynamo-spring 是什么
dynamo-spring 是一个 Java 工具库,提供三大能力:
| 能力 | 说明 |
|---|---|
| 动态编译 | 运行时编译 Java 源码,字节码保存在内存中,通过会话模型 ClassLoader 加载,无需落盘 .class 文件 |
| AOP 代理 | 基于 ByteBuddy 的方法拦截,提供 before / after / afterThrow 三个增强点 |
| Spring Bean 管理 | 运行时注册/注销单例 Bean 和 Controller 路由,通过子类化 Spring MVC 官方扩展点实现 |
库分两个模块,按需选择:
| 模块 | 适用场景 |
|---|---|
dynamo-spring-core |
纯 JDK 环境,只要动态编译 + AOP 代理 |
dynamo-spring-spring-boot-starter |
Spring Boot 项目,三大能力 + 自动配置 |
已发布到 Maven Central,JDK 17+,Spring Boot 3.5+。
5 行跑通
引入依赖后,5 行代码跑通动态编译:
import cn.wubo.dynamo.spring.DynamoRuntime;
try (DynamoRuntime runtime = DynamoRuntime.create()) {
Class<?> clazz = runtime.compileAndLoad(
"public class A { public String hi() { return \"hi\"; } }");
Object o = clazz.getDeclaredConstructor().newInstance();
System.out.println(o.getClass().getMethod("hi").invoke(o)); // hi
}
不需要提前编译,不需要落盘 .class,源码字符串直接变成可执行对象。
三大能力速览
1. 动态编译:compiler 包
DynamoClassLoader 是会话模型——每个实例独立持有 classpath 和字节码缓存,try-with-resources 关闭后自动清理。
try (DynamoClassLoader loader = DynamoClassLoader.create()) {
Class<?> clazz = loader.compileAndLoad("""
public class RiskRule {
public boolean check(int amount) { return amount > 10000; }
}
""");
Object rule = clazz.getDeclaredConstructor().newInstance();
boolean blocked = (boolean) clazz.getMethod("check", int.class).invoke(rule, 50000);
// blocked = true
}
编译失败会抛出 CompilationException,携带完整的诊断信息,方便在运行时捕获并反馈给调用方。
2. AOP 代理:aspect 包
基于 ByteBuddy 的方法拦截。用内置的 SimpleAdvice 一行创建代理,自动记录方法执行耗时:
MyService target = new MyServiceImpl();
MyService proxy = DynamoProxy.proxy(target, new SimpleAdvice());
proxy.doSomething(); // before → 执行 → after,耗时自动记录
需要更细粒度的控制?实现 IAdvice 接口,拿到 before / after / afterThrow 三个增强点,想做什么做什么。
3. Spring Bean 管理:bean 包
不反射 Spring 私有 API ——通过子类化 RequestMappingHandlerMapping,走 Spring 官方推荐的 WebMvcRegistrations 扩展点。注册进去的 Controller 路由立刻生效,不用重启。
DefaultListableBeanFactory bf = (DefaultListableBeanFactory) ctx.getBeanFactory();
// 注册一个 Controller,路由立即生效
DynamoBean.registerController(bf, "emergencyCtrl", EmergencyController.class);
// 不再需要时移除路由
DynamoBean.unregisterController(bf, "emergencyCtrl");
典型用法:线上紧急加一个临时接口、灰度新功能、多租户按需注册不同路由。
几个设计亮点
最后提几个值得关注的设计决策:
- ClassLoader 用会话模型,不是单例。每个
DynamoClassLoader实例独立持有字节码缓存,try-with-resources关闭后自动清理。多个动态编译任务互不干扰,也不用担心内存泄漏。 - 不反射 Spring 私有 API。动态 Controller 路由通过子类化
RequestMappingHandlerMapping+WebMvcRegistrations注入,升级 Spring Boot 版本时不需要重新验证反射点。 - ByteBuddy 而非 CGLIB。原生 ByteBuddy,不是 Spring 重新打包版本,对 JDK 21+ 支持更及时。
上手试试
<dependency>
<groupId>io.github.wb04307201</groupId>
<artifactId>dynamo-spring-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
try (DynamoRuntime runtime = DynamoRuntime.create()) {
Class<?> clazz = runtime.compileAndLoad("public class A { public String hi() { return \"hi\"; } }");
Object o = clazz.getDeclaredConstructor().newInstance();
System.out.println(o.getClass().getMethod("hi").invoke(o)); // hi
}
项目开源于 Apache 2.0 协议。觉得有用,欢迎去 GitHub 点个 Star ⭐
- GitHub:https://github.com/wb04307201/dynamo-spring
- Gitee:https://gitee.com/wb04307201/dynamo-spring

浙公网安备 33010602011771号