一个用于Java动态类加载、编译和Spring集成的工具库,支持运行时动态编译Java代码、加载类文件、注册Spring Bean等功能。
- 支持在运行时动态编译Java源代码
- 编译结果存储在内存中,无需写入磁盘文件
- 支持编译包含内部类的复杂Java代码
- 提供内存级类加载器 DynamicClassLoader
- 支持从JAR文件加载类
- 支持单例模式和一次性加载模式
- 动态注册单例Bean到Spring上下文
- 动态注册控制器(Controller)并自动映射请求路径
- 支持Bean的动态注销和重新注册
- 提供便捷的方法调用API
- 支持类级别和Bean级别的方法调用
- 集成CGLIB代理和切面编程支持
- 内置简单切面实现,可记录方法执行时间
- 支持自定义切面逻辑
- 提供方法执行前、执行后和异常处理的拦截点
主要的工具类,提供以下功能:
- compiler(): 编译Java源代码并加载到内存
- compilerOnce(): 一次性编译并加载Java代码
- load(): 加载已编译的类
- addJarPath(): 添加JAR文件到类路径
- registerSingleton(): 注册单例Bean
- registerController(): 注册控制器
- clear(): 清理动态类加载器缓存
方法调用工具类,提供:
- invokeClass(): 调用类中的方法
- invokeBean(): 调用Spring Bean中的方法
- proxy(): 创建代理对象并应用切面
Spring上下文工具类,提供:
- Bean的注册和注销
- 控制器的动态注册
- Spring上下文访问功能
增加 JitPack 仓库
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
1.1.0版本后升级到jdk17 SpringBoot3+
1.2.0重构核心代码
继续使用jdk 8请查看jdk8分支
<dependency>
<groupId>com.gitee.wb04307201</groupId>
<artifactId>loader-util</artifactId>
<version>1.2.0</version>
</dependency>
void testClass() {
String javaSourceCode = """
package cn.wubo.loader.util;
public class TestClass {
public String testMethod(String name){
return String.format("Hello,%s!",name);
}
}
""";
LoaderUtils.compiler(javaSourceCode, "cn.wubo.loader.util.TestClass");
Class<?> clazz = LoaderUtils.load("cn.wubo.loader.util.TestClass");
String str = (String) MethodUtils.invokeClass(clazz, "testMethod", "world");
}
//注意:如果重复编译同样的类,会发生异常,如果确实需要这种场景请使用LoaderUtils.compilerOnce
//也可以使用LoaderUtils.clear方法关闭旧的DynamicClassLoader单例后重新编译
// 通过LoaderUtils.compiler编译的类会缓存到内存中,可以在其他方法中获得
void testClassDelay() {
Class<?> clazz = LoaderUtils.load("cn.wubo.loader.util.TestClass");
String str = (String) MethodUtils.invokeClass(clazz, "testMethod", "world");
}
//如果不想将编译的类会缓存到内存,请使用LoaderUtils.compilerOnce方法
void testClassOnce() {
String javaSourceCode = """
package cn.wubo.loader.util;
public class TestClass7 {
public String testMethod(String name){
return String.format("Hello,%s!",name);
}
}
""";
Class<?> clazz = LoaderUtils.compilerOnce(javaSourceCode, "cn.wubo.loader.util.TestClass7");
String str = (String) MethodUtils.invokeClass(clazz, "testMethod", "world");
}
void testJarClass() {
LoaderUtils.addJarPath("./hutool-all-5.8.29.jar");
Class<?> clazz = LoaderUtils.load("cn.hutool.core.util.IdUtil");
String str = (String) MethodUtils.invokeClass(clazz, "randomUUID");
}
使用DynamicBean需要配置@ComponentScan,包括cn.wubo.loader.util.SpringContextUtils文件
void testBean() {
String javaSourceCode = """
package cn.wubo.loader.util;
public class TestClass2 {
public String testMethod(String name){
return String.format("Hello,%s!",name);
}
}
""";
LoaderUtils.compiler(javaSourceCode, "cn.wubo.loader.util.TestClass2");
Class<?> clazz = LoaderUtils.load("cn.wubo.loader.util.TestClass2");
String beanName = LoaderUtils.registerSingleton(clazz);
String str = MethodUtils.invokeBean(beanName, "testMethod", "world");
}
public void loadController() {
String fullClassName = "cn.wubo.loaderutiltest.DemoController";
String javaSourceCode = """
package cn.wubo.loaderutiltest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping(value = "test")
public class DemoController {
@GetMapping(value = "hello")
public String hello(@RequestParam(value = "name") String name) {
return String.format("Hello,%s!",name);
}
}
""";
LoaderUtils.compiler(javaSourceCode, "cn.wubo.loaderutiltest.DemoController");
Class<?> clazz = LoaderUtils.load("cn.wubo.loaderutiltest.DemoController");
String beanName = LoaderUtils.registerController(clazz);
}
GET http://localhost:8080/test/hello?name=world
Accept: application/json
Hello,world!
void testAspect() {
String javaSourceCode = """
package cn.wubo.loader.util;
public class TestClass6 {
public String testMethod(String name){
return String.format("Hello,%s!",name);
}
}
""";
LoaderUtils.compiler(javaSourceCode, "cn.wubo.loader.util.TestClass6");
Class<?> clazz = LoaderUtils.load("cn.wubo.loader.util.TestClass6");
try {
Object obj = MethodUtils.proxy(clazz.newInstance());
String str = MethodUtils.invokeClass(obj, "testMethod", "world");
} catch (InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
输出示例
2023-04-08 21:22:14.174 INFO 32660 --- [nio-8080-exec-1] cn.wubo.loader.util.aspect.SimpleAspect : SimpleAspect before cn.wubo.loader.util.TestClass testMethod
2023-04-08 21:22:14.175 INFO 32660 --- [nio-8080-exec-1] cn.wubo.loader.util.aspect.SimpleAspect : SimpleAspect after cn.wubo.loader.util.TestClass testMethod
2023-04-08 21:22:14.175 INFO 32660 --- [nio-8080-exec-1] cn.wubo.loader.util.aspect.SimpleAspect : StopWatch 'cn.wubo.loader.util.TestClass testMethod': running time = 65800 ns
可以通过继承IAspect接口实现自定义切面,并通过MethodUtils.proxy(Class<?> clazz, Class<? extends IAspect> aspectClass)方法调用切面
因为本地和服务器的差异导致classpath路径不同,
进而使服务上动态编译class时会发生找不到import类的异常,
因此需要对maven编译配置和启动命令做出一定的修改
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<!-- 是否要把第三方jar加入到类构建路径 -->
<addClasspath>true</addClasspath>
<!-- 外部依赖jar包的最终位置 -->
<classpathPrefix>lib/</classpathPrefix>
<!--指定jar程序入口-->
<mainClass>cn.wubo.loaderutiltest.LoaderUtilTestApplication</mainClass>
</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>
<!-- lib依赖包输出目录,打包的时候不打进jar包里 -->
<outputDirectory>${project.build.directory}/lib</outputDirectory>
<excludeTransitive>false</excludeTransitive>
<stripVersion>false</stripVersion>
<includeScope>runtime</includeScope>
</configuration>
</execution>
</executions>
</plugin>
java -jar -Dloader.path=lib/ loader-util-test-0.0.1-SNAPSHOT.jar
如果编译报错: Can't initialize javac processor due to (most likely) a class loader problem: java.lang.NoClassDefFoundError: com/sun/tools/javac/processing/JavacProcessingEnvironment
这是因为JAVA编译器是通过JavaFileManager来加载相关依赖类的,而JavaFileManager来自tools.jar。
解决办法: