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;

Reply via email to