From 5ec09209aa5f035400bb6963c7e728c4d2d5202e Mon Sep 17 00:00:00 2001 From: phanes <5411232+phanes@user.noreply.gitee.com> Date: Sat, 5 Jul 2025 16:18:58 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20Spring=20Boot=20=E5=8A=A8?= =?UTF-8?q?=E6=80=81=E7=BC=96=E8=AF=91=E5=92=8C=20Bean=20=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E6=BC=94=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- demo_springboot_function/pom.xml | 83 ++++ .../main/java/demo/ControllerRegister.java | 60 +++ .../src/main/java/demo/DemoApp.java | 18 + .../src/main/java/demo/DemoController.java | 24 ++ .../src/main/java/demo/DemoService.java | 26 ++ .../main/java/demo/DynamicBeanManager.java | 290 ++++++++++++++ .../src/main/resources/application.yml | 9 + .../main/resources/codefile/HelloLiquor.txt | 45 +++ .../src/test/java/demo/DemoAppTest.java | 365 ++++++++++++++++++ pom.xml | 1 + 10 files changed, 921 insertions(+) create mode 100644 demo_springboot_function/pom.xml create mode 100644 demo_springboot_function/src/main/java/demo/ControllerRegister.java create mode 100644 demo_springboot_function/src/main/java/demo/DemoApp.java create mode 100644 demo_springboot_function/src/main/java/demo/DemoController.java create mode 100644 demo_springboot_function/src/main/java/demo/DemoService.java create mode 100644 demo_springboot_function/src/main/java/demo/DynamicBeanManager.java create mode 100644 demo_springboot_function/src/main/resources/application.yml create mode 100644 demo_springboot_function/src/main/resources/codefile/HelloLiquor.txt create mode 100644 demo_springboot_function/src/test/java/demo/DemoAppTest.java diff --git a/demo_springboot_function/pom.xml b/demo_springboot_function/pom.xml new file mode 100644 index 0000000..b85e087 --- /dev/null +++ b/demo_springboot_function/pom.xml @@ -0,0 +1,83 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-parent + 2.7.18 + + + + org.noear + demo_springboot_function + demo + + ${project.artifactId} + + + UTF-8 + UTF-8 + 1.8 + + + + + org.projectlombok + lombok + true + + + + org.noear + liquor + 1.5.7 + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.junit.platform + junit-platform-launcher + test + + + + org.junit.jupiter + junit-jupiter-engine + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + 2.7.18 + + demo.DemoApp + + + + + repackage + + + + + + + + \ No newline at end of file diff --git a/demo_springboot_function/src/main/java/demo/ControllerRegister.java b/demo_springboot_function/src/main/java/demo/ControllerRegister.java new file mode 100644 index 0000000..7a5aed3 --- /dev/null +++ b/demo_springboot_function/src/main/java/demo/ControllerRegister.java @@ -0,0 +1,60 @@ +package demo; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.stereotype.Component; +import org.springframework.util.FileCopyUtils; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; + +/** + * 动态Controller注册器 + * 使用Spring原生Bean注册机制,完全集成到Spring生命周期中 + * + * @author phanes + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class ControllerRegister implements ApplicationRunner { + + private final DynamicBeanManager dynamicBeanManager; + private final ResourceLoader resourceLoader; + + @Override + public void run(ApplicationArguments args) throws Exception { + this.registerHelloLiquorController(); + } + + /** + * 注册HelloLiquor控制器 + */ + public void registerHelloLiquorController() { + try { + // 读取资源文件 + Resource resource = resourceLoader.getResource("classpath:codefile/HelloLiquor.txt"); + String classCode; + try (InputStreamReader reader = new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8)) { + classCode = FileCopyUtils.copyToString(reader); + } + + // 编译并注册动态Controller + dynamicBeanManager.registerDynamicController("helloLiquorController", "demo.HelloLiquor", classCode); + + log.info("动态Controller注册成功: HelloLiquor"); + } catch (IOException e) { + log.error("读取HelloLiquor.txt文件失败", e); + throw new RuntimeException("动态Controller注册失败", e); + } catch (Exception e) { + log.error("动态Controller注册失败", e); + throw new RuntimeException("动态Controller注册失败", e); + } + } + +} diff --git a/demo_springboot_function/src/main/java/demo/DemoApp.java b/demo_springboot_function/src/main/java/demo/DemoApp.java new file mode 100644 index 0000000..9f0fd23 --- /dev/null +++ b/demo_springboot_function/src/main/java/demo/DemoApp.java @@ -0,0 +1,18 @@ +package demo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Spring Boot 启动类 + * + * @author phanes + */ +@SpringBootApplication +public class DemoApp { + + public static void main(String[] args) { + SpringApplication.run(DemoApp.class, args); + } + +} diff --git a/demo_springboot_function/src/main/java/demo/DemoController.java b/demo_springboot_function/src/main/java/demo/DemoController.java new file mode 100644 index 0000000..3ef2bf5 --- /dev/null +++ b/demo_springboot_function/src/main/java/demo/DemoController.java @@ -0,0 +1,24 @@ +package demo; + +import lombok.RequiredArgsConstructor; +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; + +/** + * @author phanes + */ +@RestController +@RequestMapping("") +@RequiredArgsConstructor +public class DemoController { + + private final DemoService demoService; + + @GetMapping(path = {"", "/hello"}) + public String hello(@RequestParam(value = "name", defaultValue = "World") String name) { + return demoService.sayHello(name); + } + +} \ No newline at end of file diff --git a/demo_springboot_function/src/main/java/demo/DemoService.java b/demo_springboot_function/src/main/java/demo/DemoService.java new file mode 100644 index 0000000..2193b28 --- /dev/null +++ b/demo_springboot_function/src/main/java/demo/DemoService.java @@ -0,0 +1,26 @@ +package demo; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** + * 演示服务类 + * 用于测试动态编译类的构造函数依赖注入功能 + * + * @author phanes + */ +@Slf4j +@Service +public class DemoService { + + public String sayHello(String name) { + String message = String.format("Hello %s!", name); + log.info("DemoService.sayHello 调用: {}", message); + return message; + } + + public String getServiceInfo() { + return "DemoService - 动态Bean依赖注入测试服务"; + } + +} diff --git a/demo_springboot_function/src/main/java/demo/DynamicBeanManager.java b/demo_springboot_function/src/main/java/demo/DynamicBeanManager.java new file mode 100644 index 0000000..263b1b5 --- /dev/null +++ b/demo_springboot_function/src/main/java/demo/DynamicBeanManager.java @@ -0,0 +1,290 @@ +package demo; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.noear.liquor.DynamicCompiler; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.mvc.method.RequestMappingInfo; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + +import java.lang.reflect.Method; +import java.time.LocalDateTime; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 动态Bean管理器 + * 提供动态编译类的Spring Bean注册、生命周期管理和RequestMapping自动刷新功能 + * + * @author phanes + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class DynamicBeanManager { + + private final ConfigurableApplicationContext applicationContext; + private final RequestMappingHandlerMapping requestMappingHandlerMapping; + + /** + * 已注册的动态Bean缓存 + */ + @Getter + private final Map registeredBeans = new ConcurrentHashMap<>(); + + /** + * 注册动态编译的Controller类到Spring容器 + * + * @param beanName Bean名称 + * @param className 类名 + * @param sourceCode 源代码 + * @throws Exception 注册过程中的异常 + */ + public void registerDynamicController(String beanName, String className, String sourceCode) throws Exception { + try { + log.info("开始注册动态Controller: {} -> {}", beanName, className); + + // 检查是否已存在,如果存在则先注销 + if (registeredBeans.containsKey(beanName)) { + unregisterDynamicController(beanName); + } + + // 编译动态类 + Class clazz = compileDynamicClass(className, sourceCode); + + // 注册为Spring Bean + registerSpringBean(beanName, clazz); + + // 刷新RequestMapping映射 + refreshRequestMappings(beanName); + + // 记录注册信息 + DynamicBeanInfo beanInfo = new DynamicBeanInfo(beanName, className, clazz); + registeredBeans.put(beanName, beanInfo); + + log.info("动态Controller注册成功: {}", beanName); + + } catch (Exception e) { + log.error("注册动态Controller失败: {} -> {}", beanName, className, e); + // 清理可能的部分注册状态 + cleanupFailedRegistration(beanName); + throw e; + } + } + + /** + * 注销动态Controller + * + * @param beanName Bean名称 + */ + public void unregisterDynamicController(String beanName) { + DynamicBeanInfo beanInfo = registeredBeans.remove(beanName); + if (beanInfo != null) { + log.info("开始注销动态Controller: {} (类名: {})", beanName, beanInfo.getClassName()); + try { + // 注销RequestMapping映射 + unregisterRequestMappings(beanName); + + // 使用DefaultListableBeanFactory进行完整的Bean清理 + DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory(); + + // 完整清理Bean实例和定义 + if (beanFactory.containsBeanDefinition(beanName)) { + // 如果Bean实例已创建,先销毁实例 + if (beanFactory.containsSingleton(beanName)) { + beanFactory.destroySingleton(beanName); + log.info("Bean实例 {} 已被销毁", beanName); + } + + // 移除Bean定义 + beanFactory.removeBeanDefinition(beanName); + log.info("Bean定义 {} 已从Spring容器中移除", beanName); + } else { + log.warn("Bean定义 {} 不存在,跳过移除", beanName); + } + + // 验证清理状态 + validateCleanupStatus(beanName, beanFactory); + + log.info("动态Controller注销成功: {}", beanName); + } catch (ClassCastException e) { + log.error("ApplicationContext类型不支持动态Bean管理: {}", beanName, e); + } catch (IllegalStateException e) { + log.error("Spring容器状态异常,无法注销Bean: {}", beanName, e); + } catch (Exception e) { + log.error("注销动态Controller失败: {}", beanName, e); + } + } else { + log.warn("尝试注销不存在的动态Controller: {}", beanName); + } + } + + /** + * 编译动态类 + */ + private Class compileDynamicClass(String className, String sourceCode) throws Exception { + DynamicCompiler compiler = new DynamicCompiler(); + compiler.addSource(className, sourceCode).build(); + return compiler.getClassLoader().loadClass(className); + } + + /** + * 使用BeanDefinition方式注册Bean(支持构造函数注入) + */ + private void registerSpringBean(String beanName, Class clazz) { + if (!(applicationContext instanceof GenericApplicationContext)) { + throw new IllegalStateException("ApplicationContext必须是GenericApplicationContext类型才能动态注册Bean"); + } + + GenericApplicationContext context = (GenericApplicationContext) applicationContext; + + // 创建BeanDefinition + GenericBeanDefinition beanDefinition = new GenericBeanDefinition(); + beanDefinition.setBeanClass(clazz); + beanDefinition.setScope(BeanDefinition.SCOPE_SINGLETON); + beanDefinition.setRole(BeanDefinition.ROLE_APPLICATION); + beanDefinition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR); + + // 注册BeanDefinition + context.registerBeanDefinition(beanName, beanDefinition); + + log.debug("Spring Bean注册成功: {} -> {}", beanName, clazz.getName()); + } + + /** + * 刷新RequestMapping映射 + */ + private void refreshRequestMappings(String beanName) { + try { + Object bean = applicationContext.getBean(beanName); + Class beanClass = bean.getClass(); + + // 遍历所有方法,为标注了@RequestMapping的方法创建映射 + Method[] methods = beanClass.getDeclaredMethods(); + int registeredCount = 0; + + for (Method method : methods) { + RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class); + if (requestMapping != null) { + // 创建RequestMappingInfo + RequestMappingInfo mappingInfo = createRequestMappingInfo(requestMapping); + + // 注册映射 + requestMappingHandlerMapping.registerMapping(mappingInfo, bean, method); + + registeredCount++; + log.debug("注册RequestMapping: {} -> {}", mappingInfo.getPathPatternsCondition(), method.getName()); + } + } + + log.info("RequestMapping映射刷新成功: {} ({}个方法)", beanName, registeredCount); + } catch (Exception e) { + log.error("刷新RequestMapping映射失败: {}", beanName, e); + } + } + + /** + * 创建RequestMappingInfo + */ + private RequestMappingInfo createRequestMappingInfo(RequestMapping requestMapping) { + // 获取配置信息 + RequestMappingInfo.BuilderConfiguration config = requestMappingHandlerMapping.getBuilderConfiguration(); + + // 创建RequestMappingInfo + RequestMappingInfo.Builder builder = RequestMappingInfo.paths(requestMapping.value()) + .methods(requestMapping.method()) + .params(requestMapping.params()) + .headers(requestMapping.headers()) + .consumes(requestMapping.consumes()) + .produces(requestMapping.produces()) + .options(config); + + return builder.build(); + } + + /** + * 注销RequestMapping映射 + */ + private void unregisterRequestMappings(String beanName) { + try { + Object bean = applicationContext.getBean(beanName); + Class beanClass = bean.getClass(); + + // 遍历所有方法,注销RequestMapping映射 + Method[] methods = beanClass.getDeclaredMethods(); + int unregisteredCount = 0; + + for (Method method : methods) { + RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class); + if (requestMapping != null) { + // 创建RequestMappingInfo(用于注销) + RequestMappingInfo mappingInfo = createRequestMappingInfo(requestMapping); + + // 注销映射 + requestMappingHandlerMapping.unregisterMapping(mappingInfo); + + unregisteredCount++; + log.debug("注销RequestMapping: {} -> {}", mappingInfo.getPathPatternsCondition(), method.getName()); + } + } + + log.info("RequestMapping映射注销成功: {} ({}个方法)", beanName, unregisteredCount); + } catch (Exception e) { + log.debug("注销RequestMapping映射时出现异常: {}", beanName, e); + } + } + + /** + * 验证Bean清理状态 + */ + private void validateCleanupStatus(String beanName, DefaultListableBeanFactory beanFactory) { + boolean definitionExists = beanFactory.containsBeanDefinition(beanName); + boolean singletonExists = beanFactory.containsSingleton(beanName); + + if (definitionExists || singletonExists) { + log.warn("Bean清理验证失败: {} (定义存在: {}, 实例存在: {})", + beanName, definitionExists, singletonExists); + } else { + log.debug("Bean清理验证成功: {} 已完全清理", beanName); + } + } + + /** + * 清理失败注册的残留状态 + */ + private void cleanupFailedRegistration(String beanName) { + try { + registeredBeans.remove(beanName); + } catch (Exception e) { + log.debug("清理失败注册状态时出现异常: {}", beanName, e); + } + } + + /** + * 动态Bean信息 + */ + @Getter + public static class DynamicBeanInfo { + private final String beanName; + private final String className; + private final Class clazz; + private final LocalDateTime registrationTime; + + public DynamicBeanInfo(String beanName, String className, Class clazz) { + this.beanName = beanName; + this.className = className; + this.clazz = clazz; + this.registrationTime = LocalDateTime.now(); + } + } + +} \ No newline at end of file diff --git a/demo_springboot_function/src/main/resources/application.yml b/demo_springboot_function/src/main/resources/application.yml new file mode 100644 index 0000000..11e7727 --- /dev/null +++ b/demo_springboot_function/src/main/resources/application.yml @@ -0,0 +1,9 @@ +server: + port: 8080 + servlet: + context-path: / + +logging: + level: + demo: debug + web: debug diff --git a/demo_springboot_function/src/main/resources/codefile/HelloLiquor.txt b/demo_springboot_function/src/main/resources/codefile/HelloLiquor.txt new file mode 100644 index 0000000..294b25c --- /dev/null +++ b/demo_springboot_function/src/main/resources/codefile/HelloLiquor.txt @@ -0,0 +1,45 @@ +package demo; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import java.time.LocalDateTime; + +public class HelloLiquor { + + private final DemoService demoService; + + @Value("${server.port}") + private String serverPort; + + @Value("${server.servlet.context-path:/}") + private String contextPath; + + @Value("${logging.level.demo:info}") + private String logLevel; + + public HelloLiquor(DemoService demoService) { + this.demoService = demoService; + } + + @ResponseBody + @GetMapping(path = {"/liquor", "/liquor/hello"}) + public String hello(@RequestParam(value = "name", defaultValue = "liquor") String name) { + return demoService.sayHello(name); + } + + @ResponseBody + @GetMapping(path = {"/liquor/getDate"}) + public String getDate() { + return LocalDateTime.now().toString(); + } + + @ResponseBody + @GetMapping(path = {"/liquor/config"}) + public String getConfig() { + return String.format("服务配置信息 - 端口: %s, 上下文路径: %s, 日志级别: %s", + serverPort, contextPath, logLevel); + } + +} diff --git a/demo_springboot_function/src/test/java/demo/DemoAppTest.java b/demo_springboot_function/src/test/java/demo/DemoAppTest.java new file mode 100644 index 0000000..ca8bfb4 --- /dev/null +++ b/demo_springboot_function/src/test/java/demo/DemoAppTest.java @@ -0,0 +1,365 @@ +package demo; + +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.context.ApplicationContext; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import java.time.LocalDateTime; +import java.time.format.DateTimeParseException; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Spring Boot API集成测试类 + * 测试静态控制器、动态控制器和动态Bean管理功能 + * + * @author phanes + */ +@Slf4j +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class DemoAppTest { + + @Value("${local.server.port}") + private int port; + + @Autowired + private TestRestTemplate restTemplate; + + @Autowired + private ApplicationContext applicationContext; + + @Autowired + private DynamicBeanManager dynamicBeanManager; + + /** + * 测试应用启动和动态Bean注册 + */ + @Test + @Order(1) + public void testApplicationStartupAndDynamicBeanRegistration() { + log.info("开始测试应用启动和动态Bean注册..."); + + // 验证应用上下文正常启动 + assertThat(applicationContext).isNotNull(); + + // 验证动态Bean管理器正常工作 + assertThat(dynamicBeanManager).isNotNull(); + + // 验证动态Bean已注册 + assertThat(dynamicBeanManager.getRegisteredBeans()).isNotEmpty(); + assertThat(dynamicBeanManager.getRegisteredBeans()).containsKey("helloLiquorController"); + + log.info("应用启动和动态Bean注册测试通过"); + } + + /** + * 测试静态控制器 - 根路径默认参数 + */ + @Test + @Order(2) + public void testIndexControllerRootWithDefaultParam() { + log.info("测试静态控制器根路径默认参数..."); + + ResponseEntity response = restTemplate.getForEntity( + "http://localhost:" + port + "/", String.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isEqualTo("Hello World!"); + + log.info("静态控制器根路径默认参数测试通过"); + } + + /** + * 测试静态控制器 - hello路径默认参数 + */ + @Test + @Order(3) + public void testIndexControllerHelloWithDefaultParam() { + log.info("测试静态控制器hello路径默认参数..."); + + ResponseEntity response = restTemplate.getForEntity( + "http://localhost:" + port + "/hello", String.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isEqualTo("Hello World!"); + + log.info("静态控制器hello路径默认参数测试通过"); + } + + /** + * 测试静态控制器 - 自定义参数 + */ + @Test + @Order(4) + public void testIndexControllerWithCustomParam() { + log.info("测试静态控制器自定义参数..."); + + ResponseEntity response = restTemplate.getForEntity( + "http://localhost:" + port + "/?name=SpringBoot", String.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isEqualTo("Hello SpringBoot!"); + + // 测试hello路径的自定义参数 + response = restTemplate.getForEntity( + "http://localhost:" + port + "/hello?name=Test", String.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isEqualTo("Hello Test!"); + + log.info("静态控制器自定义参数测试通过"); + } + + /** + * 测试动态控制器 - liquor路径默认参数 + */ + @Test + @Order(5) + public void testDynamicControllerLiquorWithDefaultParam() { + log.info("测试动态控制器liquor路径默认参数..."); + + ResponseEntity response = restTemplate.getForEntity( + "http://localhost:" + port + "/liquor", String.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isEqualTo("Hello liquor!"); + + log.info("动态控制器liquor路径默认参数测试通过"); + } + + /** + * 测试动态控制器 - liquor/hello路径默认参数 + */ + @Test + @Order(6) + public void testDynamicControllerLiquorHelloWithDefaultParam() { + log.info("测试动态控制器liquor/hello路径默认参数..."); + + ResponseEntity response = restTemplate.getForEntity( + "http://localhost:" + port + "/liquor/hello", String.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isEqualTo("Hello liquor!"); + + log.info("动态控制器liquor/hello路径默认参数测试通过"); + } + + /** + * 测试动态控制器 - 自定义参数 + */ + @Test + @Order(7) + public void testDynamicControllerWithCustomParam() { + log.info("测试动态控制器自定义参数..."); + + ResponseEntity response = restTemplate.getForEntity( + "http://localhost:" + port + "/liquor?name=DynamicTest", String.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isEqualTo("Hello DynamicTest!"); + + // 测试liquor/hello路径的自定义参数 + response = restTemplate.getForEntity( + "http://localhost:" + port + "/liquor/hello?name=Compiler", String.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isEqualTo("Hello Compiler!"); + + log.info("动态控制器自定义参数测试通过"); + } + + /** + * 测试动态控制器 - 时间获取接口 + */ + @Test + @Order(8) + public void testDynamicControllerGetDate() { + log.info("测试动态控制器时间获取接口..."); + + ResponseEntity response = restTemplate.getForEntity( + "http://localhost:" + port + "/liquor/getDate", String.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody()).isNotEmpty(); + + // 验证返回的时间格式是否为有效的LocalDateTime字符串 + try { + LocalDateTime.parse(response.getBody()); + log.info("时间格式验证成功: {}", response.getBody()); + } catch (DateTimeParseException e) { + throw new AssertionError("返回的时间格式无效: " + response.getBody(), e); + } + + log.info("动态控制器时间获取接口测试通过"); + } + + /** + * 测试动态控制器 - 配置信息获取接口 + */ + @Test + @Order(9) + public void testDynamicControllerGetConfig() { + log.info("测试动态控制器配置信息获取接口..."); + + ResponseEntity response = restTemplate.getForEntity( + "http://localhost:" + port + "/liquor/config", String.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody()).isNotEmpty(); + + // 验证返回内容包含配置信息 + assertThat(response.getBody()).contains("服务配置信息"); + assertThat(response.getBody()).contains("端口: 0"); + assertThat(response.getBody()).contains("上下文路径: /"); + assertThat(response.getBody()).contains("日志级别: debug"); + + log.info("动态控制器配置信息获取接口测试通过: {}", response.getBody()); + } + + /** + * 测试动态Bean管理功能 + */ + @Test + @Order(10) + public void testDynamicBeanManagement() { + log.info("测试动态Bean管理功能..."); + + // 验证动态Bean注册信息 + Map registeredBeans = dynamicBeanManager.getRegisteredBeans(); + assertThat(registeredBeans).hasSize(1); + + DynamicBeanManager.DynamicBeanInfo beanInfo = registeredBeans.get("helloLiquorController"); + assertThat(beanInfo).isNotNull(); + assertThat(beanInfo.getBeanName()).isEqualTo("helloLiquorController"); + assertThat(beanInfo.getClassName()).isEqualTo("demo.HelloLiquor"); + assertThat(beanInfo.getClazz()).isNotNull(); + assertThat(beanInfo.getRegistrationTime()).isNotNull(); + + // 验证动态Bean在Spring容器中存在 + assertThat(applicationContext.containsBean("helloLiquorController")).isTrue(); + + // 验证动态编译的类可以被加载 + Object dynamicBean = applicationContext.getBean("helloLiquorController"); + assertThat(dynamicBean).isNotNull(); + assertThat(dynamicBean.getClass().getName()).isEqualTo("demo.HelloLiquor"); + + log.info("动态Bean管理功能测试通过 - Bean: {}, 类: {}, 注册时间: {}", + beanInfo.getBeanName(), + beanInfo.getClassName(), + beanInfo.getRegistrationTime()); + } + + /** + * 测试动态控制器重新注册功能 + */ + @Test + @Order(11) + public void testDynamicControllerReRegistration() throws Exception { + log.info("测试动态控制器重新注册功能..."); + + dynamicBeanManager.unregisterDynamicController("helloLiquorController"); + + // 读取修改后的HelloLiquor.txt内容 + String updatedClassCode = " package demo;\n" + + "\n" + + " import org.springframework.beans.factory.annotation.Autowired;\n" + + " import org.springframework.web.bind.annotation.GetMapping;\n" + + " import org.springframework.web.bind.annotation.RequestParam;\n" + + " import org.springframework.web.bind.annotation.ResponseBody;\n" + + "\n" + + " public class HelloLiquor {\n" + + "\n" + + " @Autowired\n" + + " private DemoService demoService;\n" + + "\n" + + " @ResponseBody\n" + + " @GetMapping(\"/liquor/profile\")\n" + + " public String getUserProfile(\n" + + " @RequestParam(value = \"name\", defaultValue = \"访客\") String name,\n" + + " @RequestParam(value = \"age\", defaultValue = \"18\") Integer age\n" + + " ) {\n" + + " String greeting = demoService.sayHello(name);\n" + + " return String.format(\"%s 年龄: %d岁, 服务信息: %s\",\n" + + " greeting, age, demoService.getServiceInfo());\n" + + " }\n" + + "\n" + + " }"; + + // 重新注册动态控制器 + dynamicBeanManager.registerDynamicController("helloLiquorController", "demo.HelloLiquor", updatedClassCode); + + // 验证重新注册成功 + Map registeredBeans = dynamicBeanManager.getRegisteredBeans(); + assertThat(registeredBeans).hasSize(1); + assertThat(registeredBeans).containsKey("helloLiquorController"); + + // 验证新的Bean可以正常工作 + Object dynamicBean = applicationContext.getBean("helloLiquorController"); + assertThat(dynamicBean).isNotNull(); + assertThat(dynamicBean.getClass().getName()).isEqualTo("demo.HelloLiquor"); + + log.info("动态控制器重新注册功能测试通过"); + } + + /** + * 测试新增的profile接口 - 默认参数 + */ + @Test + @Order(12) + public void testNewProfileInterfaceWithDefaultParams() { + log.info("测试新增的profile接口默认参数..."); + + ResponseEntity response = restTemplate.getForEntity( + "http://localhost:" + port + "/liquor/profile", String.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody()).contains("Hello 访客!"); + assertThat(response.getBody()).contains("年龄: 18岁"); + assertThat(response.getBody()).contains("服务信息: DemoService - 动态Bean依赖注入测试服务"); + + log.info("新增profile接口默认参数测试通过: {}", response.getBody()); + } + + /** + * 测试新增的profile接口 - 自定义参数 + */ + @Test + @Order(13) + public void testNewProfileInterfaceWithCustomParams() { + log.info("测试新增的profile接口自定义参数..."); + + ResponseEntity response = restTemplate.getForEntity( + "http://localhost:" + port + "/liquor/profile?name=张三&age=25", String.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody()).contains("Hello 张三!"); + assertThat(response.getBody()).contains("年龄: 25岁"); + assertThat(response.getBody()).contains("服务信息: DemoService - 动态Bean依赖注入测试服务"); + + // 测试部分参数自定义 + response = restTemplate.getForEntity( + "http://localhost:" + port + "/liquor/profile?name=李四", String.class); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).contains("Hello 李四!"); + assertThat(response.getBody()).contains("年龄: 18岁"); // 使用默认年龄 + + log.info("新增profile接口自定义参数测试通过"); + } + +} diff --git a/pom.xml b/pom.xml index 58c7d08..93389ac 100755 --- a/pom.xml +++ b/pom.xml @@ -22,6 +22,7 @@ liquor_benchmark demo_solon_jarinjar demo_springboot_jarinjar + demo_springboot_function demo_dynamic_compiling_and_debugging_solon -- Gitee