diff --git a/README.md b/README.md index 0c29a125c3f0253eb33aa23fd815f30c69fe5569..5d9aefebad235e595a7e18fb870410fa28c861a8 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# loader-util 动态编译工具 +# loader-util 动态编译、加载、执行工具 [![](https://jitpack.io/v/com.gitee.wb04307201/loader-util.svg)](https://jitpack.io/#com.gitee.wb04307201/loader-util) [![star](https://gitee.com/wb04307201/loader-util/badge/star.svg?theme=dark)](https://gitee.com/wb04307201/loader-util) @@ -8,13 +8,10 @@ ![MIT](https://img.shields.io/badge/License-Apache2.0-blue.svg) ![JDK](https://img.shields.io/badge/JDK-17+-green.svg) ![SpringBoot](https://img.shields.io/badge/Srping%20Boot-3+-green.svg) > 在应用运行期动态编译加载类、bean、rest、切面的工具 -> 包含: -> 1. DynamicBean 动态加载Bean并执行; -> 2. DynamicClass 动态编译加载Class并执行; -> 3. DynamicJar 动态加载外部jar到项目中; -> 4. DynamicGroovy 动态编译加载Groovy并执行; -> 5. DynamicController 动态编译加载Controller并执行; -> 6. proxy 动态代理切面。 +> +> 重构了代码逻辑,将class、bean、rest合成到LoaderUtils中, +> 并增加DynamicClassLoader的单例模式、增加DynamicClassLoader的动态类缓存区, +> 缓存到内存的动态类可以在任何地方取出并执行 ## 代码示例 1. 使用[动态编译工具](https://gitee.com/wb04307201/loader-util)实现的[动态编译工具工具示例代码](https://gitee.com/wb04307201/loader-util-test) @@ -33,123 +30,116 @@ ``` 1.1.0版本后升级到jdk17 SpringBoot3+ +1.2.0重构核心代码 继续使用jdk 8请查看jdk8分支 ```xml com.gitee.wb04307201 loader-util - 1.1.4 + 1.2.0 ``` ### 使用 -#### 1. DynamicClass 动态编译Class并执行 +#### 编译Class并执行 ```java - @GetMapping(value = "/test/class") - public String testClass(){ - String javaSourceCode = "package cn.wubo.loader.util;\n" + - "\n" + - "public class TestClass {\n" + - " \n" + - " public String testMethod(String name){\n" + - " return String.format(\"Hello,%s!\",name);\n" + - " }\n" + - "}"; - String fullClassName = "cn.wubo.loader.util.TestClass"; - String methodName = "testMethod"; - DynamicClass dynamicClass = DynamicClass.init(javaSourceCode, fullClassName).compiler(); - Class clasz = dynamicClass.load(); - return (String) MethodUtils.invokeClass(clasz, methodName, "world"); - } +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"); +} ``` -#### 2. DynamicJar 动态加载外部jar并执行 +#### 加载外部jar并执行 ```java - @GetMapping(value = "/test/jar") - public String testJar(){ - Class clasz = DynamicJar.init("D:\\maven-repository\\repository\\cn\\hutool\\hutool-all\\5.3.2\\hutool-all-5.3.2.jar").load("cn.hutool.core.util.IdUtil"); - return (String) MethodUtils.invokeClass(clasz, "randomUUID"); - } +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"); +} ``` -#### 3. DynamicBean 动态编译加载Bean并执行 +#### 编译Class并加载到Bean > 使用DynamicBean需要配置@ComponentScan,包括cn.wubo.loader.util.SpringContextUtils文件 ```java - @GetMapping(value = "/test/bean") - public String testBean(){ - String javaSourceCode = "package cn.wubo.loader.util;\n" + - "\n" + - "public class TestClass {\n" + - " \n" + - " public String testMethod(String name){\n" + - " return String.format(\"Hello,%s!\",name);\n" + - " }\n" + - "}"; - String fullClassName = "cn.wubo.loader.util.TestClass"; - String methodName = "testMethod"; - String beanName = DynamicBean.init(DynamicClass.init(javaSourceCode,fullClassName)).load(); - return (String) MethodUtils.invokeBean(beanName,methodName,"world"); - } -``` - -#### 4.可通过Groovy动态编译Class -添加依赖 -```xml - - org.apache.groovy - groovy - 4.0.21 - -``` -编译class -```java - @GetMapping(value = "/loadAndInvokeGroovy") - public String loadAndInvokeGroovy() { - try (GroovyClassLoader groovyClassLoader = new GroovyClassLoader()) { - groovyClassLoader.parseClass(javaSourceCode); - Class clasz = groovyClassLoader.parseClass(javaSourceCode); - return (String) MethodUtils.invokeClass(clasz, methodName, "world"); - } catch (IOException e) { - throw new RuntimeException(e); - } - } +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并执行 ```java - @GetMapping(value = "/loadController") - public String loadController() { - String fullClassName = "cn.wubo.loaderutiltest.DemoController"; - String javaSourceCode = """ - package cn.wubo.loaderutiltest; - - import lombok.AllArgsConstructor; - import lombok.Data; - import lombok.NoArgsConstructor; - 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 User hello(@RequestParam(value = "name") String name) { - return new User(name); - } - - @Data - @AllArgsConstructor - @NoArgsConstructor - public static class User { - private String name; - } - } - """; - return DynamicController.init(DynamicClass.init(javaSourceCode, fullClassName)).load(); - } +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); +} ``` ```http request GET http://localhost:8080/test/hello?name=world @@ -158,25 +148,28 @@ Accept: application/json Hello,world! ``` -#### 6. proxy 动态代理切面 +#### 动态增加切面代理 ```java - @GetMapping(value = "/testAspect") - public String testAspect() throws InstantiationException, IllegalAccessException { - String javaSourceCode = "package cn.wubo.loader.util;\n" + - "\n" + - "public class TestClass {\n" + - " \n" + - " public String testMethod(String name){\n" + - " return String.format(\"Hello,%s!\",name);\n" + - " }\n" + - "}"; - String fullClassName = "cn.wubo.loader.util.TestClass"; - String methodName = "testMethod"; - DynamicClass dynamicClass = DynamicClass.init(javaSourceCode, fullClassName).compiler(); - Class clasz = dynamicClass.load(); - Object obj = MethodUtils.proxy(clasz.newInstance()); - return (String) MethodUtils.invokeClass(obj, methodName, "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); + } +} ``` 输出示例 ```text diff --git a/hutool-all-5.8.29.jar b/hutool-all-5.8.29.jar new file mode 100644 index 0000000000000000000000000000000000000000..13e345b80225592474f2fd0df95451aa9af6c48d Binary files /dev/null and b/hutool-all-5.8.29.jar differ diff --git a/pom.xml b/pom.xml index b90d66e5f989aaeeaeed8f2eb9332b3dbb511128..46f7f95dd12f6189f31a73fbda38a182e9230e57 100644 --- a/pom.xml +++ b/pom.xml @@ -19,14 +19,14 @@ org.springframework.boot spring-boot-dependencies - 3.2.5 + 3.3.1 pom import org.projectlombok lombok - 1.18.32 + 1.18.34 ch.qos.logback @@ -54,9 +54,17 @@ ch.qos.logback logback-classic + + + org.springframework.boot + spring-boot-starter-test + test + org.apache.groovy groovy + 4.0.21 + test \ No newline at end of file diff --git a/src/main/java/cn/wubo/loader/util/LoaderUtils.java b/src/main/java/cn/wubo/loader/util/LoaderUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..082977d0625706b374e11e537c915abbff440e91 --- /dev/null +++ b/src/main/java/cn/wubo/loader/util/LoaderUtils.java @@ -0,0 +1,149 @@ +package cn.wubo.loader.util; + +import cn.wubo.loader.util.class_loader.DynamicClassLoader; +import cn.wubo.loader.util.class_loader.JavaBuilder; +import cn.wubo.loader.util.class_loader.JavaMemClass; +import cn.wubo.loader.util.exception.LoaderRuntimeException; +import lombok.extern.slf4j.Slf4j; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Map; + +@Slf4j +public class LoaderUtils { + + /** + * 编译Java源代码并加载到内存中。 + * + * @param javaSourceCode Java源代码字符串。 + * @param fullClassName 完全限定类名。 + */ + public static void compiler(String javaSourceCode, String fullClassName) { + // 使用JavaBuilder编译Java源代码并获取内存中的类映射 + Map javaMemClassMap = JavaBuilder.builder().compiler(javaSourceCode, fullClassName).getJavaMemClassMap(); + // 获取动态类加载器实例 + DynamicClassLoader classLoader = DynamicClassLoader.getInstance(); + // 将编译后的类信息添加到类加载器中 + for (Map.Entry entry : javaMemClassMap.entrySet()) { + classLoader.addClassMap(entry.getKey(), entry.getValue().getBytes()); + } + } + + /** + * 动态编译并加载Java源代码。 + *

+ * 该方法接收Java源代码和完整的类名作为参数,通过动态编译源代码并使用自定义类加载器加载生成的类,实现动态代码的执行。 + * 这种方式常用于运行时生成和执行代码,例如在某些框架中用于动态生成代理类。 + * + * @param javaSourceCode Java源代码字符串。 + * @param fullClassName 完整的类名,包括包名。 + * @return 编译并加载后的类的Class对象。 + * @throws LoaderRuntimeException 如果编译或加载过程中发生错误,将抛出此运行时异常。 + */ + public static Class compilerOnce(String javaSourceCode, String fullClassName) { + // 使用JavaBuilder编译Java源代码,生成内存中的类对象。 + Map javaMemClassMap = JavaBuilder.builder().compiler(javaSourceCode, fullClassName).getJavaMemClassMap(); + + try (DynamicClassLoader dynamicClassLoader = new DynamicClassLoader(Thread.currentThread().getContextClassLoader())) { + // 将内存中的类信息添加到自定义类加载器的类映射中。 + for (Map.Entry entry : javaMemClassMap.entrySet()) { + dynamicClassLoader.addClassMap(entry.getKey(), entry.getValue().getBytes()); + } + // 使用自定义类加载器加载指定的类。 + return dynamicClassLoader.loadClass(fullClassName); + } catch (IOException | ClassNotFoundException e) { + // 抛出自定义异常,以更清晰地指示编译或加载过程中的错误。 + throw new LoaderRuntimeException(e.getMessage(), e); + } + } + + /** + * 通过类名加载类。 + * + * @param fullClassName 完全限定类名。 + * @return 加载的类。 + */ + public static Class load(String fullClassName) { + try { + // 获取动态类加载器实例并加载类 + DynamicClassLoader classLoader = DynamicClassLoader.getInstance(); + return classLoader.loadClass(fullClassName); + } catch (ClassNotFoundException e) { + // 抛出运行时异常,携带错误信息 + throw new LoaderRuntimeException(e.getMessage(), e); + } + } + + /** + * 将JAR路径添加到类路径中。 + * + * @param jarPath JAR文件路径。 + */ + public static void addJarPath(String jarPath) { + try { + // 获取动态类加载器实例并添加JAR路径 + DynamicClassLoader.getInstance().addURL(new URL("jar:file:" + new File(jarPath).getAbsolutePath() + "!/")); + } catch (MalformedURLException e) { + // 抛出运行时异常,携带错误信息 + throw new LoaderRuntimeException(e.getMessage(), e); + } + } + + /** + * 注册单例 bean 到 Spring 应用上下文中。 + * + * 此方法用于将指定的类作为单例 bean 注册到 Spring 应用上下文中。如果该类已经注册为 bean,则先销毁已存在的 bean 实例,然后重新注册。 + * 这确保了应用上下文中只有一个该类的实例存在。 + * + * @param clazz 要注册为单例 bean 的类。 + * @return 注册后的 bean 名称。 + */ + public static String registerSingleton(Class clazz) { + // 通过类名生成 bean 名称。 + String beanName = SpringContextUtils.beanName(clazz.getName()); + // 检查是否已存在同名的 bean。如果存在,则先销毁该 bean。 + if (Boolean.TRUE.equals(SpringContextUtils.containsBean(beanName))) { + SpringContextUtils.destroy(beanName); + } + // 注册指定类为单例 bean。 + SpringContextUtils.registerSingleton(beanName, clazz); + // 返回注册后的 bean 名称。 + return beanName; + } + + /** + * 注册控制器类到Spring上下文中。 + * + * 本方法用于将给定的控制器类注册到Spring应用程序上下文中。如果该类已经注册,则先注销原有的实例,再重新注册。 + * 这确保了Spring上下文中总是存在最新版本的控制器类实例。 + * + * @param clazz 待注册的控制器类。此参数不应为null。 + * @return 注册后的bean名称。如果给定的类已经被注册,则返回更新后的bean名称。 + */ + public static String registerController(Class clazz) { + // 通过类名生成bean名称 + String beanName = SpringContextUtils.beanName(clazz.getName()); + // 检查是否已经存在同名的bean + if (Boolean.TRUE.equals(SpringContextUtils.containsBean(beanName))) + // 如果存在,则先注销原有的bean + SpringContextUtils.unregisterController(beanName); + // 注册新的控制器类 + SpringContextUtils.registerController(beanName, clazz); + // 返回注册后的bean名称 + return beanName; + } + + /** + * 清理动态类加载器的缓存。 + * + * 该方法调用了DynamicClassLoader的clear方法,旨在清理动态加载的类,以释放内存资源。 + * 当系统不再需要这些动态加载的类,或者需要重新加载类时,可以调用此方法。 + */ + public static void clear() { + DynamicClassLoader.clear(); + } +} + diff --git a/src/main/java/cn/wubo/loader/util/SpringContextUtils.java b/src/main/java/cn/wubo/loader/util/SpringContextUtils.java index 969d7051a56295bdd25406f624a18993dbfc4a76..50ddf180a26a15f9eaf3b6dac69b133af5ce2930 100644 --- a/src/main/java/cn/wubo/loader/util/SpringContextUtils.java +++ b/src/main/java/cn/wubo/loader/util/SpringContextUtils.java @@ -26,80 +26,107 @@ public class SpringContextUtils implements BeanFactoryAware { } /** - * 注册单例Bean + * 注册一个单例bean。此方法为泛型方法,允许注册任何类型的单例对象。 + * 通过传入类类型,系统可以在运行时创建该类型的单例实例。 * - * @param type Bean的类型 + * @param type 类类型,表示要注册为单例的类。 + * @param 类型参数,表示泛型类型。 */ public static void registerSingleton(Class type) { + // 使用类的名称作为bean的名称来注册单例 registerSingleton(beanName(type.getName()), type); } /** - * 注册单例Bean + * 注册一个单例对象到Spring Bean工厂。 * - * @param beanName Bean的名称 - * @param type Bean的类型 + * 该方法通过以下步骤实现单例注册: + * 1. 使用提供的类型创建一个对象实例。 + * 2. 对该对象进行自动装配,使其能够与其他Spring管理的bean进行依赖注入。 + * 3. 将该对象注册为一个单例bean,使用提供的bean名称进行标识。 + * + * @param beanName 将要注册的bean的名称。 + * @param type bean的类型,用于创建对象实例。 + * @param 泛型参数,指定bean的类型。 */ public static void registerSingleton(String beanName, Class type) { - // 创建Bean实例 + // 根据提供的类型创建bean实例。 T obj = listableBeanFactory.createBean(type); - // 自动装配Bean依赖 + // 对创建的bean实例进行自动装配。 listableBeanFactory.autowireBean(obj); - // 注册单例Bean + // 将装配好的bean实例注册为单例。 listableBeanFactory.registerSingleton(beanName, obj); } /** - * 注册控制器 + * 注册控制器类并触发Spring MVC的映射处理方法。 + * + * 本方法主要用于在运行时动态注册控制器类,使得这些类能够被Spring MVC框架识别并处理相应的HTTP请求。 + * 通过调用registerSingleton方法将控制器类注册为Spring Bean,确保Spring容器能够管理这个类的实例。 + * 随后,通过获取RequestMappingHandlerMapping实例并调用其detectHandlerMethods方法,来触发Spring MVC对新注册控制器的映射处理。 + * 这一过程对于动态加载控制器类,例如在插件开发或者热部署场景中,是非常关键的。 * - * @param beanName 控制器的bean名称 - * @param type 控制器的类型 + * @param beanName 控制器类在Spring容器中的Bean名称。 + * @param type 控制器类的类型。 + * @throws LoaderRuntimeException 如果在尝试检测处理器方法过程中发生异常,则抛出此运行时异常。 + * @param 控制器类的类型参数。 */ public static void registerController(String beanName, Class type) { + // 注册控制器类为Spring Bean。 registerSingleton(beanName, type); - // 获取RequestMappingHandlerMapping对象 + + // 获取RequestMappingHandlerMapping实例,用于处理请求映射。 RequestMappingHandlerMapping requestMappingHandlerMapping = getBean("requestMappingHandlerMapping"); + try { - // 获取detectHandlerMethods方法 + // 通过反射获取父类的父类(即AbstractHandlerMethodMapping)中的detectHandlerMethods方法。 + // 这是因为RequestMappingHandlerMapping自身并没有提供公开的detectHandlerMethods方法。 Method method = requestMappingHandlerMapping.getClass().getSuperclass().getSuperclass().getDeclaredMethod("detectHandlerMethods", Object.class); - // 设置detectHandlerMethods方法可访问 + // 设置方法可访问,以绕过Java的访问控制。 method.setAccessible(true); - // 调用detectHandlerMethods方法,将控制器对象作为参数传入 + // 调用detectHandlerMethods方法,传入控制器Bean的名称,以触发映射处理。 method.invoke(requestMappingHandlerMapping, beanName); } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { - // 抛出异常 + // 如果在处理过程中发生异常,则抛出自定义的运行时异常。 throw new LoaderRuntimeException(e.getMessage(), e); } } /** - * 注销控制器 + * 取消注册一个控制器(bean)。 + * 该方法通过获取特定bean实例,分析并移除其上的@RequestMapping映射,从而实现控制器的注销功能。 + * 主要用于在运行时动态调整应用程序的路由配置。 * - * @param beanName 控制器的bean名称 + * @param beanName 需要注销的控制器bean的名称。 */ public static void unregisterController(String beanName) { - // 获取控制器对象 + // 根据bean名称获取bean实例。 Object obj = getBean(beanName); - // 获取控制器的类型 + // 获取bean实例的类类型。 Class type = obj.getClass(); - // 获取RequestMappingHandlerMapping对象 + + // 获取RequestMappingHandlerMapping的实例,用于处理@RequestMapping的映射注销。 RequestMappingHandlerMapping requestMappingHandlerMapping = getBean("requestMappingHandlerMapping"); - // 遍历控制器的方法 + + // 遍历类型上的所有方法,寻找并处理@RequestMapping注解的方法。 ReflectionUtils.doWithMethods(type, method -> { - // 获取最具体的实现方法 + // 获取最具体的方法,用于处理重载方法的情况。 Method mostSpecificMethod = ClassUtils.getMostSpecificMethod(method, type); try { - // 获取RequestMappingInfo实例 + // 获取RequestMappingHandlerMapping中用于获取方法映射的方法。 Method declaredMethod = requestMappingHandlerMapping.getClass().getDeclaredMethod("getMappingForMethod", Method.class, Class.class); + // 设置该方法可访问,因为它是protected的。 declaredMethod.setAccessible(true); + // 调用getMappingForMethod方法,获取对应方法的RequestMappingInfo对象。 RequestMappingInfo requestMappingInfo = (RequestMappingInfo) declaredMethod.invoke(requestMappingHandlerMapping, mostSpecificMethod, type); - // 如果RequestMappingInfo不为空,则注销对应的映射 + // 如果RequestMappingInfo不为空,则从映射中注销该方法。 if (requestMappingInfo != null) requestMappingHandlerMapping.unregisterMapping(requestMappingInfo); } catch (InvocationTargetException | NoSuchMethodException | IllegalAccessException e) { + // 抛出运行时异常,封装原始异常信息。 throw new LoaderRuntimeException(e.getMessage(), e); } }); - // 销毁控制器的bean + // 销毁单例bean,确保它在容器中被重新创建而不是重新使用。 listableBeanFactory.destroySingleton(beanName); } diff --git a/src/main/java/cn/wubo/loader/util/bean_loader/DynamicBean.java b/src/main/java/cn/wubo/loader/util/bean_loader/DynamicBean.java deleted file mode 100644 index 48a5cd470c57995f03ba09c3b1ac67aa3ead62e1..0000000000000000000000000000000000000000 --- a/src/main/java/cn/wubo/loader/util/bean_loader/DynamicBean.java +++ /dev/null @@ -1,46 +0,0 @@ -package cn.wubo.loader.util.bean_loader; - -import cn.wubo.loader.util.SpringContextUtils; -import cn.wubo.loader.util.class_loader.DynamicClass; -import lombok.extern.slf4j.Slf4j; - -@Slf4j -public class DynamicBean { - - private DynamicClass dynamicClass; - - public DynamicBean(DynamicClass dynamicClass) { - this.dynamicClass = dynamicClass; - } - - /** - * 初始化一个动态类生成的动态Bean对象 - * - * @param dynamicClass 动态类对象,表示描述生成动态Bean的类信息 - * @return 初始化完成的动态Bean对象 - */ - public static DynamicBean init(DynamicClass dynamicClass) { - log.debug("初始化bean fullClassName:{}", dynamicClass.getFullClassName()); - return new DynamicBean(dynamicClass); - } - - /** - * 加载动态类并注册到Spring容器中。 - * 此方法首先尝试获取动态类的bean名称,然后检查该bean是否已在Spring容器中存在。 - * 如果已存在,则销毁该bean。接着,加载动态类的Class对象,并将其作为单例bean注册到Spring容器中。 - * - * @return 注册的bean名称 - */ - public String load() { - // 获取动态类的bean名称 - String beanName = SpringContextUtils.beanName(dynamicClass.getFullClassName()); - // 如果Spring容器中已存在该bean,则销毁该bean - if (Boolean.TRUE.equals(SpringContextUtils.containsBean(beanName))) SpringContextUtils.destroy(beanName); - // 加载动态类并获取其Class对象 - Class type = dynamicClass.compiler().load(); - // 将该Class对象注册为Spring容器中的单例bean - SpringContextUtils.registerSingleton(beanName, type); - // 返回注册的bean名称 - return beanName; - } -} diff --git a/src/main/java/cn/wubo/loader/util/class_loader/DynamicClass.java b/src/main/java/cn/wubo/loader/util/class_loader/DynamicClass.java deleted file mode 100644 index e8d049d0a53e5177137ec94ecc28bca20c1a759b..0000000000000000000000000000000000000000 --- a/src/main/java/cn/wubo/loader/util/class_loader/DynamicClass.java +++ /dev/null @@ -1,211 +0,0 @@ -package cn.wubo.loader.util.class_loader; - -import cn.wubo.loader.util.exception.LoaderRuntimeException; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; - -import javax.tools.*; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -/** - * 将java内容编译成class,放入内存中 - */ -@Slf4j -public class DynamicClass { - private String javaSourceCode; - @Getter - private String fullClassName; - private List options = new ArrayList<>(); - private MemFileManager fileManager; - - public DynamicClass(String javaSourceCode, String fullClassName) { - this.javaSourceCode = javaSourceCode; - this.fullClassName = fullClassName; - } - - /** - * 初始化一个DynamicClass对象 - * - * @param javaSourceCode Java源代码字符串 - * @param fullClassName 完整的类名 - * @return 初始化后的DynamicClass对象 - */ - public static DynamicClass init(String javaSourceCode, String fullClassName) { - log.debug("初始化class javaSourceCode:{} fullClassName:{}", javaSourceCode, fullClassName); - return new DynamicClass(javaSourceCode, fullClassName); - } - - - /** - * -cp <目录和 zip/jar 文件的类搜索路径> - * -classpath <目录和 zip/jar 文件的类搜索路径> - * --class-path <目录和 zip/jar 文件的类搜索路径> - * 使用 ; 分隔的, 用于搜索类文件的目录, JAR 档案 - * 和 ZIP 档案列表。 - * -p <模块路径> - * --module-path <模块路径>... - * 用 ; 分隔的目录列表, 每个目录 - * 都是一个包含模块的目录。 - * --upgrade-module-path <模块路径>... - * 用 ; 分隔的目录列表, 每个目录 - * 都是一个包含模块的目录, 这些模块 - * 用于替换运行时映像中的可升级模块 - * --add-modules <模块名称>[,<模块名称>...] - * 除了初始模块之外要解析的根模块。 - * <模块名称> 还可以为 ALL-DEFAULT, ALL-SYSTEM, - * ALL-MODULE-PATH. - * --list-modules - * 列出可观察模块并退出 - * -d - * --describe-module <模块名称> - * 描述模块并退出 - * --dry-run 创建 VM 并加载主类, 但不执行 main 方法。 - * 此 --dry-run 选项对于验证诸如 - * 模块系统配置这样的命令行选项可能非常有用。 - * --validate-modules - * 验证所有模块并退出 - * --validate-modules 选项对于查找 - * 模块路径中模块的冲突及其他错误可能非常有用。 - * -D<名称>=<值> - * 设置系统属性 - * -verbose:[class|module|gc|jni] - * 为给定子系统启用详细输出 - * -version 将产品版本输出到错误流并退出 - * --version 将产品版本输出到输出流并退出 - * -showversion 将产品版本输出到错误流并继续 - * --show-version - * 将产品版本输出到输出流并继续 - * --show-module-resolution - * 在启动过程中显示模块解析输出 - * -? -h -help - * 将此帮助消息输出到错误流 - * --help 将此帮助消息输出到输出流 - * -X 将额外选项的帮助输出到错误流 - * --help-extra 将额外选项的帮助输出到输出流 - * -ea[:<程序包名称>...|:<类名>] - * -enableassertions[:<程序包名称>...|:<类名>] - * 按指定的粒度启用断言 - * -da[:<程序包名称>...|:<类名>] - * -disableassertions[:<程序包名称>...|:<类名>] - * 按指定的粒度禁用断言 - * -esa | -enablesystemassertions - * 启用系统断言 - * -dsa | -disablesystemassertions - * 禁用系统断言 - * -agentlib:<库名>[=<选项>] - * 加载本机代理库 <库名>, 例如 -agentlib:jdwp - * 另请参阅 -agentlib:jdwp=help - * -agentpath:<路径名>[=<选项>] - * 按完整路径名加载本机代理库 - * -javaagent:[=<选项>] - * 加载 Java 编程语言代理, 请参阅 java.lang.instrument - * -splash:<图像路径> - * 使用指定的图像显示启动屏幕 - * 自动支持和使用 HiDPI 缩放图像 - * (如果可用)。应始终将未缩放的图像文件名 (例如, image.ext) - * 作为参数传递给 -splash 选项。 - * 将自动选取提供的最合适的缩放 - * 图像。 - * 有关详细信息, 请参阅 SplashScreen API 文档 - * - * @param options - * @return - * @argument 文件 - * 一个或多个包含选项的参数文件 - * -disable-@files - * 阻止进一步扩展参数文件 - * --enable-preview - * 允许类依赖于此发行版的预览功能 - * 要为长选项指定参数, 可以使用 --<名称>=<值> 或 - * --<名称> <值>。 - */ - public DynamicClass config(List options) { - log.debug("添加编译配置 options:{}", options.stream().collect(Collectors.joining(" "))); - this.options = options; - return this; - } - - /** - * 编译配置classpath - * - * @param classpath - * @return - */ - public DynamicClass addClasspath(String classpath) { - log.debug("添加编译配置 classpath:{}", classpath); - options.add("-classpath"); - options.add(classpath); - return this; - } - - /** - * 编译Java代码 - * - * @return 返回DynamicClass对象 - */ - public DynamicClass compiler() { - log.debug("开始执行编译"); - - // 获取Java编译器 - JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); - - // 创建诊断收集器 - DiagnosticCollector diagnosticCollector = new DiagnosticCollector<>(); - - // 创建内存文件管理器 - this.fileManager = new MemFileManager(compiler.getStandardFileManager(diagnosticCollector, null, null)); - - // 创建JavaMemSource对象 - JavaMemSource file = new JavaMemSource(fullClassName, javaSourceCode); - - // 创建编译单元Iterable - Iterable compilationUnits = Collections.singletonList(file); - - log.debug("获取编译任务"); - // 获取编译任务 - JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, diagnosticCollector, options, null, compilationUnits); - - log.debug("执行编译"); - boolean result = task.call(); - // 执行编译任务 - if (!result) { - // 处理编译错误 - String errorMessage = diagnosticCollector.getDiagnostics().stream() - .map(Object::toString) - .reduce("", (acc, x) -> acc + "\r\n" + x); - log.debug("编译失败: {}", errorMessage); - throw new LoaderRuntimeException("编译失败: " + errorMessage); - } - log.debug("编译成功"); - return this; - } - - - - /** - * 加载指定类的类对象。 - * - * @return 返回指定类的类对象。 - * @throws LoaderRuntimeException 如果找不到指定类则抛出该异常。 - */ - public Class load() { - try { - // 获取已编译的类数据 - Map compiledClasses = fileManager.getAllCompiledClassesData(); - // 创建动态类加载器 - DynamicClassLoader classLoader = new DynamicClassLoader(Thread.currentThread().getContextClassLoader()); - // 将已编译的类数据添加到动态类加载器中 - compiledClasses.forEach(classLoader::addClass); - // 加载指定类的类对象 - return classLoader.loadClass(fullClassName); - } catch (ClassNotFoundException e) { - // 加载类失败,抛出LoaderRuntimeException异常 - throw new LoaderRuntimeException("加载类失败: " + e.getMessage(), e); - } - } - -} diff --git a/src/main/java/cn/wubo/loader/util/class_loader/DynamicClassLoader.java b/src/main/java/cn/wubo/loader/util/class_loader/DynamicClassLoader.java index 0b239d3905944dd55b07ea49c677fb9496911ffe..56b20bd60180b6dc3c3220f634777d4901d80cca 100644 --- a/src/main/java/cn/wubo/loader/util/class_loader/DynamicClassLoader.java +++ b/src/main/java/cn/wubo/loader/util/class_loader/DynamicClassLoader.java @@ -1,56 +1,98 @@ package cn.wubo.loader.util.class_loader; -import java.security.SecureClassLoader; +import cn.wubo.loader.util.exception.LoaderRuntimeException; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; import java.util.HashMap; import java.util.Map; /** - * @description:动态编译加载器 - * @author: wubo - * @date: 2022-11-21 + * 动态类加载器,扩展自URLClassLoader,支持动态加载类。 + * 通过将类的字节码与类名映射,实现动态加载和查找类的功能。 */ -public class DynamicClassLoader extends SecureClassLoader { +@Slf4j +public class DynamicClassLoader extends URLClassLoader { /** - * 编译的时候返回的class字节数组-支持内部类 + * 类映射,存储类的完全限定名和对应的Class对象。 */ - private final Map classBytes = new HashMap<>(); + private Map> classMap = new HashMap<>(); + /** + * 构造函数,使用当前线程的上下文类加载器作为父类加载器。 + * + * @param parent 父类加载器 + */ public DynamicClassLoader(ClassLoader parent) { - super(parent); + super(new URL[0], parent); } /** - * 该方法用于添加一个类的完全限定名及其对应的数据到类信息存储结构中。 + * 单例模式,获取动态类加载器的实例。 + */ + @Getter + private static DynamicClassLoader instance = new DynamicClassLoader(Thread.currentThread().getContextClassLoader()); + + /** + * 清理并重新初始化动态类加载器。 + * 该方法旨在重新配置动态类加载器,以便它可以继续加载新的类,而不会受到之前加载类的影响。 + * 这是通过关闭当前的动态类加载器实例并创建一个新的实例来实现的。 + * 在这个过程中,它使用当前线程的上下文类加载器作为新实例的基础。 + * 如果在关闭当前实例或创建新实例的过程中发生IO异常,将会抛出一个LoaderRuntimeException。 + */ + public static void clear() { + try { + // 关闭当前的动态类加载器实例 + instance.close(); + // 创建一个新的动态类加载器实例,基于当前线程的上下文类加载器 + instance = new DynamicClassLoader(Thread.currentThread().getContextClassLoader()); + } catch (IOException e) { + // 抛出运行时异常,带有IO异常的信息和根异常 + throw new LoaderRuntimeException(e.getMessage(), e); + } + } + + /** + * 添加URL到类加载器的搜索路径。 + * + * @param url 要添加的URL + */ + @Override + public void addURL(URL url) { + super.addURL(url); + } + + /** + * 将类的字节码添加到类映射中,以供动态加载。 * - * @param fullClassName 类的完全限定名,指明了类在项目中的唯一标识。 - * @param classData 类的数据,以字节数组的形式表示,包含了类的二进制信息。 + * @param fullClassName 类的完全限定名 + * @param classData 类的字节码数据 */ - public void addClass(String fullClassName, byte[] classData) { - // 将类的完全限定名和对应的字节数组存储到classBytes中 - classBytes.put(fullClassName, classData); + public void addClassMap(String fullClassName, byte[] classData) { + classMap.put(fullClassName, defineClass(fullClassName, classData, 0, classData.length)); } /** - * 重写父类方法,用于查找指定的类。 - * 这个方法首先尝试从一个映射(classBytes)中获取指定类名的字节码数据。 - * 如果找到了字节码数据,则使用这些数据定义并返回一个类对象。 - * 如果没有找到相应的字节码数据,则抛出ClassNotFoundException。 + * 根据类的完全限定名查找并加载类。 + * 首先尝试从类映射中加载类,如果未找到,则调用父类加载器的findClass方法。 * - * @param fullClassName 指定的类名,包括包名。 - * @return 查找到的类对象。 - * @throws ClassNotFoundException 如果找不到指定的类,则抛出该异常。 + * @param fullClassName 类的完全限定名 + * @return 加载的Class对象 + * @throws ClassNotFoundException 如果类未找到 */ @Override protected Class findClass(String fullClassName) throws ClassNotFoundException { - // 尝试从classBytes映射中获取指定类的字节码数据 - byte[] classData = classBytes.get(fullClassName); - if (classData == null) { - // 如果未能找到类的字节码,则抛出异常 - throw new ClassNotFoundException("[动态编译]找不到类: " + fullClassName); - } - // 定义并返回一个类对象,使用找到的字节码数据 - return defineClass(fullClassName, classData, 0, classData.length); + // 尝试从类映射中加载类 + Class clazz = classMap.get(fullClassName); + // 如果类在映射中不存在,则记录debug信息 + if (clazz == null) log.debug("[动态编译]classMap未找到类:" + fullClassName); + else return clazz; // 如果类在classMap中找到,则直接返回 + // 如果classMap中未找到类,则调用父ClassLoader的findClass方法尝试加载类 + return super.findClass(fullClassName); } - } + diff --git a/src/main/java/cn/wubo/loader/util/class_loader/JavaBuilder.java b/src/main/java/cn/wubo/loader/util/class_loader/JavaBuilder.java new file mode 100644 index 0000000000000000000000000000000000000000..85d77096303672b1e8596266cd6b1bb8ea94bb0f --- /dev/null +++ b/src/main/java/cn/wubo/loader/util/class_loader/JavaBuilder.java @@ -0,0 +1,80 @@ +package cn.wubo.loader.util.class_loader; + +import cn.wubo.loader.util.exception.LoaderRuntimeException; +import lombok.extern.slf4j.Slf4j; + +import javax.tools.*; +import java.io.Writer; +import java.util.Collections; +import java.util.Map; + +@Slf4j +public class JavaBuilder { + // Java编译器实例 + private JavaCompiler compiler; + // 用于收集编译过程中的诊断信息 + private DiagnosticCollector diagnosticCollector; + // 编译结果输出的writer + private Writer out; + // 内存文件管理器,用于在内存中管理编译后的类文件 + private MemFileManager memFileManager; + // 编译选项 + private Iterable options; + // 需要编译的类名集合 + private Iterable classes; + + /** + * JavaBuilder的构造函数。 + * 初始化Java编译器和相关组件。 + */ + public JavaBuilder() { + this.compiler = ToolProvider.getSystemJavaCompiler(); + this.diagnosticCollector = new DiagnosticCollector<>(); + this.memFileManager = new MemFileManager(compiler.getStandardFileManager(diagnosticCollector, null, null)); + } + + /** + * 静态方法,用于创建JavaBuilder实例。 + * 这是一种常见的Builder模式,用于实例化对象。 + * + * @return JavaBuilder的实例 + */ + public static JavaBuilder builder() { + return new JavaBuilder(); + } + + /** + * 编译给定的Java源代码。 + * + * @param javaSourceCode Java源代码字符串 + * @param fullClassName 完全限定类名 + * @return 当前JavaBuilder实例,支持方法链调用 + */ + public JavaBuilder compiler(String javaSourceCode, String fullClassName) { + log.debug("开始执行编译"); + JavaMemSource file = new JavaMemSource(fullClassName, javaSourceCode); + Iterable compilationUnits = Collections.singletonList(file); + log.debug("获取编译任务"); + JavaCompiler.CompilationTask task = compiler.getTask(out, memFileManager, diagnosticCollector, options, classes, compilationUnits); + log.debug("执行编译"); + boolean result = task.call(); + if (!result) { + String errorMessage = diagnosticCollector.getDiagnostics().stream().map(Object::toString).reduce("", (acc, x) -> acc + "\r\n" + x); + log.debug("编译失败: {}", errorMessage); + throw new LoaderRuntimeException("编译失败: " + errorMessage); + } + log.debug("编译成功"); + return this; + } + + /** + * 获取编译后的类的映射。 + * 这个映射将类名映射到内存中的类对象,可以用于后续的类加载和使用。 + * + * @return 编译后的类的映射 + */ + public Map getJavaMemClassMap() { + return memFileManager.getJavaMemClassMap(); + } +} + diff --git a/src/main/java/cn/wubo/loader/util/class_loader/JavaMemClass.java b/src/main/java/cn/wubo/loader/util/class_loader/JavaMemClass.java index 45042c8d68e0e07fd4f19cc6bd3beff5b19281ed..7eb0cf4fdba82f617d929188d351a9b4c1d36953 100644 --- a/src/main/java/cn/wubo/loader/util/class_loader/JavaMemClass.java +++ b/src/main/java/cn/wubo/loader/util/class_loader/JavaMemClass.java @@ -6,41 +6,44 @@ import java.io.OutputStream; import java.net.URI; /** - * @description: class保存对象(内存:不生成class文件) - * @author: wubo - * @date: 2022-11-21 + * JavaMemClass 是为了在内存中处理Java类文件而设计的。 + * 它继承自SimpleJavaFileObject,用于表示类文件的内容。 */ public class JavaMemClass extends SimpleJavaFileObject { + /** + * 用于存储类文件字节码的 ByteArrayOutputStream。 + */ protected final ByteArrayOutputStream classByteArrayOutputStream = new ByteArrayOutputStream(); + /** + * 构造函数初始化JavaMemClass对象。 + * + * @param name 类的全限定名,使用点(.)分隔。 + * @param kind 类文件的类型,例如SOURCE或CLASS。 + */ public JavaMemClass(String name, Kind kind) { super(URI.create("string:///" + name.replace('.', '/') - + kind.extension), kind); + + kind.extension), kind); } - + /** - * 获取字节数组 + * 获取类文件的字节码。 * - * 该方法不需要接受任何参数,它将返回一个字节数组。 - * 主要用于将内部缓存的字节信息转换为字节数组输出。 - * - * @return byte[] 返回一个包含字节信息的数组 + * @return 类文件的字节码数组。 */ public byte[] getBytes() { - // 将内部存储的字节流转换为字节数组并返回 return classByteArrayOutputStream.toByteArray(); } /** - * 重写openOutputStream方法 - * 这个方法重写了父类中的openOutputStream方法,目的是提供一个特定的输出流返回。 + * 打开一个输出流,用于写入类文件的内容。 * - * @return 返回classByteArrayOutputStream对象 - 这是一个OutputStream的实例, - * 用于允许外部写入数据到类的内部缓存中。 + * @return 一个ByteArrayOutputStream,用于写入类文件的字节码。 */ @Override public OutputStream openOutputStream() { return classByteArrayOutputStream; } } + diff --git a/src/main/java/cn/wubo/loader/util/class_loader/JavaMemSource.java b/src/main/java/cn/wubo/loader/util/class_loader/JavaMemSource.java index 4df5c3500577ccf1b059af0e3d9937c415ba2393..0f4623c8e265fb13528175a3ea28fa4abe902e13 100644 --- a/src/main/java/cn/wubo/loader/util/class_loader/JavaMemSource.java +++ b/src/main/java/cn/wubo/loader/util/class_loader/JavaMemSource.java @@ -4,34 +4,36 @@ import javax.tools.SimpleJavaFileObject; import java.net.URI; /** - * @description:java源码保存对象(内存:不生成class文件) - * @author: wubo - * @date: 2022-11-21 + * JavaMemSource类继承自SimpleJavaFileObject,用于在内存中表示Java源代码。 + * 该类的主要作用是提供一种方式来存储和访问Java源代码字符串,而不需要将源代码写入到实际的文件中。 */ public class JavaMemSource extends SimpleJavaFileObject{ /** - * java源码 + * 存储Java源代码的字符串。 */ private String javaSourceCode; + /** + * 构造函数初始化JavaMemSource对象。 + * + * @param name Java源文件的全限定名,使用'.'作为包名的分隔符。 + * @param javaSourceCode Java源代码的字符串表示。 + */ public JavaMemSource(String name, String javaSourceCode) { super(URI.create("string:///" + name.replace('.', '/')+ Kind.SOURCE.extension), Kind.SOURCE); this.javaSourceCode = javaSourceCode; } - + /** - * 此方法用于获取源代码的注释内容。 - * 它会返回源代码文件中的字符内容,包括注释部分。 + * 获取Java源代码的字符序列。 * - * @param ignoreEncodingErrors 指定是否忽略编码错误。如果设置为true, - * 在读取源代码文件时将忽略任何编码错误, - * 否则遇到编码错误将抛出异常。 - * @return 返回源代码的字符序列,包括代码中的所有注释。 + * @param ignoreEncodingErrors 是否忽略编码错误。该参数在此实现中未使用,因为源代码以字符串形式存储。 + * @return 返回Java源代码的字符序列。 */ @Override public CharSequence getCharContent(boolean ignoreEncodingErrors) { - // 直接返回存储的源代码字符序列 return javaSourceCode; } } + diff --git a/src/main/java/cn/wubo/loader/util/class_loader/MemFileManager.java b/src/main/java/cn/wubo/loader/util/class_loader/MemFileManager.java index 61387edfedbdbaaac9dfc1bc9bf8e22ed1b6728c..c051502b3d6f838ff6be6adf252ecdd56846be6b 100644 --- a/src/main/java/cn/wubo/loader/util/class_loader/MemFileManager.java +++ b/src/main/java/cn/wubo/loader/util/class_loader/MemFileManager.java @@ -8,54 +8,52 @@ import java.util.HashMap; import java.util.Map; /** - * @description: 内存文件管理器 - * @author: wubo - * @date: 2022-11-21 + * 在内存中管理编译后Java类的文件管理器。 + * 该类通过继承ForwardingJavaFileManager,实现了一个内存中的Java类管理机制, + * 主要用于在不将类文件写入磁盘的情况下,存储和管理编译后的Java类。 */ public class MemFileManager extends ForwardingJavaFileManager { - private final Map compiledClasses = new HashMap<>(); + /** + * 用于存储编译后的Java类的内存映射。 + * 键为类名,值为JavaMemClass对象,后者包含类的字节码和其他元数据。 + */ + private Map javaMemClassMap = new HashMap<>(); + /** + * 构造函数,初始化MemFileManager。 + * + * @param fileManager 基础文件管理器,通常是一个与文件系统交互的文件管理器。 + */ protected MemFileManager(JavaFileManager fileManager) { super(fileManager); } /** - * 根据给定参数获取用于输出Java代码的JavaFileObject对象。 - * 这个方法在编译过程中被调用,用于生成和返回一个JavaFileObject实例, - * 该实例代表了将要被输出的Java源代码或编译后的字节码文件。 + * 重写getJavaFileForOutput方法,用于在内存中创建并返回一个新的JavaFileObject。 + * 这个方法是编译器在需要写入.class文件时调用的,我们在这里拦截这个调用, + * 并将.class文件的内容存储在JavaMemClass对象中,而不是写入磁盘。 * - * @param location 代码发生的位置。指定生成文件的位置。 - * @param className 类名。指定将要生成的文件所对应的类名。 - * @param kind Java文件对象的类型。指定生成文件的类型(如源代码、字节码等)。 - * @param sibling 与新创建的JavaFileObject具有相同父级文件对象的兄弟文件对象。 - * 可用于指定新文件在文件系统中的相对位置。 - * @return 用于输出Java代码的JavaFileObject对象。返回一个JavaMemClass实例, - * 该实例用于存储将要输出的Java类的字节码。 + * @param location 位置标识,用于指示.class文件应该放置的位置,这里忽略,因为我们在内存中管理类。 + * @param className 类的全限定名。 + * @param kind 类文件的类型,例如SOURCE、CLASS等。 + * @param sibling 如果存在,表示与新类文件相邻的现有文件对象。 + * @return JavaMemClass对象,它在内存中代表了.class文件。 */ @Override public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind, FileObject sibling) { - // 创建一个JavaMemClass实例,用于存储将要编译的类的字节码 JavaMemClass javaMemClass = new JavaMemClass(className, kind); - // 将新创建的JavaMemClass实例与对应的类名存储到compiledClasses中, - // 以便于后续访问和使用 - compiledClasses.put(className, javaMemClass); + javaMemClassMap.put(className, javaMemClass); return javaMemClass; } /** - * 获取所有编译好的类的字节码数据 + * 提供对javaMemClassMap的访问,允许外部获取和检查编译后的类。 * - * @return 所有编译好的类的字节码数据的Map,键为类名,值为类的字节码数据 + * @return 当前内存中所有编译后类的映射。 */ - public Map getAllCompiledClassesData() { - // 创建一个空的Map用于存储类的字节码数据 - Map classDataMap = new HashMap<>(); - // 遍历编译好的所有类,将每个类的字节码数据添加到Map中 - for (Map.Entry entry : compiledClasses.entrySet()) { - // 将每个编译好的类的字节码数据存入Map中 - classDataMap.put(entry.getKey(), entry.getValue().getBytes()); - } - return classDataMap; + public Map getJavaMemClassMap() { + return javaMemClassMap; } } + diff --git a/src/main/java/cn/wubo/loader/util/controller_loader/DynamicController.java b/src/main/java/cn/wubo/loader/util/controller_loader/DynamicController.java deleted file mode 100644 index 9190a4a785fd0e0aaf6e25ffffc65f244f192939..0000000000000000000000000000000000000000 --- a/src/main/java/cn/wubo/loader/util/controller_loader/DynamicController.java +++ /dev/null @@ -1,40 +0,0 @@ -package cn.wubo.loader.util.controller_loader; - -import cn.wubo.loader.util.SpringContextUtils; -import cn.wubo.loader.util.class_loader.DynamicClass; -import lombok.extern.slf4j.Slf4j; - -@Slf4j -public class DynamicController { - - private DynamicClass dynamicClass; - - public DynamicController(DynamicClass dynamicClass) { - this.dynamicClass = dynamicClass; - } - - public static DynamicController init(DynamicClass dynamicClass) { - log.debug("初始化controller fullClassName:{}", dynamicClass.getFullClassName()); - return new DynamicController(dynamicClass); - } - - /** - * 加载动态类并注册到Spring容器中。 - * 此方法首先尝试获取动态类的bean名称,然后检查该bean是否已在Spring容器中注册。 - * 如果已注册,则注销该bean;未注册则将动态类加载为Class对象,并将其注册为Spring容器中的控制器。 - * - * @return 注册的bean名称 - */ - public String load() { - // 获取动态类的bean名称 - String beanName = SpringContextUtils.beanName(dynamicClass.getFullClassName()); - // 检查Spring容器中是否已存在该bean,若存在则注销 - if (Boolean.TRUE.equals(SpringContextUtils.containsBean(beanName))) SpringContextUtils.unregisterController(beanName); - // 加载动态类并获取其Class对象 - Class type = dynamicClass.compiler().load(); - // 将动态类的Class对象注册为Spring控制器 - SpringContextUtils.registerController(beanName, type); - // 返回bean名称 - return beanName; - } -} diff --git a/src/main/java/cn/wubo/loader/util/jar_loader/DynamicJar.java b/src/main/java/cn/wubo/loader/util/jar_loader/DynamicJar.java deleted file mode 100644 index 7593636f050e66d8f5186139e9b66c01ff676438..0000000000000000000000000000000000000000 --- a/src/main/java/cn/wubo/loader/util/jar_loader/DynamicJar.java +++ /dev/null @@ -1,54 +0,0 @@ -package cn.wubo.loader.util.jar_loader; - -import cn.wubo.loader.util.exception.LoaderRuntimeException; -import lombok.extern.slf4j.Slf4j; - -import java.io.File; -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.URLClassLoader; - -/** - * 将外部jar读入内存中 - */ -@Slf4j -public class DynamicJar { - String filePath; - - public DynamicJar(String filePath) { - this.filePath = filePath; - } - - /** - * 初始化一个DynamicJar对象 - * - * @param filePath 文件路径 - * @return DynamicJar对象 - */ - public static DynamicJar init(String filePath) { - log.debug("jar文件路径初始化:{}", filePath); - return new DynamicJar(filePath); - } - - /** - * 加载指定的类名。 - * - * @param fullClassName 待加载的类的完整名称,包括包名。 - * @return 加载的类对象。如果指定的类成功加载,则返回对应的Class对象。 - * @throws LoaderRuntimeException 如果类加载失败,将抛出此运行时异常。 - */ - public Class load(String fullClassName) { - try { - // 将文件路径转换为URL格式,为创建URLClassLoader做准备 - URL url = new File(filePath).toURI().toURL(); - try (URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url})) { - // 使用URLClassLoader加载指定的类 - return urlClassLoader.loadClass(fullClassName); - } // URLClassLoader的资源被自动关闭 - } catch (ClassNotFoundException | IOException e) { - // 当类找不到或发生IO异常时,抛出自定义的加载异常 - throw new LoaderRuntimeException(e.getMessage(), e); - } - } -} diff --git a/src/test/java/LoderUtilsTest.java b/src/test/java/LoderUtilsTest.java new file mode 100644 index 0000000000000000000000000000000000000000..3d21a80b47841d1fe0c1b0bd8cfcf012132599a3 --- /dev/null +++ b/src/test/java/LoderUtilsTest.java @@ -0,0 +1,195 @@ +import cn.wubo.loader.util.LoaderUtils; +import cn.wubo.loader.util.MethodUtils; +import cn.wubo.loader.util.SpringContextUtils; +import groovy.lang.GroovyClassLoader; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; + +import java.io.IOException; + +@SpringBootTest(classes = SpringContextUtils.class) +@AutoConfigureMockMvc +class LoderUtilsTest { + + @Test + 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"); + Assertions.assertEquals(str, "Hello,world!"); + } + + @Test + void testClassDelay() { + Class clazz = LoaderUtils.load("cn.wubo.loader.util.TestClass"); + String str = (String) MethodUtils.invokeClass(clazz, "testMethod", "world"); + Assertions.assertEquals(str, "Hello,world!"); + } + + @Test + void testInnerClass() { + String javaSourceCode = """ + package cn.wubo.loader.util; + + import lombok.AllArgsConstructor; + import lombok.Data; + import lombok.NoArgsConstructor; + + public class TestClass1 { + + public Object testMethod(String name){ + return new User(name); + } + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class User { + private String name; + } + } + """; + LoaderUtils.compiler(javaSourceCode, "cn.wubo.loader.util.TestClass1"); + Class clazz = LoaderUtils.load("cn.wubo.loader.util.TestClass1"); + Object obj = MethodUtils.invokeClass(clazz, "testMethod", "world"); + Assertions.assertEquals(obj.toString(), "TestClass1.User(name=world)"); + } + + @Test + 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"); + Assertions.assertFalse(str.isEmpty()); + } + + @Test + 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"); + Assertions.assertEquals(str, "Hello,world!"); + } + + @Test + void testBeans() { + String javaSourceCode = """ + package cn.wubo.loader.util; + + public class TestClass3 { + + public String testMethod(String name){ + return String.format("Hello,%s!",name); + } + } + """; + + LoaderUtils.compiler(javaSourceCode, "cn.wubo.loader.util.TestClass3"); + Class clazz = LoaderUtils.load("cn.wubo.loader.util.TestClass3"); + String beanName = LoaderUtils.registerSingleton(clazz); + Assertions.assertEquals(beanName, "testClass3"); + + String javaSourceCode2 = """ + package cn.wubo.loader.util; + + import cn.wubo.loader.util.MethodUtils; + + public class TestClass4 { + + public String testMethod(String name) { + return MethodUtils.invokeBean("testClass3", "testMethod", "world"); + } + } + """; + LoaderUtils.compiler(javaSourceCode2, "cn.wubo.loader.util.TestClass4"); + Class clazz2 = LoaderUtils.load("cn.wubo.loader.util.TestClass4"); + String beanName2 = LoaderUtils.registerSingleton(clazz2); + Assertions.assertEquals(beanName2, "testClass4"); + String str2 = MethodUtils.invokeBean(beanName2, "testMethod", "world"); + Assertions.assertEquals(str2, "Hello,world!"); + } + + @Test + void testGroovy() { + String javaSourceCode = """ + package cn.wubo.loader.util; + + public class TestClass5 { + + public String testMethod(String name){ + return String.format("Hello,%s!",name); + } + } + """; + try (GroovyClassLoader groovyClassLoader = new GroovyClassLoader()) { + groovyClassLoader.parseClass(javaSourceCode); + Class clazz = groovyClassLoader.parseClass(javaSourceCode); + String str = (String) MethodUtils.invokeClass(clazz, "testMethod", "world"); + Assertions.assertEquals(str, "Hello,world!"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Test + 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); + } + } + + @Test + 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"); + Assertions.assertEquals(str, "Hello,world!"); + } +} diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml new file mode 100644 index 0000000000000000000000000000000000000000..5e107958a45be527f7bda13e740ad98dc5e19492 --- /dev/null +++ b/src/test/resources/application.yml @@ -0,0 +1,3 @@ +logging: + level: + root: debug