This is an automated email from the ASF dual-hosted git repository.
dmsolr pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu.git
The following commit(s) were added to refs/heads/master by this push:
new 81a85f3a4a feat(client-springmvc): register all Spring MVC path
aliases (#7813)
81a85f3a4a is described below
commit 81a85f3a4ab3cf799930ae8b1f185f5b4fd72b10
Author: Daming <[email protected]>
AuthorDate: Mon Oct 19 11:42:44 2026 +0800
feat(client-springmvc): register all Spring MVC path aliases (#7813)
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.
Co-authored-by: Luke Haochao Zhuang <[email protected]>
---
.../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;