This is an automated email from the ASF dual-hosted git repository. jianbin pushed a commit to branch 2.x in repository https://gitbox.apache.org/repos/asf/incubator-seata.git
The following commit(s) were added to refs/heads/2.x by this push: new 07f56b52a3 optimize: add support for parsing @RequestParam annotation in netty-http-server (#7430) 07f56b52a3 is described below commit 07f56b52a327cab72f020a5e773aab2a8eb110bc Author: xiaoyu <93440108+yvce...@users.noreply.github.com> AuthorDate: Sat Jun 14 16:00:42 2025 +0800 optimize: add support for parsing @RequestParam annotation in netty-http-server (#7430) --- changes/en-us/2.x.md | 3 + changes/zh-cn/2.x.md | 4 +- .../core/rpc/netty/http/HttpDispatchHandler.java | 3 + .../seata/core/rpc/netty/http/ParamMetaData.java | 30 ++++ .../seata/core/rpc/netty/http/ParameterParser.java | 33 +++- .../rpc/netty/http/HttpDispatchHandlerTest.java | 1 + .../core/rpc/netty/http/ParameterParserTest.java | 152 +++++++++++++++++- .../http/RestControllerBeanPostProcessor.java | 114 ++++++++++++-- .../http/RestControllerBeanPostProcessorTest.java | 172 +++++++++++++++++++-- .../seata/server/controller/ClusterController.java | 6 +- 10 files changed, 487 insertions(+), 31 deletions(-) diff --git a/changes/en-us/2.x.md b/changes/en-us/2.x.md index d2749f0ab3..f32621d09f 100644 --- a/changes/en-us/2.x.md +++ b/changes/en-us/2.x.md @@ -56,6 +56,7 @@ Add changes here for all PR submitted to the 2.x branch. - [[#7418](https://github.com/apache/incubator-seata/pull/7418)] add jackson notice - [[#7419](https://github.com/apache/incubator-seata/pull/7419)] Add maven profile to support packaging source code - [[#7428](https://github.com/apache/incubator-seata/pull/7428)] pmd-check log as ERROR level +- [[#7430](https://github.com/apache/incubator-seata/pull/7430)] Add support for parsing @RequestParam annotation in netty-http-server - [[#7432](https://github.com/apache/incubator-seata/pull/7432)] conditionally include test modules using Maven profiles @@ -120,6 +121,8 @@ Thanks to these contributors for their code commits. Please report an unintended - [PengningYang](https://github.com/PengningYang) - [WangzJi](https://github.com/WangzJi) - [maple525866](https://github.com/maple525866) +- [YvCeung](https://github.com/YvCeung) + Also, we receive many valuable issues, questions and advices from our community. Thanks for you all. diff --git a/changes/zh-cn/2.x.md b/changes/zh-cn/2.x.md index 599cfcf1de..0e5350ad4d 100644 --- a/changes/zh-cn/2.x.md +++ b/changes/zh-cn/2.x.md @@ -56,6 +56,7 @@ - [[#7418](https://github.com/apache/incubator-seata/pull/7418)] 添加 jackson notice - [[#7419](https://github.com/apache/incubator-seata/pull/7419)] 添加 Maven 配置文件以支持源码打包 - [[#7428](https://github.com/apache/incubator-seata/pull/7428)] 修改 pmd-check 输出日志为 ERROR 级别 +- [[#7430](https://github.com/apache/incubator-seata/pull/7430)] 在netty-http-server中增加了对解析@RequestParam注释的支持 ### security: @@ -122,8 +123,7 @@ - [PengningYang](https://github.com/PengningYang) - [WangzJi](https://github.com/WangzJi) - [maple525866](https://github.com/maple525866) - - +- [YvCeung](https://github.com/YvCeung) 同时,我们收到了社区反馈的很多有价值的issue和建议,非常感谢大家。 diff --git a/core/src/main/java/org/apache/seata/core/rpc/netty/http/HttpDispatchHandler.java b/core/src/main/java/org/apache/seata/core/rpc/netty/http/HttpDispatchHandler.java index 40dd67d2a5..782c7ebf6a 100644 --- a/core/src/main/java/org/apache/seata/core/rpc/netty/http/HttpDispatchHandler.java +++ b/core/src/main/java/org/apache/seata/core/rpc/netty/http/HttpDispatchHandler.java @@ -48,6 +48,9 @@ import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; +/** + * A Netty HTTP request handler that dispatches incoming requests to corresponding controller methods + */ public class HttpDispatchHandler extends SimpleChannelInboundHandler<HttpRequest> { private static final Logger LOGGER = LoggerFactory.getLogger(HttpDispatchHandler.class); diff --git a/core/src/main/java/org/apache/seata/core/rpc/netty/http/ParamMetaData.java b/core/src/main/java/org/apache/seata/core/rpc/netty/http/ParamMetaData.java index 4828e38cbf..41625ec3ff 100644 --- a/core/src/main/java/org/apache/seata/core/rpc/netty/http/ParamMetaData.java +++ b/core/src/main/java/org/apache/seata/core/rpc/netty/http/ParamMetaData.java @@ -20,6 +20,12 @@ public class ParamMetaData { private ParamConvertType paramConvertType; + private String paramName; + + private boolean required; + + private String defaultValue; + public ParamConvertType getParamConvertType() { return paramConvertType; } @@ -28,6 +34,30 @@ public class ParamMetaData { this.paramConvertType = paramConvertType; } + public String getParamName() { + return paramName; + } + + public void setParamName(String paramName) { + this.paramName = paramName; + } + + public boolean isRequired() { + return required; + } + + public void setRequired(boolean required) { + this.required = required; + } + + public String getDefaultValue() { + return defaultValue; + } + + public void setDefaultValue(String defaultValue) { + this.defaultValue = defaultValue; + } + public enum ParamConvertType { /** diff --git a/core/src/main/java/org/apache/seata/core/rpc/netty/http/ParameterParser.java b/core/src/main/java/org/apache/seata/core/rpc/netty/http/ParameterParser.java index 149681a4ec..5ec665ea80 100644 --- a/core/src/main/java/org/apache/seata/core/rpc/netty/http/ParameterParser.java +++ b/core/src/main/java/org/apache/seata/core/rpc/netty/http/ParameterParser.java @@ -26,12 +26,18 @@ import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.util.List; import java.util.Map; +import java.util.Optional; + import org.apache.seata.common.rpc.http.HttpContext; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import static com.fasterxml.jackson.databind.SerializationFeature.FAIL_ON_EMPTY_BEANS; +/** + * A utility class for parsing HTTP request parameters and converting them into Java objects. + * Supports various parameter types including request params, request body, model attributes, etc. + */ public class ParameterParser { private static final Logger LOGGER = LoggerFactory.getLogger(ParameterParser.class); @@ -39,6 +45,9 @@ public class ParameterParser { private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper() .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).configure(FAIL_ON_EMPTY_BEANS, false); + private static final String DEFAULT_NONE = "\n\t\t\n\t\t\n\ue000\ue001\ue002\n\t\t\t\t\n"; + + public static ObjectNode convertParamMap(Map<String, List<String>> paramMap) { ObjectNode paramNode = OBJECT_MAPPER.createObjectNode(); for (Map.Entry<String, List<String>> entry : paramMap.entrySet()) { @@ -89,7 +98,7 @@ public class ParameterParser { private static Object getArgValue(Class<?> parameterType, String parameterName, ParamMetaData paramMetaData, ObjectNode paramMap, HttpContext httpContext) { ParamMetaData.ParamConvertType paramConvertType = paramMetaData.getParamConvertType(); - if (parameterType.equals(HttpContext.class)) { + if (HttpContext.class.equals(parameterType)) { return httpContext; } else if (ParamMetaData.ParamConvertType.MODEL_ATTRIBUTE.equals(paramConvertType)) { JsonNode param = paramMap.get("param"); @@ -97,6 +106,28 @@ public class ParameterParser { } else if (ParamMetaData.ParamConvertType.REQUEST_BODY.equals(paramConvertType)) { JsonNode body = paramMap.get("body"); return OBJECT_MAPPER.convertValue(body, parameterType); + } else if (ParamMetaData.ParamConvertType.REQUEST_PARAM.equals(paramConvertType)) { + String paramName = paramMetaData.getParamName(); + JsonNode jsonNode = Optional.ofNullable(paramMap.get("param")) + .map(body -> body.get(paramName)) + .orElse(null); + + // Step 1: If body exists and contains paramName, use its value first + if (jsonNode != null && !jsonNode.isNull()) { + return OBJECT_MAPPER.convertValue(jsonNode, parameterType); + } + + // Step 2: If the parameter is missing but a defaultValue is set, use the defaultValue + String defaultValue = paramMetaData.getDefaultValue(); + if (defaultValue != null && !defaultValue.equals(DEFAULT_NONE)) { + return OBJECT_MAPPER.convertValue(defaultValue, parameterType); + } + + // Step 3: If the parameter is required but no value or defaultValue is provided, throw an exception + if (paramMetaData.isRequired()) { + throw new IllegalArgumentException("Required request parameter '" + paramName + "' is missing"); + } + return null; } else { JsonNode paramNode = paramMap.get("param"); if (paramNode != null) { diff --git a/core/src/test/java/org/apache/seata/core/rpc/netty/http/HttpDispatchHandlerTest.java b/core/src/test/java/org/apache/seata/core/rpc/netty/http/HttpDispatchHandlerTest.java index 4b55cc2af7..9763f9c18b 100644 --- a/core/src/test/java/org/apache/seata/core/rpc/netty/http/HttpDispatchHandlerTest.java +++ b/core/src/test/java/org/apache/seata/core/rpc/netty/http/HttpDispatchHandlerTest.java @@ -57,6 +57,7 @@ class HttpDispatchHandlerTest { Method method = TestController.class.getMethod("handleRequest", String.class); ParamMetaData paramMetaData = new ParamMetaData(); paramMetaData.setParamConvertType(ParamMetaData.ParamConvertType.REQUEST_PARAM); + paramMetaData.setParamName("param"); ParamMetaData[] paramMetaDatas = new ParamMetaData[]{paramMetaData}; HttpInvocation invocation = new HttpInvocation(); diff --git a/core/src/test/java/org/apache/seata/core/rpc/netty/http/ParameterParserTest.java b/core/src/test/java/org/apache/seata/core/rpc/netty/http/ParameterParserTest.java index f9bfcf071c..1e4ee9b0e4 100644 --- a/core/src/test/java/org/apache/seata/core/rpc/netty/http/ParameterParserTest.java +++ b/core/src/test/java/org/apache/seata/core/rpc/netty/http/ParameterParserTest.java @@ -30,11 +30,16 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; class ParameterParserTest { private final ObjectMapper objectMapper = new ObjectMapper(); + private static final String DEFAULT_NONE = "\n\t\t\n\t\t\n\ue000\ue001\ue002\n\t\t\t\t\n"; @Test void testConvertParamMapWithSingleValue() throws JsonProcessingException { @@ -95,9 +100,152 @@ class ParameterParserTest { assertNotNull(args[0]); } - // 测试辅助类 + @Test + void testGetArgValuesWithRequestParam() throws Exception { + Method method = TestClassA.class.getMethod("objectMethod", String.class); + + ParamMetaData paramMetaData = new ParamMetaData(); + paramMetaData.setParamConvertType(ParamMetaData.ParamConvertType.REQUEST_PARAM); + paramMetaData.setParamName("userName"); + paramMetaData.setDefaultValue("a"); + paramMetaData.setRequired(false); + + ObjectNode paramMap = objectMapper.createObjectNode(); + ObjectNode bodyNode = paramMap.putObject("param"); + bodyNode.put("userName", "LiHua"); + HttpContext httpContext = new HttpContext(null,null,false); + Object[] args = ParameterParser.getArgValues( + new ParamMetaData[]{paramMetaData}, + method, + paramMap, httpContext + ); + + assertEquals(1, args.length); + assertNotNull(args[0]); + assertEquals("LiHua", args[0]); + } + + @Test + void testGetArgValuesWithRequestParamAndDefaultValue() throws Exception { + Method method = TestClassA.class.getMethod("objectMethod", String.class); + + ParamMetaData paramMetaData = new ParamMetaData(); + paramMetaData.setParamConvertType(ParamMetaData.ParamConvertType.REQUEST_PARAM); + paramMetaData.setParamName("userName"); + paramMetaData.setDefaultValue("XiaMing"); + paramMetaData.setRequired(false); + + ObjectNode paramMap = objectMapper.createObjectNode(); + HttpContext httpContext = new HttpContext(null,null,false); + Object[] args = ParameterParser.getArgValues( + new ParamMetaData[]{paramMetaData}, + method, + paramMap, httpContext + ); + + assertEquals(1, args.length); + assertNotNull(args[0]); + assertEquals("XiaMing", args[0]); + } + + @Test + void testGetArgValuesWithRequestParamThrowException() throws Exception { + Method method = TestClassA.class.getMethod("objectMethod", String.class); + + ParamMetaData paramMetaData = new ParamMetaData(); + paramMetaData.setParamConvertType(ParamMetaData.ParamConvertType.REQUEST_PARAM); + paramMetaData.setParamName("userName"); + paramMetaData.setDefaultValue(DEFAULT_NONE); + paramMetaData.setRequired(true); + assertThrows(IllegalArgumentException.class, () ->{ + ObjectNode paramMap = objectMapper.createObjectNode(); + HttpContext httpContext = new HttpContext(null,null,false); + ParameterParser.getArgValues( + new ParamMetaData[]{paramMetaData}, + method, + paramMap, httpContext + ); + }); + } + + @Test + void testGetArgValuesWithRequestParamAndReturnNull() throws Exception { + Method method = TestClassA.class.getMethod("objectMethod", String.class); + + ParamMetaData paramMetaData = new ParamMetaData(); + paramMetaData.setParamConvertType(ParamMetaData.ParamConvertType.REQUEST_PARAM); + paramMetaData.setParamName("userName"); + paramMetaData.setRequired(false); + + ObjectNode paramMap = objectMapper.createObjectNode(); + HttpContext httpContext = new HttpContext(null,null,false); + Object[] args = ParameterParser.getArgValues( + new ParamMetaData[]{paramMetaData}, + method, + paramMap, httpContext + ); + + assertEquals(1, args.length); + assertNull(args[0]); + } + + @Test + void testGetArgValuesWithJavaBeanParam() throws Exception { + Method method = TestClassB.class.getMethod("objectMethod", User.class); + + ParamMetaData paramMetaData = new ParamMetaData(); + paramMetaData.setParamConvertType(ParamMetaData.ParamConvertType.MODEL_ATTRIBUTE); + ObjectNode paramMap = objectMapper.createObjectNode(); + ObjectNode bodyNode = paramMap.putObject("param"); + bodyNode.put("name", "LiHua"); + bodyNode.put("age", 10); + HttpContext httpContext = new HttpContext(null, null, false); + Object[] args = ParameterParser.getArgValues( + new ParamMetaData[]{paramMetaData}, + method, + paramMap, httpContext + ); + + assertEquals(1, args.length); + assertTrue(args[0] instanceof User); + assertEquals("LiHua", ((User) args[0]).name); + assertEquals(10, ((User) args[0]).age); + } + + + // Test support class class TestClass { public void objectMethod(Object obj) { } } + + // Test support classA + class TestClassA{ + public void objectMethod(String userName){ + + } + } + + // Test support classB + class TestClassB{ + public void objectMethod(User user){ + + } + } + + static class User{ + String name; + Integer age; + + public User(){ + } + + public void setName(String name){ + this.name = name; + } + + public void setAge(Integer age){ + this.age = age; + } + } } \ No newline at end of file diff --git a/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/main/java/org/apache/seata/spring/boot/autoconfigure/http/RestControllerBeanPostProcessor.java b/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/main/java/org/apache/seata/spring/boot/autoconfigure/http/RestControllerBeanPostProcessor.java index ec96beff70..64fad3e381 100644 --- a/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/main/java/org/apache/seata/spring/boot/autoconfigure/http/RestControllerBeanPostProcessor.java +++ b/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/main/java/org/apache/seata/spring/boot/autoconfigure/http/RestControllerBeanPostProcessor.java @@ -34,11 +34,17 @@ import org.springframework.web.bind.annotation.RestController; import java.lang.annotation.Annotation; import java.lang.reflect.Method; +import java.lang.reflect.Parameter; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static org.springframework.web.bind.annotation.ValueConstants.DEFAULT_NONE; /** * Handles classes annotated with @RestController to establish a request path -> controller mapping relationship @@ -50,6 +56,8 @@ public class RestControllerBeanPostProcessor implements BeanPostProcessor { private static final List<Class<? extends Annotation>> MAPPING_CLASS = new ArrayList<>(); private static final Map<Class<? extends Annotation>, ParamMetaData.ParamConvertType> MAPPING_PARAM_TYPE = new HashMap<>(); + private static final Set<Class<?>> SIMPLE_TYPE = new HashSet<>(); + private static final Set<Class<?>> SPECIAL_INJECTED_TYPE = new HashSet<>(); static { MAPPING_CLASS.add(GetMapping.class); @@ -61,6 +69,31 @@ public class RestControllerBeanPostProcessor implements BeanPostProcessor { MAPPING_PARAM_TYPE.put(RequestParam.class, ParamMetaData.ParamConvertType.REQUEST_PARAM); MAPPING_PARAM_TYPE.put(RequestBody.class, ParamMetaData.ParamConvertType.REQUEST_BODY); MAPPING_PARAM_TYPE.put(ModelAttribute.class, ParamMetaData.ParamConvertType.MODEL_ATTRIBUTE); + + SIMPLE_TYPE.add(String.class); + SIMPLE_TYPE.add(Integer.class); + SIMPLE_TYPE.add(int.class); + SIMPLE_TYPE.add(Long.class); + SIMPLE_TYPE.add(long.class); + SIMPLE_TYPE.add(Boolean.class); + SIMPLE_TYPE.add(boolean.class); + SIMPLE_TYPE.add(Double.class); + SIMPLE_TYPE.add(double.class); + SIMPLE_TYPE.add(Float.class); + SIMPLE_TYPE.add(float.class); + SIMPLE_TYPE.add(Short.class); + SIMPLE_TYPE.add(short.class); + SIMPLE_TYPE.add(Byte.class); + SIMPLE_TYPE.add(byte.class); + SIMPLE_TYPE.add(Character.class); + SIMPLE_TYPE.add(char.class); + SIMPLE_TYPE.add(java.math.BigDecimal.class); + SIMPLE_TYPE.add(java.math.BigInteger.class); + SIMPLE_TYPE.add(java.util.Date.class); + SIMPLE_TYPE.add(java.time.LocalDate.class); + SIMPLE_TYPE.add(java.time.LocalDateTime.class); + + SPECIAL_INJECTED_TYPE.add(org.apache.seata.common.rpc.http.HttpContext.class); } @Override @@ -106,19 +139,20 @@ public class RestControllerBeanPostProcessor implements BeanPostProcessor { Class<?>[] parameterTypes = method.getParameterTypes(); Annotation[][] parameterAnnotations = method.getParameterAnnotations(); ParamMetaData[] paramMetaDatas = new ParamMetaData[parameterTypes.length]; + Parameter[] parameters = method.getParameters(); for (int i = 0; i < parameterTypes.length; i++) { + Annotation matchedAnnotation = null; Class<? extends Annotation> parameterAnnotationType = null; if (parameterAnnotations[i] != null && parameterAnnotations[i].length > 0) { - parameterAnnotationType = parameterAnnotations[i][0].annotationType(); - } - - if (parameterAnnotationType == null) { - parameterAnnotationType = RequestParam.class; + for (Annotation annotation : parameterAnnotations[i]) { + if (MAPPING_PARAM_TYPE.containsKey(annotation.annotationType())) { + parameterAnnotationType = annotation.annotationType(); + matchedAnnotation = annotation; + break; + } + } } - - ParamMetaData paramMetaData = new ParamMetaData(); - ParamMetaData.ParamConvertType paramConvertType = MAPPING_PARAM_TYPE.get(parameterAnnotationType); - paramMetaData.setParamConvertType(paramConvertType); + ParamMetaData paramMetaData = buildParamMetaData(matchedAnnotation, parameterTypes[i], parameterAnnotationType, parameters[i]); paramMetaDatas[i] = paramMetaData; } int maxSize = Math.max(prePaths.size(), postPaths.size()); @@ -143,5 +177,67 @@ public class RestControllerBeanPostProcessor implements BeanPostProcessor { } } + private static ParamMetaData buildParamMetaData(Annotation matchedAnnotation, Class<?> parameterType, + Class<? extends Annotation> parameterAnnotationType, Parameter parameter) { + ParamMetaData paramMetaData = new ParamMetaData(); + + // No annotation on the parameter: resolve the default annotation type based on the parameter type + if (parameterAnnotationType == null) { + parameterAnnotationType = resolveDefaultAnnotationType(parameterType); + ParamMetaData.ParamConvertType paramConvertType = MAPPING_PARAM_TYPE.get(parameterAnnotationType); + paramMetaData.setParamConvertType(paramConvertType); + if (parameterAnnotationType == RequestParam.class) { + paramMetaData.setParamName(parameter.getName()); + paramMetaData.setRequired(true); + paramMetaData.setDefaultValue(DEFAULT_NONE); + } + // Annotation is present on the parameter; proceed with standard parsing logic + } else { + ParamMetaData.ParamConvertType paramConvertType = MAPPING_PARAM_TYPE.get(parameterAnnotationType); + paramMetaData.setParamConvertType(paramConvertType); + if (parameterAnnotationType == RequestParam.class) { + RequestParam requestParam = (RequestParam) matchedAnnotation; + boolean required = true; + String defaultValue = null; + String paramName = Optional.ofNullable(requestParam.name()) + .filter(name -> !name.isEmpty()) + .orElseGet(() -> { + String value = requestParam.value(); + return !value.isEmpty() ? value : parameter.getName(); + }); + + required = requestParam.required(); + defaultValue = requestParam.defaultValue(); + + if (!DEFAULT_NONE.equals(defaultValue)) { + required = false; + } + + paramMetaData.setParamName(paramName); + paramMetaData.setRequired(required); + paramMetaData.setDefaultValue(defaultValue); + } + } + + return paramMetaData; + } + + /** + * Determines the default annotation type for a parameter based on its class. + * Returns: + * - null for special injected types (e.g., HttpContext), + * - RequestParam for primitives, simple types, or MultipartFile, + * - ModelAttribute for all others. + */ + private static Class<? extends Annotation> resolveDefaultAnnotationType(Class<?> paramType) { + if (SPECIAL_INJECTED_TYPE.stream().anyMatch(t -> t.isAssignableFrom(paramType))) { + return null; + } else if (paramType.isPrimitive() || SIMPLE_TYPE.contains(paramType) + || org.springframework.web.multipart.MultipartFile.class.isAssignableFrom(paramType)) { + return RequestParam.class; + } else { + return ModelAttribute.class; + } + } } \ No newline at end of file diff --git a/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/test/java/org/apache/seata/spring/boot/autoconfigure/http/RestControllerBeanPostProcessorTest.java b/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/test/java/org/apache/seata/spring/boot/autoconfigure/http/RestControllerBeanPostProcessorTest.java index 33805817a2..6a5c984e75 100644 --- a/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/test/java/org/apache/seata/spring/boot/autoconfigure/http/RestControllerBeanPostProcessorTest.java +++ b/seata-spring-autoconfigure/seata-spring-autoconfigure-core/src/test/java/org/apache/seata/spring/boot/autoconfigure/http/RestControllerBeanPostProcessorTest.java @@ -16,20 +16,34 @@ */ package org.apache.seata.spring.boot.autoconfigure.http; +import org.apache.seata.common.rpc.http.HttpContext; import org.apache.seata.core.rpc.netty.http.ControllerManager; import org.apache.seata.core.rpc.netty.http.HttpInvocation; +import org.apache.seata.core.rpc.netty.http.ParamMetaData; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.Mock; +import org.mockito.MockedStatic; import org.mockito.MockitoAnnotations; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; -import static org.mockito.Mockito.verify; +import javax.annotation.Nonnull; -public class RestControllerBeanPostProcessorTest { +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; - @Mock - private ControllerManager controllerManager; +public class RestControllerBeanPostProcessorTest { private RestControllerBeanPostProcessor processor; @@ -44,13 +58,104 @@ public class RestControllerBeanPostProcessorTest { // Mock the bean and its annotations TestController mockBean = new TestController(); + try (MockedStatic<ControllerManager> mocked = mockStatic(ControllerManager.class)) { + // Call the method under test + processor.postProcessAfterInitialization(mockBean, "testController"); + + // Verify that the paths were added correctly + mocked.verify(() -> ControllerManager.addHttpInvocation(any(HttpInvocation.class)), times(2)); + } + } + + @Test + public void testRegisterHttpInvocationWithCorrectMetadata() { + // Mock the bean and its annotations + TestApiController controller = new TestApiController(); + // Call the method under test - processor.postProcessAfterInitialization(mockBean, "testController"); + processor.postProcessAfterInitialization(controller, "testApiController"); + + // Verify whether the parsed data is correct + HttpInvocation getInvocation = ControllerManager.getHttpInvocation("/api/get"); + assertNotNull(getInvocation, "getMethod should be registered"); + assertEquals("getMethod", getInvocation.getMethod().getName()); + assertSame(controller, getInvocation.getController()); + + // Verify whether the defaultValue attribute "value" of @RequestParam is correct + ParamMetaData[] getParams = getInvocation.getParamMetaData(); + assertEquals(1, getParams.length); + assertEquals("param", getParams[0].getParamName()); + assertEquals("defaultValue", getParams[0].getDefaultValue()); + assertEquals(ParamMetaData.ParamConvertType.REQUEST_PARAM, getParams[0].getParamConvertType()); + assertFalse(getParams[0].isRequired()); + + // Verify whether the default value of the "required" attribute of @RequestParam is correct + HttpInvocation postInvocation = ControllerManager.getHttpInvocation("/api/post"); + assertNotNull(postInvocation, "postMethod should be registered"); + assertEquals("postMethod", postInvocation.getMethod().getName()); + assertSame(controller, postInvocation.getController()); + + // Verify whether the value of the "name" attribute of @RequestParam is correct + ParamMetaData[] postParams = postInvocation.getParamMetaData(); + assertEquals(1, postParams.length); + assertEquals("requestBody", postParams[0].getParamName()); + assertEquals(ParamMetaData.ParamConvertType.REQUEST_PARAM, postParams[0].getParamConvertType()); + + HttpInvocation updateInvocation = ControllerManager.getHttpInvocation("/api/update"); + assertNotNull(updateInvocation, "updateMethod should be registered"); + assertEquals("updateMethod", updateInvocation.getMethod().getName()); + assertSame(controller, updateInvocation.getController()); - // Verify that the paths were added correctly - HttpInvocation httpInvocation = new HttpInvocation(); - httpInvocation.setPath("/path"); - verify(controllerManager).addHttpInvocation(httpInvocation); + ParamMetaData[] updateParams = updateInvocation.getParamMetaData(); + assertEquals(2, updateParams.length); + + // Verify whether the value attribute of @RequestParam is correct + assertEquals("userName", updateParams[0].getParamName()); + assertEquals(ParamMetaData.ParamConvertType.REQUEST_PARAM, updateParams[0].getParamConvertType()); + assertEquals("age", updateParams[1].getParamName()); + + // Verify whether @RequestParam can be correctly parsed when there are multiple annotations before a parameter + assertEquals(ParamMetaData.ParamConvertType.REQUEST_PARAM, updateParams[1].getParamConvertType()); + assertFalse(updateParams[1].isRequired()); + } + + @Test + public void testRegisterHttpInvocationWithNoAnnotation() { + // Mock the bean and its annotations + TestNonController controller = new TestNonController(); + + // Call the method under test + processor.postProcessAfterInitialization(controller, "testNonController"); + + // Verify whether the parsed data is correct + HttpInvocation getInvocation = ControllerManager.getHttpInvocation("/non/get"); + assertNotNull(getInvocation, "getMethod should be registered"); + assertEquals("getMethod", getInvocation.getMethod().getName()); + assertSame(controller, getInvocation.getController()); + + ParamMetaData[] getParams = getInvocation.getParamMetaData(); + assertEquals(1, getParams.length); + assertEquals("param", getParams[0].getParamName()); + assertEquals(ParamMetaData.ParamConvertType.REQUEST_PARAM, getParams[0].getParamConvertType()); + assertTrue(getParams[0].isRequired()); + + HttpInvocation postInvocation = ControllerManager.getHttpInvocation("/non/post"); + assertNotNull(postInvocation, "postMethod should be registered"); + assertEquals("postMethod", postInvocation.getMethod().getName()); + assertSame(controller, postInvocation.getController()); + + ParamMetaData[] postParams = postInvocation.getParamMetaData(); + assertEquals(1, postParams.length); + assertEquals(ParamMetaData.ParamConvertType.MODEL_ATTRIBUTE, postParams[0].getParamConvertType()); + + HttpInvocation updateInvocation = ControllerManager.getHttpInvocation("/non/update"); + assertNotNull(updateInvocation, "updateMethod should be registered"); + assertEquals("updateMethod", updateInvocation.getMethod().getName()); + assertSame(controller, updateInvocation.getController()); + + ParamMetaData[] updateParams = updateInvocation.getParamMetaData(); + assertEquals(1, updateParams.length); + assertNull(updateParams[0].getParamConvertType()); } @RestController @@ -67,7 +172,50 @@ public class RestControllerBeanPostProcessorTest { return "POST"; } } -} + @RestController + @RequestMapping("/api") + static class TestApiController { + + @GetMapping("/get") + public String getMethod(@RequestParam(defaultValue = "defaultValue") String param) { + return "GET"; + } + + @PostMapping("/post") + public String postMethod(@RequestParam(name = "requestBody") String body) { + return "POST"; + } + + @GetMapping("/update") + public String updateMethod(@RequestParam(value = "userName") String name, + @Nonnull @RequestParam(required = false) Integer age) { + return "update"; + } + } + + @RestController + @RequestMapping("/non") + static class TestNonController { + @GetMapping("/get") + public String getMethod(String param) { + return "GET"; + } + @PostMapping("/post") + public String postMethod(User user) { + return "POST"; + } + + @GetMapping("/update") + public String updateMethod(HttpContext httpContext) { + return "update"; + } + } + + static class User{ + String name; + Integer age; + } +} \ No newline at end of file diff --git a/server/src/main/java/org/apache/seata/server/controller/ClusterController.java b/server/src/main/java/org/apache/seata/server/controller/ClusterController.java index b701f26537..b6edda641e 100644 --- a/server/src/main/java/org/apache/seata/server/controller/ClusterController.java +++ b/server/src/main/java/org/apache/seata/server/controller/ClusterController.java @@ -111,13 +111,9 @@ public class ClusterController { public void watch(HttpContext context, @RequestBody Map<String, Object> groupTerms, @RequestParam(defaultValue = "28000") Integer timeout) { context.setAsync(true); - if (timeout == null) { - timeout = 28000; - } - Integer finalTimeout = timeout; groupTerms.forEach((group, term) -> { Watcher<HttpContext> watcher = - new Watcher<>(group, context, finalTimeout, Long.parseLong(String.valueOf(term))); + new Watcher<>(group, context, timeout, Long.parseLong(String.valueOf(term))); clusterWatcherManager.registryWatcher(watcher); }); } --------------------------------------------------------------------- To unsubscribe, e-mail: notifications-unsubscr...@seata.apache.org For additional commands, e-mail: notifications-h...@seata.apache.org