This is an automated email from the ASF dual-hosted git repository. dmsolr pushed a commit to branch feat/issue-7737-springmvc-path-aliases in repository https://gitbox.apache.org/repos/asf/shenyu.git
commit cb0c95873c0c9558f0d4533c8d943fcf0ed36d69 Author: Luke Haochao Zhuang <[email protected]> AuthorDate: Sun Oct 18 18:10:16 2026 +0800 feat(client-springmvc): register all Spring MVC path aliases (#7737) Both the annotation-driven registrar and the legacy context-refreshed listener collapsed native @RequestMapping and @ShenyuSpringMvcClient path arrays to their first element, so alias paths were never registered and stayed unreachable through the gateway. - SpringMvcApiMetaRegister: resolve the full class/method path lists from the retained annotations (explicit ShenYu override first, native RequestMapping fallback) and register every class x method x namespace combination, deduplicating overlapping aliases; wildcard (/**) beans now register every class-level alias. - SpringMvcClientEventListener: expand method-level aliases (previously only the first path was supported) and publish each class x method x namespace combination. - Add regression tests covering native/ShenYu multi-path cartesian product, deduplication, wildcard expansion and method-level alias registration. --- .../init/SpringMvcClientEventListener.java | 112 ++++++++++----- .../register/SpringMvcApiMetaRegister.java | 158 ++++++++++++++++----- .../init/SpringMvcClientEventListenerTest.java | 54 +++++++ .../register/SpringMvcApiMetaRegisterTest.java | 101 +++++++++++++ 4 files changed, 357 insertions(+), 68 deletions(-) diff --git a/shenyu-client/shenyu-client-http/shenyu-client-springmvc/src/main/java/org/apache/shenyu/client/springmvc/init/SpringMvcClientEventListener.java b/shenyu-client/shenyu-client-http/shenyu-client-springmvc/src/main/java/org/apache/shenyu/client/springmvc/init/SpringMvcClientEventListener.java index 78e7489fff..c1dfc9171e 100644 --- a/shenyu-client/shenyu-client-http/shenyu-client-springmvc/src/main/java/org/apache/shenyu/client/springmvc/init/SpringMvcClientEventListener.java +++ b/shenyu-client/shenyu-client-http/shenyu-client-springmvc/src/main/java/org/apache/shenyu/client/springmvc/init/SpringMvcClientEventListener.java @@ -238,15 +238,18 @@ public class SpringMvcClientEventListener extends AbstractContextRefreshedEventL // the result of ReflectionUtils#getUniqueDeclaredMethods contains method such as hashCode, wait, toSting // add Objects.nonNull(requestMapping) to make sure not register wrong method if (Objects.nonNull(methodShenyuClient) && Objects.nonNull(requestMapping)) { - List<String> namespaceIds = super.getNamespace(); - for (String namespaceId : namespaceIds) { - final String apiPath = hasMethodAnnotation - ? buildApiPath(method, superPath, methodShenyuClient) - : buildApiPathFromRequestMapping(method, superPath); - final MetaDataRegisterDTO metaData = buildMetaDataDTO(bean, methodShenyuClient, - apiPath, clazz, method, namespaceId); - getPublisher().publishEvent(metaData); - getMetaDataMap().put(method, metaData); + // register every method-level alias (native and ShenYu) under the current class super path + final List<String> apiPaths = (hasMethodAnnotation + ? buildApiPaths(method, superPath, methodShenyuClient) + : buildApiPathsFromRequestMapping(method, superPath)) + .stream().distinct().collect(Collectors.toList()); + for (String namespaceId : super.getNamespace()) { + for (String apiPath : apiPaths) { + final MetaDataRegisterDTO metaData = buildMetaDataDTO(bean, methodShenyuClient, + apiPath, clazz, method, namespaceId); + getPublisher().publishEvent(metaData); + getMetaDataMap().put(method, metaData); + } } } } @@ -254,55 +257,94 @@ public class SpringMvcClientEventListener extends AbstractContextRefreshedEventL @Override protected String buildApiPath(final Method method, final String superPath, @NonNull final ShenyuSpringMvcClient methodShenyuClient) { - String contextPath = getContextPath(); - final String[] annotationPaths = methodShenyuClient.path(); - final String annotationPath = ArrayUtils.isNotEmpty(annotationPaths) ? annotationPaths[0] : ""; - if (StringUtils.isNotBlank(annotationPath)) { - return pathJoin(contextPath, superPath, annotationPath); + return buildApiPaths(method, superPath, methodShenyuClient).get(0); + } + + /** + * Builds every effective api path alias for the method under the given super path, honouring an + * explicit {@link ShenyuSpringMvcClient} override first and falling back to the native mapping array. + * + * @param method the method + * @param superPath the class-level super path + * @param methodShenyuClient the effective ShenYu annotation + * @return all effective api paths, never empty + */ + protected List<String> buildApiPaths(final Method method, final String superPath, + @NonNull final ShenyuSpringMvcClient methodShenyuClient) { + final String contextPath = getContextPath(); + final List<String> annotationPaths = distinctNonBlank(methodShenyuClient.path()); + if (!annotationPaths.isEmpty()) { + return joinAll(contextPath, superPath, annotationPaths); } - final String path = getPathByMethod(method); - if (StringUtils.isNotBlank(path)) { - return pathJoin(contextPath, superPath, path); + final List<String> nativePaths = getPathListByMethod(method); + if (!nativePaths.isEmpty()) { + return joinAll(contextPath, superPath, nativePaths); } - return pathJoin(contextPath, superPath); + return Collections.singletonList(pathJoin(contextPath, superPath)); } String buildApiPathFromRequestMapping(final Method method, final String superPath) { - String contextPath = getContextPath(); - final String path = getPathByMethod(method); - if (StringUtils.isNotBlank(path)) { - return pathJoin(contextPath, superPath, path); + return buildApiPathsFromRequestMapping(method, superPath).get(0); + } + + /** + * Builds every effective api path alias for the method from its native mapping array under the given super path. + * + * @param method the method + * @param superPath the class-level super path + * @return all effective api paths, never empty + */ + protected List<String> buildApiPathsFromRequestMapping(final Method method, final String superPath) { + final String contextPath = getContextPath(); + final List<String> nativePaths = getPathListByMethod(method); + if (!nativePaths.isEmpty()) { + return joinAll(contextPath, superPath, nativePaths); } - return pathJoin(contextPath, superPath); + return Collections.singletonList(pathJoin(contextPath, superPath)); + } + + private List<String> joinAll(final String contextPath, final String superPath, final List<String> paths) { + return paths.stream() + .map(path -> pathJoin(contextPath, superPath, path)) + .collect(Collectors.toList()); } private String formatPath(final String path) { return path.replaceAll("/+", "/").replaceFirst("/$", ""); } - private String getPathByMethod(@NonNull final Method method) { + private List<String> getPathListByMethod(@NonNull final Method method) { for (Class<? extends Annotation> mapping : mappingAnnotation) { - final String pathByAnnotation = getPathByAnnotation(AnnotatedElementUtils.findMergedAnnotation(method, mapping)); - if (StringUtils.isNotBlank(pathByAnnotation)) { - return pathByAnnotation; + final List<String> paths = getPathListByAnnotation(AnnotatedElementUtils.findMergedAnnotation(method, mapping)); + if (!paths.isEmpty()) { + return paths; } } - return null; + return Collections.emptyList(); } - private String getPathByAnnotation(@Nullable final Annotation annotation) { + private List<String> getPathListByAnnotation(@Nullable final Annotation annotation) { if (Objects.isNull(annotation)) { - return null; + return Collections.emptyList(); } final Object value = AnnotationUtils.getValue(annotation, "value"); if (value instanceof String && StringUtils.isNotBlank((String) value)) { - return (String) value; + return Collections.singletonList((String) value); + } + if (value instanceof String[]) { + return distinctNonBlank((String[]) value); } - // Only the first path is supported temporarily - if (value instanceof String[] && ArrayUtils.isNotEmpty((String[]) value) && StringUtils.isNotBlank(((String[]) value)[0])) { - return ((String[]) value)[0]; + return Collections.emptyList(); + } + + private List<String> distinctNonBlank(final String[] paths) { + if (ArrayUtils.isEmpty(paths)) { + return Collections.emptyList(); } - return null; + return Arrays.stream(paths) + .filter(StringUtils::isNotBlank) + .distinct() + .collect(Collectors.toList()); } @Override diff --git a/shenyu-client/shenyu-client-http/shenyu-client-springmvc/src/main/java/org/apache/shenyu/client/springmvc/register/SpringMvcApiMetaRegister.java b/shenyu-client/shenyu-client-http/shenyu-client-springmvc/src/main/java/org/apache/shenyu/client/springmvc/register/SpringMvcApiMetaRegister.java index ec88f6e5be..506a762010 100644 --- a/shenyu-client/shenyu-client-http/shenyu-client-springmvc/src/main/java/org/apache/shenyu/client/springmvc/register/SpringMvcApiMetaRegister.java +++ b/shenyu-client/shenyu-client-http/shenyu-client-springmvc/src/main/java/org/apache/shenyu/client/springmvc/register/SpringMvcApiMetaRegister.java @@ -17,6 +17,7 @@ package org.apache.shenyu.client.springmvc.register; +import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.apache.shenyu.client.core.disruptor.ShenyuClientRegisterEventPublisher; import org.apache.shenyu.client.core.register.ApiBean; @@ -29,9 +30,15 @@ import org.apache.shenyu.common.enums.RpcTypeEnum; import org.apache.shenyu.common.utils.PathUtils; import org.apache.shenyu.register.common.dto.MetaDataRegisterDTO; import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.web.bind.annotation.RequestMapping; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; import java.util.List; import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; public class SpringMvcApiMetaRegister extends AbstractApiMetaRegistrar { @@ -66,12 +73,27 @@ public class SpringMvcApiMetaRegister extends AbstractApiMetaRegistrar { ShenyuSpringMvcClient annotation = apiBean.getAnnotation(ShenyuSpringMvcClient.class); - return Objects.nonNull(annotation) && annotation.path()[0].endsWith("/**"); + return Objects.nonNull(annotation) && isWildcard(annotation); } @Override protected List<MetaDataRegisterDTO> preParseList(final ApiBean apiBean) { - return clientRegisterConfig.getNamespace().stream().map(namespaceId -> preParse(apiBean, namespaceId)).collect(Collectors.toList()); + ShenyuSpringMvcClient annotation = apiBean.getAnnotation(ShenyuSpringMvcClient.class); + List<String> beanPaths = distinctNonBlank(annotation.path()); + if (beanPaths.isEmpty()) { + beanPaths = Collections.singletonList(StringUtils.EMPTY); + } + List<MetaDataRegisterDTO> result = new ArrayList<>(); + for (String namespaceId : clientRegisterConfig.getNamespace()) { + Set<String> seen = new LinkedHashSet<>(); + for (String beanPath : beanPaths) { + String apiPath = PathUtils.pathJoin(clientRegisterConfig.getContextPath(), beanPath); + if (seen.add(apiPath)) { + result.add(buildPreMetaData(apiBean, annotation, apiPath, namespaceId)); + } + } + } + return result; } @Override @@ -82,8 +104,15 @@ public class SpringMvcApiMetaRegister extends AbstractApiMetaRegistrar { private MetaDataRegisterDTO preParse(final ApiBean apiBean, final String namespaceId) { ShenyuSpringMvcClient annotation = apiBean.getAnnotation(ShenyuSpringMvcClient.class); - String apiPath = PathUtils.pathJoin(clientRegisterConfig.getContextPath(), annotation.path()[0]); + List<String> beanPaths = distinctNonBlank(annotation.path()); + String apiPath = PathUtils.pathJoin(clientRegisterConfig.getContextPath(), + beanPaths.isEmpty() ? StringUtils.EMPTY : beanPaths.get(0)); + + return buildPreMetaData(apiBean, annotation, apiPath, namespaceId); + } + private MetaDataRegisterDTO buildPreMetaData(final ApiBean apiBean, final ShenyuSpringMvcClient annotation, + final String apiPath, final String namespaceId) { return MetaDataRegisterDTO.builder() .namespaceId(namespaceId) .contextPath(clientRegisterConfig.getContextPath()) @@ -107,7 +136,7 @@ public class SpringMvcApiMetaRegister extends AbstractApiMetaRegistrar { protected Boolean match(final ApiBean apiBean) { ShenyuSpringMvcClient annotation = apiBean.getAnnotation(ShenyuSpringMvcClient.class); if (Objects.nonNull(annotation)) { - return !annotation.path()[0].endsWith("/**"); + return !isWildcard(annotation); } return true; } @@ -122,47 +151,110 @@ public class SpringMvcApiMetaRegister extends AbstractApiMetaRegistrar { ShenyuSpringMvcClient methodAnnotation = apiDefinition.getAnnotation(ShenyuSpringMvcClient.class); - String methodPath = Objects.isNull(methodAnnotation) ? StringUtils.EMPTY : methodAnnotation.path()[0]; - - if (StringUtils.isEmpty(methodPath)) { - methodPath = apiDefinition.getMethodPath(); - } - ShenyuSpringMvcClient classAnnotation = apiDefinition.getApiBean() .getAnnotation(ShenyuSpringMvcClient.class); - String beanPath = Objects.isNull(classAnnotation) || StringUtils.isBlank(classAnnotation.path()[0]) - ? apiDefinition.getBeanPath() : classAnnotation.path()[0]; - - String apiPath = PathUtils.pathJoin(clientRegisterConfig.getContextPath(), beanPath, methodPath); + List<String> beanPaths = resolveBeanPaths(apiDefinition, classAnnotation); + List<String> methodPaths = resolveMethodPaths(apiDefinition, methodAnnotation); String pathDesc = Objects.isNull(methodAnnotation) ? classAnnotation.desc() : methodAnnotation.desc(); boolean enabled = (Objects.isNull(classAnnotation) || classAnnotation.enabled()) && (Objects.isNull(methodAnnotation) || methodAnnotation.enabled()); - String ruleName = Objects.isNull(methodAnnotation) || StringUtils.isEmpty(methodAnnotation.ruleName()) - ? apiPath : methodAnnotation.ruleName(); + String explicitRuleName = Objects.isNull(methodAnnotation) ? null : methodAnnotation.ruleName(); boolean registerMetaData = (Objects.isNull(classAnnotation) || classAnnotation.registerMetaData()) && (Objects.isNull(methodAnnotation) || methodAnnotation.registerMetaData()); - return clientRegisterConfig.getNamespace().stream().map(namespaceId -> MetaDataRegisterDTO.builder() - .namespaceId(namespaceId) - .contextPath(clientRegisterConfig.getContextPath()) - .addPrefixed(addPrefixed) - .appName(appName) - .host(host) - .port(port) - .serviceName(apiDefinition.getBeanClass().getName()) - .methodName(apiDefinition.getApiMethodName()) - .path(apiPath) - .pathDesc(pathDesc) - .parameterTypes(apiDefinition.getParameterTypes()) - .rpcType(RpcTypeEnum.HTTP.getName()) - .enabled(enabled) - .ruleName(ruleName) - .registerMetaData(registerMetaData) - .build()).collect(Collectors.toList()); + List<MetaDataRegisterDTO> result = new ArrayList<>(); + for (String namespaceId : clientRegisterConfig.getNamespace()) { + Set<String> seen = new LinkedHashSet<>(); + for (String beanPath : beanPaths) { + for (String methodPath : methodPaths) { + String apiPath = PathUtils.pathJoin(clientRegisterConfig.getContextPath(), beanPath, methodPath); + if (!seen.add(apiPath)) { + continue; + } + String ruleName = StringUtils.isEmpty(explicitRuleName) ? apiPath : explicitRuleName; + result.add(MetaDataRegisterDTO.builder() + .namespaceId(namespaceId) + .contextPath(clientRegisterConfig.getContextPath()) + .addPrefixed(addPrefixed) + .appName(appName) + .host(host) + .port(port) + .serviceName(apiDefinition.getBeanClass().getName()) + .methodName(apiDefinition.getApiMethodName()) + .path(apiPath) + .pathDesc(pathDesc) + .parameterTypes(apiDefinition.getParameterTypes()) + .rpcType(RpcTypeEnum.HTTP.getName()) + .enabled(enabled) + .ruleName(ruleName) + .registerMetaData(registerMetaData) + .build()); + } + } + } + return result; + } + + /** + * Resolves every effective class-level path alias, honouring an explicit {@link ShenyuSpringMvcClient} + * override first and falling back to the native {@link RequestMapping} array so that no alias is truncated. + */ + private List<String> resolveBeanPaths(final ApiBean.ApiDefinition apiDefinition, + final ShenyuSpringMvcClient classAnnotation) { + if (Objects.nonNull(classAnnotation)) { + List<String> paths = distinctNonBlank(classAnnotation.path()); + if (!paths.isEmpty()) { + return paths; + } + } + RequestMapping requestMapping = apiDefinition.getApiBean().getAnnotation(RequestMapping.class); + if (Objects.nonNull(requestMapping)) { + List<String> paths = distinctNonBlank(requestMapping.path()); + if (!paths.isEmpty()) { + return paths; + } + } + return Collections.singletonList(StringUtils.defaultString(apiDefinition.getBeanPath())); + } + + /** + * Resolves every effective method-level path alias, honouring an explicit {@link ShenyuSpringMvcClient} + * override first and falling back to the native {@link RequestMapping} array so that no alias is truncated. + */ + private List<String> resolveMethodPaths(final ApiBean.ApiDefinition apiDefinition, + final ShenyuSpringMvcClient methodAnnotation) { + if (Objects.nonNull(methodAnnotation)) { + List<String> paths = distinctNonBlank(methodAnnotation.path()); + if (!paths.isEmpty()) { + return paths; + } + } + RequestMapping requestMapping = apiDefinition.getAnnotation(RequestMapping.class); + if (Objects.nonNull(requestMapping)) { + List<String> paths = distinctNonBlank(requestMapping.path()); + if (!paths.isEmpty()) { + return paths; + } + } + return Collections.singletonList(StringUtils.defaultString(apiDefinition.getMethodPath())); + } + + private boolean isWildcard(final ShenyuSpringMvcClient annotation) { + return ArrayUtils.isNotEmpty(annotation.path()) && annotation.path()[0].endsWith("/**"); + } + + private List<String> distinctNonBlank(final String[] paths) { + if (ArrayUtils.isEmpty(paths)) { + return Collections.emptyList(); + } + return Arrays.stream(paths) + .filter(StringUtils::isNotBlank) + .distinct() + .collect(Collectors.toList()); } } diff --git a/shenyu-client/shenyu-client-http/shenyu-client-springmvc/src/test/java/org/apache/shenyu/client/springmvc/init/SpringMvcClientEventListenerTest.java b/shenyu-client/shenyu-client-http/shenyu-client-springmvc/src/test/java/org/apache/shenyu/client/springmvc/init/SpringMvcClientEventListenerTest.java index ecd3b3520b..266f5c7f1e 100644 --- a/shenyu-client/shenyu-client-http/shenyu-client-springmvc/src/test/java/org/apache/shenyu/client/springmvc/init/SpringMvcClientEventListenerTest.java +++ b/shenyu-client/shenyu-client-http/shenyu-client-springmvc/src/test/java/org/apache/shenyu/client/springmvc/init/SpringMvcClientEventListenerTest.java @@ -382,6 +382,50 @@ public class SpringMvcClientEventListenerTest { } } + @Test + public void testMultiMethodPathPublishesAllAliases() { + try { + SpringMvcMultiMethodPathTestBean multiMethodBean = new SpringMvcMultiMethodPathTestBean(); + + Map<String, Object> controllerBeans = new LinkedHashMap<>(); + controllerBeans.put("multiMethodPathBean", multiMethodBean); + when(applicationContext.getBeansWithAnnotation(eq(Controller.class))).thenReturn(controllerBeans); + when(applicationContext.getBeansWithAnnotation(eq(ApiModule.class))).thenReturn(new LinkedHashMap<>()); + + when(applicationContext.getEnvironment()).thenReturn(env); + when(env.getProperty("shenyu.discovery.type", ShenyuClientConstants.DISCOVERY_LOCAL_MODE)).thenReturn("local"); + when(applicationContext.getAutowireCapableBeanFactory()).thenReturn(beanFactory); + + try (MockedStatic<ShenyuClientRegisterEventPublisher> publisherMock = mockStatic(ShenyuClientRegisterEventPublisher.class); + MockedStatic<PortUtils> portUtilsMock = mockStatic(PortUtils.class)) { + + ShenyuClientRegisterEventPublisher publisher = mock(ShenyuClientRegisterEventPublisher.class); + publisherMock.when(ShenyuClientRegisterEventPublisher::getInstance).thenReturn(publisher); + portUtilsMock.when(() -> PortUtils.findPort(beanFactory)).thenReturn(8080); + + SpringMvcClientEventListener listener = buildSpringMvcClientEventListener(false, false); + listener.onApplicationEvent(new ContextRefreshedEvent(applicationContext)); + + ArgumentCaptor<DataTypeParent> captor = ArgumentCaptor.forClass(DataTypeParent.class); + verify(publisher, atLeastOnce()).publishEvent(captor.capture()); + + List<String> metaPaths = captor.getAllValues().stream() + .filter(e -> e instanceof MetaDataRegisterDTO) + .map(e -> ((MetaDataRegisterDTO) e).getPath()) + .sorted() + .collect(Collectors.toList()); + Assertions.assertEquals(2, metaPaths.size(), + "Expected 2 metadata entries for 2 method-level aliases"); + Assertions.assertTrue(metaPaths.get(0).endsWith("/multi-method/m-a"), + "Expected path ending with /multi-method/m-a but got: " + metaPaths.get(0)); + Assertions.assertTrue(metaPaths.get(1).endsWith("/multi-method/m-b"), + "Expected path ending with /multi-method/m-b but got: " + metaPaths.get(1)); + } + } finally { + registerUtilsMockedStatic.close(); + } + } + @Test public void testBuildApiDocSextetDefaultProducesConsumes() throws NoSuchMethodException { try { @@ -521,6 +565,16 @@ public class SpringMvcClientEventListenerTest { } } + @RestController + @ShenyuSpringMvcClient(path = "/multi-method") + static class SpringMvcMultiMethodPathTestBean { + + @GetMapping({"/m-a", "/m-b"}) + public String greet() { + return "ok"; + } + } + @RestController static class ApiDocTestBean { diff --git a/shenyu-client/shenyu-client-http/shenyu-client-springmvc/src/test/java/org/apache/shenyu/client/springmvc/register/SpringMvcApiMetaRegisterTest.java b/shenyu-client/shenyu-client-http/shenyu-client-springmvc/src/test/java/org/apache/shenyu/client/springmvc/register/SpringMvcApiMetaRegisterTest.java index 770564343a..3c44a90c9e 100644 --- a/shenyu-client/shenyu-client-http/shenyu-client-springmvc/src/test/java/org/apache/shenyu/client/springmvc/register/SpringMvcApiMetaRegisterTest.java +++ b/shenyu-client/shenyu-client-http/shenyu-client-springmvc/src/test/java/org/apache/shenyu/client/springmvc/register/SpringMvcApiMetaRegisterTest.java @@ -139,6 +139,61 @@ public class SpringMvcApiMetaRegisterTest { return beansExtractor.extract(null, objectHashMap).get(0); } + @Test + void testNativeMultiPathRegistersAllCombinations() throws Exception { + + ApiBean apiBean = createSimpleApiBean(TestApiBeanNativeMultiPath.class); + + springMvcApiMetaRegister.register(apiBean); + + assertThat(collectSortedPaths(), equalTo(List.of( + "/testContext/legacy/bar", + "/testContext/legacy/foo", + "/testContext/v1/bar", + "/testContext/v1/foo"))); + } + + @Test + void testShenyuMultiPathRegistersAllCombinations() throws Exception { + + ApiBean apiBean = createSimpleApiBean(TestApiBeanShenyuMultiPath.class); + + springMvcApiMetaRegister.register(apiBean); + + assertThat(collectSortedPaths(), equalTo(List.of( + "/testContext/legacy/bar", + "/testContext/legacy/foo", + "/testContext/v1/bar", + "/testContext/v1/foo"))); + } + + @Test + void testDuplicateAliasesAreDeduplicated() throws Exception { + + ApiBean apiBean = createSimpleApiBean(TestApiBeanDuplicateAlias.class); + + springMvcApiMetaRegister.register(apiBean); + + assertThat(collectSortedPaths(), equalTo(List.of("/testContext/v1/foo"))); + } + + @Test + void testWildcardMultiPathRegistersEveryAlias() throws Exception { + + ApiBean apiBean = createSimpleApiBean(TestApiBeanWildcardMultiPath.class); + + springMvcApiMetaRegister.register(apiBean); + + assertThat(collectSortedPaths(), equalTo(List.of("/testContext/a/**", "/testContext/b/**"))); + } + + private List<String> collectSortedPaths() { + return testShenyuClientRegisterEventPublisher.events.stream() + .map(MetaDataRegisterDTO::getPath) + .sorted() + .collect(Collectors.toList()); + } + @ParameterizedTest @NullSource @ValueSource(strings = {"first;second", "", " "}) @@ -231,6 +286,52 @@ public class SpringMvcApiMetaRegisterTest { } } + @RestController + @ShenyuSpringMvcClient + @RequestMapping({"/v1", "/legacy"}) + static class TestApiBeanNativeMultiPath { + + @RequestMapping({"/foo", "/bar"}) + public String testMethod() { + return ""; + } + } + + @RestController + @ShenyuSpringMvcClient({"/v1", "/legacy"}) + @RequestMapping("/nativeClass") + static class TestApiBeanShenyuMultiPath { + + @RequestMapping("/nativeMethod") + @ShenyuSpringMvcClient({"/foo", "/bar"}) + public String testMethod() { + return ""; + } + } + + @RestController + @ShenyuSpringMvcClient({"/v1", "/v1"}) + @RequestMapping("/nativeClass") + static class TestApiBeanDuplicateAlias { + + @RequestMapping("/nativeMethod") + @ShenyuSpringMvcClient("/foo") + public String testMethod() { + return ""; + } + } + + @RestController + @ShenyuSpringMvcClient({"/a/**", "/b/**"}) + @RequestMapping("/nativeClass") + static class TestApiBeanWildcardMultiPath { + + @RequestMapping("/testMethod") + public String testMethod() { + return ""; + } + } + static class TestShenyuClientRegisterEventPublisher extends ShenyuClientRegisterEventPublisher { private MetaDataRegisterDTO metaData;
