一个用于动态加载和管理 Java 类的工具库,支持动态编译、AOP 代理、Spring Bean 管理。会话模型 ClassLoader、ByteBuddy 代理、子类化 Spring MVC、自动配置。
Access rate limit exceeded. Please log in to continue.
dynamo-spring
A Java toolkit for dynamically loading and managing Java classes — dynamic compilation, AOP proxies, and Spring Bean management. Session-scoped ClassLoader, ByteBuddy proxies, subclassed Spring MVC, and auto-configuration.
Quick Start
Maven Dependency
The library is split into two modules — pick what you need:
<!-- Spring Boot integration (recommended): includes the DynamoRuntime facade + DynamoBean + auto-configuration -->
<dependency>
<groupId>io.github.wb04307201</groupId>
<artifactId>dynamo-spring-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
<!-- Pure JDK / non-Spring: dynamic compilation + AOP proxies only (no spring-webmvc) -->
<!--
<dependency>
<groupId>io.github.wb04307201</groupId>
<artifactId>dynamo-spring-core</artifactId>
<version>1.3.0</version>
</dependency>
-->
| Module | Contents | Dependencies |
|---|---|---|
dynamo-spring-core |
compiler + aspect + exception |
javaparser + byte-buddy + spring-core + slf4j |
dynamo-spring-spring-boot-starter |
Above + DynamoRuntime + bean package + auto-configuration |
Above + spring-webmvc + spring-boot-autoconfigure |
5 Lines to Running
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));
}
Package ↔ Module
| Java Package | Module |
|---|---|
cn.wubo.dynamo.spring.compiler |
core |
cn.wubo.dynamo.spring.aspect |
core |
cn.wubo.dynamo.spring.exception |
core |
cn.wubo.dynamo.spring (DynamoRuntime) |
spring-boot-starter |
cn.wubo.dynamo.spring.bean |
spring-boot-starter |
Three Core Capabilities
1. Dynamic Compilation (compiler package)
DynamoClassLoader is a session-scoped ClassLoader — each instance independently holds its own classpath, bytecode cache, and file manager.
import cn.wubo.dynamo.spring.compiler.CompilationResult;
import cn.wubo.dynamo.spring.compiler.CompilerOptions;
import cn.wubo.dynamo.spring.compiler.DynamoClassLoader;
try (DynamoClassLoader loader = DynamoClassLoader.create()) {
CompilationResult result = loader.compile(sourceCode);
if (result.isSuccess()) {
Class<?> clazz = result.getCompiledClass();
// ...
} else {
result.getDiagnostics().forEach(System.err::println);
}
}
Extra options:
loader.compile(source, CompilerOptions.create()
.sourceVersion("17")
.targetVersion("17")
.classpath("/path/to/lib.jar")
.enablePreview());
More overloads:
// Specify a parent ClassLoader (for loading classes beyond JDK's built-ins in isolated environments)
ClassLoader parent = Thread.currentThread().getContextClassLoader();
DynamoClassLoader loader = DynamoClassLoader.create(parent);
// Feed bytecode directly (skip source compilation)
Class<?> clazz = loader.defineClass("com.example.Precompiled", bytes);
// Add multiple JARs at once
loader.addJarPaths("/path/a.jar", "/path/b.jar");
// Throw CompilationException on failure (carries a CompilationResult)
try {
loader.compileAndLoad(badSource);
} catch (CompilationException ex) {
CompilationResult r = ex.getResult(); // structured diagnostics
}
2. AOP Proxies (aspect package)
ByteBuddy-based proxies with a three-method IAdvice interception interface.
import cn.wubo.dynamo.spring.aspect.DynamoProxy;
import cn.wubo.dynamo.spring.aspect.IAdvice;
import java.lang.reflect.Method;
IAdvice advice = new IAdvice() {
public void before(Object t, Method m, Object[] a) { /* ... */ }
public void after(Object t, Method m, Object[] a, Object r) { /* ... */ }
public void afterThrow(Object t, Method m, Object[] a, Throwable c) { /* ... */ }
};
MyService proxy = DynamoProxy.proxy(target, advice);
Or use the built-in SimpleAdvice (thread-safe via ThreadLocal):
import cn.wubo.dynamo.spring.aspect.SimpleAdvice;
MyService proxy = DynamoProxy.proxy(MyService.class, new SimpleAdvice());
Known limitation:
SimpleAdvicedoes not support re-entrant calls on the same thread — if a thread re-enters anySimpleAdvice-proxied method betweenbeforeandafter, the inner call overwrites the outer call's timer.
3. Spring Bean Management (bean package)
No reflection on Spring private APIs — implemented by subclassing RequestMappingHandlerMapping.
import cn.wubo.dynamo.spring.bean.DynamoBean;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
DefaultListableBeanFactory bf = (DefaultListableBeanFactory) ctx.getBeanFactory();
// Register a controller (bean definition + route)
DynamoBean.registerController(bf, "myCtrl", MyController.class);
// Unregister a controller (only removes the route; bean definition remains in the container)
DynamoBean.unregisterController(bf, "myCtrl");
// To truly "replace": unregister the old route first, then register the new type
DynamoBean.unregisterController(bf, "myCtrl");
DynamoBean.registerController(bf, "myCtrl", NewType.class);
// Plain singleton Bean registration (no routes involved)
DynamoBean.registerSingleton(bf, "myBean", MyBean.class);
DynamoBean.unregisterSingleton(bf, "myBean");
About
refreshController:refreshController(bf, name, type)is equivalent toregisterController— it registers the new type's route but does not clear the old route. To truly replace,unregisterControllerfirst, then register.
4. DynamoRuntime High-Level Facade
Glues all three capabilities together into a single lifecycle:
import cn.wubo.dynamo.spring.DynamoRuntime;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
DefaultListableBeanFactory bf = (DefaultListableBeanFactory) ctx.getBeanFactory();
try (DynamoRuntime runtime = DynamoRuntime.withBeanFactory(bf)) {
Class<?> controller = runtime.compileAndLoad(sourceCode);
runtime.registerController("dynamicCtrl", controller);
// The route is now live
}
// After the ClassLoader closes, the Bean remains in the container (managed by Spring lifecycle)
// runtime also supports: registerBean / unregisterBean / unregisterController /
// refreshController / addJarPath / compile / compileAndLoad (same semantics as DynamoClassLoader)
// Lightweight usage without a BeanFactory:
try (DynamoRuntime runtime = DynamoRuntime.create()) {
Class<?> c = runtime.compileAndLoad(source);
}
// Or specify a parent ClassLoader:
try (DynamoRuntime runtime = DynamoRuntime.create(customParent)) { ... }
Spring Boot Auto-Configuration
Activated automatically once the dependency is on the classpath. No @EnableXxx, no @Import needed:
WebMvcRegistrationsreplaces the defaultRequestMappingHandlerMappingwithDynamoRequestMappingHandlerMapping- All
spring.mvc.*properties continue to work (WebMvcRegistrationstransparently replaces the mapping; all setters are inherited by the subclass)
To disable:
spring.autoconfigure.exclude=cn.wubo.dynamo.spring.bean.DynamoBeanAutoConfiguration
Production-Classpath Configuration
Because local and server classpath paths differ, dynamic compilation on the server may fail to resolve imported classes. Add the following to your consumer project's pom.xml:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<classpathPrefix>lib/</classpathPrefix>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>package</phase>
<goals><goal>copy-dependencies</goal></goals>
<configuration>
<outputDirectory>${project.build.directory}/lib</outputDirectory>
<stripVersion>false</stripVersion>
<includeScope>runtime</includeScope>
</configuration>
</execution>
</executions>
</plugin>
At startup:
java -jar -Dloader.path=lib/ your-app.jar
Build, Test, CI
The repository root is the parent POM — run all commands from the root:
mvn -B test # Unit tests for all modules (113, distributed across core + starter)
mvn -B verify # Unit + integration tests (123 total = 113 unit + 10 IT; JaCoCo report generated to each module's target/site/jacoco/)
# Run a single module
mvn -B test -pl dynamo-spring-core -am
mvn -B verify -pl dynamo-spring-test -am
CI: GitHub Actions runs on ubuntu-latest with JDK 17, triggered on push and PR.
Demo Application
The dynamo-spring-test module ships with a runnable Spring Boot web application that combines the three core capabilities and the DynamoRuntime high-level facade into 4 tabs — play with it directly in your browser:
# Install all modules to the local repository (required first time; spring-boot:run depends on the starter jar)
mvn -B install -DskipTests
# Start the demo (default http://localhost:8080)
mvn -B -pl dynamo-spring-test spring-boot:run \
-Dspring-boot.run.jvmArguments="-Dnet.bytebuddy.experimental=true"
-Dnet.bytebuddy.experimental=trueenables ByteBuddy's experimental support on JDK 21+. It is not required on JDK 17.
The Four Tabs
| Tab | Path Prefix | Demonstration |
|---|---|---|
| 1. Dynamic Compilation | /api/compile/* |
Source → compile → load → instantiate → reflective call; list loaded instances, delete to free the ClassLoader |
| 2. AOP Interception | /api/aspect/* |
Source → compile + create ByteBuddy proxy; advice selector: log / timing / throw; on call, returns accumulated before / after / afterThrow logs |
| 3. Dynamic Controller | /api/bean/* |
Source (@RestController) → compile → DynamoBean.registerController → instantly routable in browser; 404 after unregister; list shows currently registered controllers and their routes |
| 4. DynamoRuntime | /api/runtime/* |
Open a session → within the session, compile / instantiate / invoke / registerController / unregisterController; Beans persist in the container after the session closes |
Key Files
dynamo-spring-test/src/main/
├── java/cn/wubo/dynamic/loader/utility/demo/
│ ├── DemoApp.java @SpringBootApplication launcher
│ ├── InstanceRegistry.java Centralized instance/proxy/session state
│ ├── CompileController.java Tab 1
│ ├── AspectController.java Tab 2
│ ├── BeanController.java Tab 3
│ ├── RuntimeController.java Tab 4
│ ├── GlobalExceptionHandler.java Translates 4xx/5xx into unified JSON
│ ├── ArgsHelper.java Reflection invocation helpers (findMethod / coerceArgs)
│ └── advice/
│ ├── LoggingAdvice.java log: records all three phases
│ ├── TimingAdvice.java timing: ThreadLocal StopWatch
│ └── ThrowAdvice.java throw: passively records; lets the source method itself throw to trigger afterThrow
└── resources/
├── application.yml server.port=8080
└── static/
├── index.html Single-page 4-tab UI
├── app.js Vanilla JS (fetch)
└── styles.css
End-to-End Examples (curl)
# Tab 1: Compile Greeter.greet("alice") → "hi alice"
curl -X POST http://localhost:8080/api/compile/load \
-H 'Content-Type: application/json' \
-d '{"source":"public class G { public String greet(String n){return \"hi \"+n;} }"}'
# Returns {"id":"...","className":"G","methods":[...]}
curl -X POST http://localhost:8080/api/compile/invoke \
-H 'Content-Type: application/json' \
-d '{"id":"...","methodName":"greet","args":["alice"]}'
# Returns {"result":"hi alice","resultType":"java.lang.String"}
# Tab 3: Register a controller dynamically
curl -X POST http://localhost:8080/api/bean/register \
-H 'Content-Type: application/json' \
-d '{"beanName":"dynCtrl","source":"@org.springframework.web.bind.annotation.RestController public class DynCtrl { @org.springframework.web.bind.annotation.GetMapping(\"/api/dyn\") public String dyn(){return \"dynamic\";} }"}'
curl http://localhost:8080/api/dyn # → "dynamic"
curl -X POST http://localhost:8080/api/bean/unregister \
-H 'Content-Type: application/json' -d '{"beanName":"dynCtrl"}'
curl -i http://localhost:8080/api/dyn # → HTTP/1.1 404
Relationship with test-jar
dynamo-spring-test produces both a default-jar (the demo app, runnable via java -jar) and a test-jar (IT infrastructure, reusable by consumers). The two artifacts do not interfere with each other:
- A normal
mvn installputs the default-jar into the local repository; - IT runs via
mvn verifythrough failsafe, operating on a separate classifier from the test-jar.
DemoApp and TestApp are two independent @SpringBootApplication classes: DemoApp lives under the cn.wubo.dynamo.spring.demo package (main classpath, used by spring-boot:run), while TestApp lives under cn.wubo.dynamo.spring (test classpath, IT launcher).
Test Coverage
| Dimension | Description |
|---|---|
| Core API | DynamoRuntime / DynamoClassLoader / DynamoProxy main paths |
| Concurrency | 8 threads × 20 compilations loading in parallel; multi-instance isolation; compile-vs-close race |
| Lifecycle | Idempotent close; post-close operations; Beans survive close |
| Edge / Error Recovery | Empty source / comments / Unicode class names / interfaces / enums / duplicate registration / unregister-of-nonexistent |
| AOP Scenarios | afterThrow triggers; advice isolation; ThreadLocal across threads |
| Spring Integration | registerController actually writes mappings; unregister + register replacement |
License
Apache License 2.0
Introduction
一个用于动态加载和管理 Java 类的工具库,支持动态编译、AOP 代理、Spring Bean 管理。会话模型 ClassLoader、ByteBuddy 代理、子类化 Spring MVC、自动配置。
Customize your domain