/loader-util

在应用运行期动态加载类、bean、rest、切面的工具

Primary LanguageJavaApache License 2.0Apache-2.0

loader-util 动态编译、加载、执行工具

star fork star fork
MIT JDK SpringBoot

在应用运行期动态编译加载类、bean、rest、切面的工具

重构了代码逻辑,将class、bean、rest合成到LoaderUtils中,
并增加DynamicClassLoader的单例模式、增加DynamicClassLoader的动态类缓存区,
缓存到内存的动态类可以在任何地方取出并执行

代码示例

  1. 使用动态编译工具实现的动态编译工具工具示例代码
  2. 使用动态调度消息中间件动态编译工具实体SQL工具实现的在线编码、动态调度、发送钉钉群消息、快速构造web页面Demo

快速开始

引入依赖

增加 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>

使用

编译Class并执行

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");
}

加载外部jar并执行

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");
}

编译Class并加载到Bean

使用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");
}

5. DynamicController 动态编译加载Controller并执行

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编译配置和启动命令做出一定的修改

1. 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>

2. 执行编译命令,会在jar包的同级目录下生成lib文件夹存放依赖包

img.png

3. 将jar包和lib文件夹上到服务器,并在启动命令中增加-Dloader.path=lib/

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。

解决办法:

  • idea启动的话,打开Project Strcutre,添加tools.jar img.png
  • 服务器启动,跑jar包的时候需要加入-Xbootclasspath/a:$toolspath/tools.jar参数,nohup java -Xbootclasspath/a:$toolspath/tools.jar -jar loader-util-test-0.0.1-SNAPSHOT.jar > /dev/null 2>&1 &