diff --git a/.artifacts b/.artifacts
index 28b0249adb27..a8895ee6ab23 100644
--- a/.artifacts
+++ b/.artifacts
@@ -116,6 +116,7 @@ dubbo-xds
dubbo-plugin-loom
dubbo-rest-jaxrs
dubbo-rest-spring
+dubbo-rest-spring6
dubbo-rest-openapi
dubbo-triple-servlet
dubbo-triple-websocket
diff --git a/dubbo-distribution/dubbo-all-shaded/pom.xml b/dubbo-distribution/dubbo-all-shaded/pom.xml
index 84884121d5b0..6f551043cdf4 100644
--- a/dubbo-distribution/dubbo-all-shaded/pom.xml
+++ b/dubbo-distribution/dubbo-all-shaded/pom.xml
@@ -275,13 +275,6 @@
compile
true
-
- org.apache.dubbo
- dubbo-rest-spring
- ${project.version}
- compile
- true
-
org.apache.dubbo
dubbo-rest-openapi
@@ -540,7 +533,6 @@
org.apache.dubbo:dubbo-rpc-injvm
org.apache.dubbo:dubbo-rpc-triple
org.apache.dubbo:dubbo-rest-jaxrs
- org.apache.dubbo:dubbo-rest-spring
org.apache.dubbo:dubbo-rest-openapi
org.apache.dubbo:dubbo-triple-servlet
org.apache.dubbo:dubbo-triple-websocket
diff --git a/dubbo-distribution/dubbo-all/pom.xml b/dubbo-distribution/dubbo-all/pom.xml
index 3ac1a3939f91..5a18075ff6f5 100644
--- a/dubbo-distribution/dubbo-all/pom.xml
+++ b/dubbo-distribution/dubbo-all/pom.xml
@@ -275,13 +275,6 @@
compile
true
-
- org.apache.dubbo
- dubbo-rest-spring
- ${project.version}
- compile
- true
-
org.apache.dubbo
dubbo-rest-openapi
@@ -539,7 +532,6 @@
org.apache.dubbo:dubbo-rpc-injvm
org.apache.dubbo:dubbo-rpc-triple
org.apache.dubbo:dubbo-rest-jaxrs
- org.apache.dubbo:dubbo-rest-spring
org.apache.dubbo:dubbo-rest-openapi
org.apache.dubbo:dubbo-triple-servlet
org.apache.dubbo:dubbo-triple-websocket
diff --git a/dubbo-distribution/dubbo-bom/pom.xml b/dubbo-distribution/dubbo-bom/pom.xml
index d6994783575e..0bbf338a8f7d 100644
--- a/dubbo-distribution/dubbo-bom/pom.xml
+++ b/dubbo-distribution/dubbo-bom/pom.xml
@@ -327,6 +327,11 @@
dubbo-rest-spring
${project.version}
+
+ org.apache.dubbo
+ dubbo-rest-spring6
+ ${project.version}
+
org.apache.dubbo
dubbo-rest-openapi
diff --git a/dubbo-plugin/dubbo-rest-spring/pom.xml b/dubbo-plugin/dubbo-rest-spring/pom.xml
index c786df8317b0..8397bcc94954 100644
--- a/dubbo-plugin/dubbo-rest-spring/pom.xml
+++ b/dubbo-plugin/dubbo-rest-spring/pom.xml
@@ -98,6 +98,12 @@
log4j-slf4j-impl
test
+
+ org.apache.dubbo
+ dubbo-spring-boot-starter
+ ${project.version}
+ test
+
diff --git a/dubbo-plugin/dubbo-rest-spring/src/main/resources/META-INF/dubbo/rest-spring-javax.properties b/dubbo-plugin/dubbo-rest-spring/src/main/resources/META-INF/dubbo/rest-spring-javax.properties
new file mode 100644
index 000000000000..bcf39c10bb06
--- /dev/null
+++ b/dubbo-plugin/dubbo-rest-spring/src/main/resources/META-INF/dubbo/rest-spring-javax.properties
@@ -0,0 +1,16 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+adapter=dubbo-rest-spring
diff --git a/dubbo-plugin/dubbo-rest-spring/src/test/groovy/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/RestProtocolTest.groovy b/dubbo-plugin/dubbo-rest-spring/src/test/groovy/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/RestProtocolTest.groovy
index ec7a111ba0ba..d718872ca5b4 100644
--- a/dubbo-plugin/dubbo-rest-spring/src/test/groovy/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/RestProtocolTest.groovy
+++ b/dubbo-plugin/dubbo-rest-spring/src/test/groovy/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/RestProtocolTest.groovy
@@ -17,147 +17,15 @@
package org.apache.dubbo.rpc.protocol.tri.rest.support.spring
-import org.apache.dubbo.remoting.http12.message.MediaType
-import org.apache.dubbo.rpc.protocol.tri.rest.service.Book
-import org.apache.dubbo.rpc.protocol.tri.rest.support.spring.service.SpringDemoServiceImpl
import org.apache.dubbo.rpc.protocol.tri.rest.support.spring.service.SpringDemoService
-import org.apache.dubbo.rpc.protocol.tri.rest.test.BaseServiceTest
-import org.apache.dubbo.rpc.protocol.tri.test.TestRequest
+import org.apache.dubbo.rpc.protocol.tri.rest.support.spring.service.SpringDemoServiceImpl
+import org.apache.dubbo.rpc.protocol.tri.rest.test.RestProtocolTestContract
import org.apache.dubbo.rpc.protocol.tri.test.TestRunnerBuilder
-class RestProtocolTest extends BaseServiceTest {
+class RestProtocolTest extends RestProtocolTestContract {
@Override
void setupService(TestRunnerBuilder builder) {
builder.provider(SpringDemoService.class, new SpringDemoServiceImpl())
}
-
- def "hello world"() {
- expect:
- runner.get(path) == output
- where:
- path | output
- '/spring/hello?name=world' | 'hello world'
- '/spring/hello/?name=world' | 'hello world'
- '/spring/hello.yml?name=world' | 'hello world'
- }
-
- def "list argument body test"() {
- expect:
- runner.post(path, body) contains output
- where:
- path | body | output
- '/spring/listArgBodyTest?age=2' | '[1,2]' | '[1,2]'
- }
-
- def "map argument body test"() {
- expect:
- def actual = runner.post(path, body)
- actual.contains('1:[2,3]') && actual.contains('4:[5,6]')
- where:
- path | body | output
- '/spring/mapArgBodyTest?age=2' | '{"1":["2",3],"4":[5,"6"]}' | '{4:[5,6],1:[2,3]}'
- }
-
- def "bean argument get test"() {
- expect:
- runner.get(path, Book.class).price == output
- where:
- path | output
- '/spring/beanArgTest' | 0
- '/spring/beanArgTest?quote=5' | 5
- }
-
- def "bean argument post test"() {
- expect:
- runner.post(path, body, Book.class).price == output
- where:
- path | body | output
- '/spring/beanArgTest' | [:] | 0
- '/spring/beanArgTest' | [price: 6] | 6
- '/spring/beanArgTest2' | '{"price": 5}' | 5
- }
-
- def "spring bean argument test"() {
- expect:
- runner.get(path) contains output
- where:
- path | output
- '/spring/bean?id=1&name=sam' | '"id":1,"name":"sam"'
- '/spring/bean?name=sam&phone=123&email=a@b.com' | '"email":"a@b.com","name":"sam","phone":"123"'
- '/spring/bean?group.name=g1&group.owner.name=jack' | '"group":{"id":0,"name":"g1","owner":{"name":"jack"'
- '/spring/bean?group.parent.parent.children[0].name=xx' | '"group":{"id":0,"parent":{"id":0,"parent":{"children":[{"id":0,"name":"xx"}],"id":0}}}'
- '/spring/bean?ids=3&ids=4' | '"ids":[3,4]'
- '/spring/bean?ids[]=3&ids[]=4' | '"ids":[3,4]'
- '/spring/bean?ids[1]=3&ids[2]=4' | '"ids":[0,3,4]'
- '/spring/bean?scores=3&scores=4' | '"scores":[3,4]'
- '/spring/bean?scores[]=3&scores[]=4' | '"scores":[3,4]'
- '/spring/bean?scores[1]=3&scores[2]=4' | '"scores":[null,3,4]'
- '/spring/bean?tags[0].name=a&tags[0].value=b&tags[1].name=c&tags[1].value=d' | '"tags":[{"name":"a","value":"b"},{"name":"c","value":"d"}]'
- '/spring/bean?tagsA[0].name=a&tagsA[0].value=b&tagsA[1].name=c&tagsA[1].value=d' | '"tagsA":[{"name":"a","value":"b"},{"name":"c","value":"d"}]'
- '/spring/bean?tagsB[0].name=e&tagsB[1].name=c&tagsB[1].value=d' | '"tagsB":[{"name":"e","value":"b"},{"name":"c","value":"d"}]'
- '/spring/bean?tagsC[0].name=e&tagsC[1].name=c&tagsC[1].value=d' | '"tagsC":[{"name":"e","value":"b"},{"name":"c","value":"d"}]'
- '/spring/bean?id=1&features[a]=xx&features[b]=2' | '"features":{"a":"xx","b":"2"}'
- '/spring/bean?tagMapB[2].name=a&tagMapB[2].value=b&tagMapB[3].name=c' | '"tagMapB":{2:{"name":"a","value":"b"},3:{"name":"c"}}'
- }
-
- def "bean body test"() {
- expect:
- runner.post(path, body) contains output
- where:
- path | body | output
- '/spring/beanBodyTest?age=2' | '[{"id":1,"name":"g1"},{"id":2,"name":"g2"}]' | '[{"id":1,"name":"g1"},{"id":2,"name":"g2"}]'
- }
-
- def "multiValueMap test"() {
- expect:
- runner.get(path) contains output
- where:
- path | output
- '/spring/multiValueMapTest?name=1&name=2&age=8' | '{"name":[1,2],"age":[8]}'
- }
-
- def "urlEncodeForm test"() {
- given:
- def request = new TestRequest(
- path: path,
- contentType: MediaType.APPLICATION_FROM_URLENCODED,
- params: [
- 'name': 'Sam',
- 'age' : 8
- ]
- )
- expect:
- runner.post(request) == output
- where:
- path | output
- '/spring/argTest' | 'Sam is 8 years old'
- }
-
- def "no interface method test"() {
- expect:
- runner.get(path) contains output
- where:
- path | output
- '/spring/noInterface' | 'ok'
- '/spring/noInterfaceAndMapping' | '404'
- }
-
- def "use interface argument name test"() {
- expect:
- runner.get(path) contains output
- where:
- path | output
- '/spring/argNameTest?name=Sam' | 'Sam'
- }
-
- def "pb server stream test"() {
- expect:
- runner.posts(path, body).size() == output
- where:
- path | body | output
- '/spring/pbServerStream' | '{"service": "3"}' | 3
- '/spring/pbServerStream' | '{}' | 0
- }
-
}
diff --git a/dubbo-plugin/dubbo-rest-spring/src/test/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/RestSpringBoot2ClasspathTest.java b/dubbo-plugin/dubbo-rest-spring/src/test/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/RestSpringBoot2ClasspathTest.java
new file mode 100644
index 000000000000..a375bb654275
--- /dev/null
+++ b/dubbo-plugin/dubbo-rest-spring/src/test/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/RestSpringBoot2ClasspathTest.java
@@ -0,0 +1,58 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.protocol.tri.rest.support.spring;
+
+import org.apache.dubbo.common.extension.Activate;
+
+import java.net.URL;
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.core.SpringVersion;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+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.assertTrue;
+
+class RestSpringBoot2ClasspathTest {
+
+ private static final String ADAPTER_RESOURCE =
+ "org/apache/dubbo/rpc/protocol/tri/rest/support/spring/HandlerInterceptorAdapter.class";
+ private static final String COMMON_AUTO_CONFIGURATION_RESOURCE =
+ "org/apache/dubbo/spring/boot/autoconfigure/DubboAutoConfiguration.class";
+ private static final String LEGACY_MARKER = "META-INF/dubbo/rest-spring-javax.properties";
+ private static final String SPRING6_MARKER = "META-INF/dubbo/rest-spring-jakarta.properties";
+
+ @Test
+ void starterAndExplicitAdapterSelectOnlySpring5JavaxImplementation() throws Exception {
+ ClassLoader classLoader = RestSpringBoot2ClasspathTest.class.getClassLoader();
+ assertNotNull(classLoader.getResource(COMMON_AUTO_CONFIGURATION_RESOURCE));
+
+ List adapters = Collections.list(classLoader.getResources(ADAPTER_RESOURCE));
+ assertEquals(1, adapters.size(), () -> "Expected one REST Spring adapter but found " + adapters);
+ assertTrue(SpringVersion.getVersion().startsWith("5."));
+ assertNotNull(classLoader.getResource(LEGACY_MARKER));
+ assertNull(classLoader.getResource(SPRING6_MARKER));
+
+ Activate activation = HandlerInterceptorAdapter.class.getAnnotation(Activate.class);
+ assertNotNull(activation);
+ assertArrayEquals(new String[] {"org.springframework.web.servlet.HandlerInterceptor"}, activation.onClass());
+ }
+}
diff --git a/dubbo-plugin/dubbo-rest-spring6/pom.xml b/dubbo-plugin/dubbo-rest-spring6/pom.xml
new file mode 100644
index 000000000000..4d2ac0e33677
--- /dev/null
+++ b/dubbo-plugin/dubbo-rest-spring6/pom.xml
@@ -0,0 +1,167 @@
+
+
+
+ 4.0.0
+
+ org.apache.dubbo
+ dubbo-parent
+ ${revision}
+ ../../pom.xml
+
+
+ dubbo-rest-spring6
+
+
+
+ org.apache.dubbo
+ dubbo-rpc-triple
+ ${project.version}
+
+
+ org.apache.dubbo
+ dubbo-config-spring6
+ ${project.version}
+
+
+ org.apache.dubbo
+ dubbo-triple-servlet
+ ${project.version}
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+ provided
+
+
+ org.springframework
+ spring-aop
+ ${spring-6.version}
+
+
+ org.springframework
+ spring-beans
+ ${spring-6.version}
+
+
+ org.springframework
+ spring-context
+ ${spring-6.version}
+
+
+ org.springframework
+ spring-core
+ ${spring-6.version}
+
+
+ org.springframework
+ spring-expression
+ ${spring-6.version}
+
+
+ org.springframework
+ spring-jcl
+ ${spring-6.version}
+
+
+ org.springframework
+ spring-web
+ ${spring-6.version}
+
+
+ org.springframework
+ spring-webmvc
+ ${spring-6.version}
+ true
+
+
+ org.apache.dubbo
+ dubbo-remoting-netty4
+ ${project.version}
+ test
+
+
+ javax.xml.bind
+ jaxb-api
+ test
+
+
+ org.glassfish.jaxb
+ jaxb-runtime
+ test
+
+
+ org.apache.dubbo
+ dubbo-rpc-triple
+ ${project.version}
+ test-jar
+ test
+
+
+ com.google.protobuf
+ protobuf-java-util
+ test
+
+
+ org.spockframework
+ spock-core
+ test
+
+
+ org.apache.logging.log4j
+ log4j-slf4j-impl
+ test
+
+
+ org.apache.dubbo
+ dubbo-spring-boot-starter
+ ${project.version}
+ test
+
+
+ org.apache.dubbo
+ dubbo-spring-boot-3-autoconfigure
+ ${project.version}
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+ 17
+ 17
+ 17
+
+
+
+ org.codehaus.gmavenplus
+ gmavenplus-plugin
+
+
+
+ compileTests
+
+
+
+
+
+
+
diff --git a/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/AbstractSpringArgumentResolver.java b/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/AbstractSpringArgumentResolver.java
new file mode 100644
index 000000000000..70421f7e2c26
--- /dev/null
+++ b/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/AbstractSpringArgumentResolver.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.protocol.tri.rest.support.spring;
+
+import org.apache.dubbo.common.utils.StringUtils;
+import org.apache.dubbo.rpc.protocol.tri.rest.argument.AbstractAnnotationBaseArgumentResolver;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.AnnotationMeta;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.NamedValueMeta;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.ParameterMeta;
+
+import java.lang.annotation.Annotation;
+import java.util.Optional;
+
+public abstract class AbstractSpringArgumentResolver extends AbstractAnnotationBaseArgumentResolver {
+
+ @Override
+ protected NamedValueMeta createNamedValueMeta(ParameterMeta param, AnnotationMeta anno) {
+ return new NamedValueMeta(
+ anno.getValue(),
+ param.getType() != Optional.class && Helper.isRequired(anno),
+ Helper.defaultValue(anno));
+ }
+
+ @Override
+ protected Object filterValue(Object value, NamedValueMeta meta) {
+ return StringUtils.EMPTY_STRING.equals(value) ? meta.defaultValue() : value;
+ }
+}
diff --git a/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/Annotations.java b/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/Annotations.java
new file mode 100644
index 000000000000..17936d09cee9
--- /dev/null
+++ b/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/Annotations.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.protocol.tri.rest.support.spring;
+
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.AnnotationEnum;
+
+import java.lang.annotation.Annotation;
+
+public enum Annotations implements AnnotationEnum {
+ RequestMapping,
+ PathVariable,
+ MatrixVariable,
+ RequestParam,
+ RequestHeader,
+ CookieValue,
+ RequestAttribute,
+ RequestPart,
+ RequestBody,
+ ModelAttribute,
+ BindParam,
+ ResponseStatus,
+ CrossOrigin,
+ ExceptionHandler,
+ HttpExchange("org.springframework.web.service.annotation.HttpExchange"),
+ Nonnull("javax.annotation.Nonnull");
+
+ private final String className;
+ private Class type;
+
+ Annotations() {
+ className = "org.springframework.web.bind.annotation." + name();
+ }
+
+ Annotations(String className) {
+ this.className = className;
+ }
+
+ @Override
+ public String className() {
+ return className;
+ }
+
+ @Override
+ public Class type() {
+ if (type == null) {
+ type = loadType();
+ }
+ return type;
+ }
+}
diff --git a/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/BeanArgumentBinder.java b/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/BeanArgumentBinder.java
new file mode 100644
index 000000000000..6f721ddfe4be
--- /dev/null
+++ b/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/BeanArgumentBinder.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.protocol.tri.rest.support.spring;
+
+import org.apache.dubbo.common.utils.CollectionUtils;
+import org.apache.dubbo.common.utils.StringUtils;
+import org.apache.dubbo.remoting.http12.HttpRequest;
+import org.apache.dubbo.remoting.http12.HttpResponse;
+import org.apache.dubbo.rpc.protocol.tri.rest.Messages;
+import org.apache.dubbo.rpc.protocol.tri.rest.RestException;
+import org.apache.dubbo.rpc.protocol.tri.rest.argument.ArgumentResolver;
+import org.apache.dubbo.rpc.protocol.tri.rest.argument.CompositeArgumentResolver;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.BeanMeta.ConstructorMeta;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.ParameterMeta;
+import org.apache.dubbo.rpc.protocol.tri.rest.util.RequestUtils;
+
+import java.lang.reflect.Modifier;
+import java.util.Map;
+
+import org.springframework.beans.MutablePropertyValues;
+import org.springframework.core.convert.ConversionService;
+import org.springframework.validation.BindException;
+import org.springframework.validation.BindingResult;
+import org.springframework.validation.DataBinder;
+import org.springframework.web.bind.WebDataBinder;
+
+import static org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.BeanMeta.resolveConstructor;
+
+final class BeanArgumentBinder {
+
+ private static final Map, ConstructorMeta> CACHE = CollectionUtils.newConcurrentHashMap();
+
+ private final ArgumentResolver argumentResolver;
+ private final ConversionService conversionService;
+
+ BeanArgumentBinder(CompositeArgumentResolver argumentResolver, ConversionService conversionService) {
+ this.argumentResolver = argumentResolver;
+ this.conversionService = conversionService;
+ }
+
+ public Object bind(ParameterMeta paramMeta, HttpRequest request, HttpResponse response) {
+ String name = StringUtils.defaultIf(paramMeta.getName(), DataBinder.DEFAULT_OBJECT_NAME);
+ try {
+ Object bean = createBean(paramMeta, request, response);
+ WebDataBinder binder = new WebDataBinder(bean, name);
+ binder.setConversionService(conversionService);
+ binder.bind(new MutablePropertyValues(RequestUtils.getParametersMap(request)));
+ BindingResult result = binder.getBindingResult();
+ if (result.hasErrors()) {
+ throw new BindException(result);
+ }
+ return binder.getTarget();
+ } catch (Exception e) {
+ throw new RestException(e, Messages.ARGUMENT_BIND_ERROR, name, paramMeta.getType());
+ }
+ }
+
+ private Object createBean(ParameterMeta paramMeta, HttpRequest request, HttpResponse response) {
+ Class> type = paramMeta.getActualType();
+ if (Modifier.isAbstract(type.getModifiers())) {
+ throw new IllegalStateException(Messages.ARGUMENT_COULD_NOT_RESOLVED.format(paramMeta.getDescription()));
+ }
+ ConstructorMeta ct = CACHE.computeIfAbsent(type, k -> {
+ try {
+ return resolveConstructor(paramMeta.getToolKit(), null, type);
+ } catch (IllegalArgumentException e) {
+ throw new IllegalStateException(Messages.ARGUMENT_COULD_NOT_RESOLVED.format(paramMeta.getDescription())
+ + ", " + e.getMessage());
+ }
+ });
+ ParameterMeta[] parameters = ct.getParameters();
+ int len = parameters.length;
+ if (len == 0) {
+ return ct.newInstance();
+ }
+ Object[] args = new Object[len];
+ for (int i = 0; i < len; i++) {
+ ParameterMeta parameter = parameters[i];
+ args[i] = parameter.isSimple() ? argumentResolver.resolve(parameter, request, response) : null;
+ }
+ return ct.newInstance(args);
+ }
+}
diff --git a/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/BindParamArgumentResolver.java b/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/BindParamArgumentResolver.java
new file mode 100644
index 000000000000..e783023245ca
--- /dev/null
+++ b/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/BindParamArgumentResolver.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.protocol.tri.rest.support.spring;
+
+import org.apache.dubbo.common.extension.Activate;
+import org.apache.dubbo.remoting.http12.HttpRequest;
+import org.apache.dubbo.remoting.http12.HttpResponse;
+import org.apache.dubbo.remoting.http12.rest.ParamType;
+import org.apache.dubbo.rpc.protocol.tri.rest.argument.AbstractAnnotationBaseArgumentResolver;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.AnnotationMeta;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.NamedValueMeta;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.ParameterMeta;
+import org.apache.dubbo.rpc.protocol.tri.rest.util.RequestUtils;
+
+import java.lang.annotation.Annotation;
+
+@Activate(onClass = "org.springframework.web.bind.annotation.BindParam")
+public final class BindParamArgumentResolver extends AbstractAnnotationBaseArgumentResolver {
+
+ @Override
+ public Class accept() {
+ return Annotations.BindParam.type();
+ }
+
+ @Override
+ protected ParamType getParamType(NamedValueMeta meta) {
+ return ParamType.Param;
+ }
+
+ @Override
+ protected NamedValueMeta createNamedValueMeta(ParameterMeta param, AnnotationMeta anno) {
+ return new NamedValueMeta(anno.getValue(), param.isAnnotated(Annotations.Nonnull));
+ }
+
+ @Override
+ protected Object resolveValue(NamedValueMeta meta, HttpRequest request, HttpResponse response) {
+ return request.parameter(meta.name());
+ }
+
+ @Override
+ protected Object resolveCollectionValue(NamedValueMeta meta, HttpRequest request, HttpResponse response) {
+ return request.parameterValues(meta.name());
+ }
+
+ @Override
+ protected Object resolveMapValue(NamedValueMeta meta, HttpRequest request, HttpResponse response) {
+ return RequestUtils.getParametersMap(request);
+ }
+}
diff --git a/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/ConfigurationWrapper.java b/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/ConfigurationWrapper.java
new file mode 100644
index 000000000000..be6d3f75ad78
--- /dev/null
+++ b/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/ConfigurationWrapper.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.protocol.tri.rest.support.spring;
+
+import org.apache.dubbo.common.config.Configuration;
+import org.apache.dubbo.rpc.model.ApplicationModel;
+
+import java.util.Properties;
+
+@SuppressWarnings("serial")
+public final class ConfigurationWrapper extends Properties {
+
+ private final Configuration configuration;
+
+ ConfigurationWrapper(ApplicationModel applicationModel) {
+ configuration = applicationModel.modelEnvironment().getConfiguration();
+ }
+
+ @Override
+ public String getProperty(String key) {
+ Object value = configuration.getProperty(key);
+ return value == null ? null : value.toString();
+ }
+
+ @Override
+ public Object get(Object key) {
+ return configuration.getProperty(key == null ? null : key.toString());
+ }
+}
diff --git a/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/CookieValueArgumentResolver.java b/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/CookieValueArgumentResolver.java
new file mode 100644
index 000000000000..83bb983ac078
--- /dev/null
+++ b/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/CookieValueArgumentResolver.java
@@ -0,0 +1,80 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.protocol.tri.rest.support.spring;
+
+import org.apache.dubbo.common.extension.Activate;
+import org.apache.dubbo.common.utils.CollectionUtils;
+import org.apache.dubbo.remoting.http12.HttpCookie;
+import org.apache.dubbo.remoting.http12.HttpRequest;
+import org.apache.dubbo.remoting.http12.HttpResponse;
+import org.apache.dubbo.remoting.http12.rest.ParamType;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.NamedValueMeta;
+
+import java.lang.annotation.Annotation;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+@Activate(onClass = "org.springframework.web.bind.annotation.CookieValue")
+public class CookieValueArgumentResolver extends AbstractSpringArgumentResolver {
+
+ @Override
+ public Class accept() {
+ return Annotations.CookieValue.type();
+ }
+
+ @Override
+ protected ParamType getParamType(NamedValueMeta meta) {
+ return ParamType.Cookie;
+ }
+
+ @Override
+ protected Object resolveValue(NamedValueMeta meta, HttpRequest request, HttpResponse response) {
+ return request.cookie(meta.name());
+ }
+
+ @Override
+ protected Object resolveCollectionValue(NamedValueMeta meta, HttpRequest request, HttpResponse response) {
+ Collection cookies = request.cookies();
+ if (cookies.isEmpty()) {
+ return Collections.emptyList();
+ }
+ String name = meta.name();
+ List result = new ArrayList<>(cookies.size());
+ for (HttpCookie cookie : cookies) {
+ if (name.equals(cookie.name())) {
+ result.add(cookie);
+ }
+ }
+ return result;
+ }
+
+ @Override
+ protected Object resolveMapValue(NamedValueMeta meta, HttpRequest request, HttpResponse response) {
+ Collection cookies = request.cookies();
+ if (cookies.isEmpty()) {
+ return Collections.emptyMap();
+ }
+ Map> mapValue = CollectionUtils.newLinkedHashMap(cookies.size());
+ for (HttpCookie cookie : cookies) {
+ mapValue.computeIfAbsent(cookie.name(), k -> new ArrayList<>()).add(cookie);
+ }
+ return mapValue;
+ }
+}
diff --git a/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/FallbackArgumentResolver.java b/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/FallbackArgumentResolver.java
new file mode 100644
index 000000000000..2f698435fb01
--- /dev/null
+++ b/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/FallbackArgumentResolver.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.protocol.tri.rest.support.spring;
+
+import org.apache.dubbo.common.extension.Activate;
+import org.apache.dubbo.remoting.http12.HttpRequest;
+import org.apache.dubbo.remoting.http12.HttpResponse;
+import org.apache.dubbo.rpc.protocol.tri.rest.RestConstants;
+import org.apache.dubbo.rpc.protocol.tri.rest.argument.AbstractArgumentResolver;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.NamedValueMeta;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.ParameterMeta;
+import org.apache.dubbo.rpc.protocol.tri.rest.util.RequestUtils;
+import org.apache.dubbo.rpc.protocol.tri.rest.util.TypeUtils;
+
+@Activate(order = Integer.MAX_VALUE - 10000, onClass = "org.springframework.web.bind.annotation.RequestMapping")
+public class FallbackArgumentResolver extends AbstractArgumentResolver {
+
+ @Override
+ public boolean accept(ParameterMeta param) {
+ return param.getToolKit().getDialect() == RestConstants.DIALECT_SPRING_MVC;
+ }
+
+ @Override
+ protected NamedValueMeta createNamedValueMeta(ParameterMeta param) {
+ return new NamedValueMeta(null, param.isAnnotated(Annotations.Nonnull));
+ }
+
+ @Override
+ protected Object resolveValue(NamedValueMeta meta, HttpRequest request, HttpResponse response) {
+ ParameterMeta parameter = meta.parameter();
+ if (parameter.isStream()) {
+ return null;
+ }
+ if (parameter.isSimple()) {
+ return request.parameter(meta.name());
+ }
+ return parameter.bind(request, response);
+ }
+
+ @Override
+ protected Object resolveCollectionValue(NamedValueMeta meta, HttpRequest request, HttpResponse response) {
+ if (TypeUtils.isSimpleProperty(meta.nestedType(0))) {
+ return request.parameterValues(meta.name());
+ }
+ return null;
+ }
+
+ @Override
+ protected Object resolveMapValue(NamedValueMeta meta, HttpRequest request, HttpResponse response) {
+ if (TypeUtils.isSimpleProperty(meta.nestedType(1))) {
+ return RequestUtils.getParametersMap(request);
+ }
+ return null;
+ }
+}
diff --git a/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/HandlerInterceptorAdapter.java b/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/HandlerInterceptorAdapter.java
new file mode 100644
index 000000000000..0fae287c2609
--- /dev/null
+++ b/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/HandlerInterceptorAdapter.java
@@ -0,0 +1,109 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.protocol.tri.rest.support.spring;
+
+import org.apache.dubbo.common.extension.Activate;
+import org.apache.dubbo.remoting.http12.HttpRequest;
+import org.apache.dubbo.remoting.http12.HttpResponse;
+import org.apache.dubbo.rpc.Result;
+import org.apache.dubbo.rpc.RpcException;
+import org.apache.dubbo.rpc.protocol.tri.rest.RestConstants;
+import org.apache.dubbo.rpc.protocol.tri.rest.filter.RestExtensionAdapter;
+import org.apache.dubbo.rpc.protocol.tri.rest.filter.RestFilter;
+import org.apache.dubbo.rpc.protocol.tri.rest.filter.RestFilter.Listener;
+import org.apache.dubbo.rpc.protocol.tri.rest.util.RestUtils;
+
+import java.util.Arrays;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.springframework.web.servlet.HandlerInterceptor;
+import org.springframework.web.servlet.ModelAndView;
+
+@Activate(onClass = {"org.springframework.web.servlet.HandlerInterceptor", "jakarta.servlet.http.HttpServletRequest"})
+public final class HandlerInterceptorAdapter implements RestExtensionAdapter {
+ @Override
+ public boolean accept(Object extension) {
+ return extension instanceof HandlerInterceptor;
+ }
+
+ @Override
+ public RestFilter adapt(HandlerInterceptor extension) {
+ return new HandlerInterceptorRestFilter(extension);
+ }
+
+ private static final class HandlerInterceptorRestFilter implements RestFilter, Listener {
+
+ private final HandlerInterceptor interceptor;
+
+ @Override
+ public int getPriority() {
+ return RestUtils.getPriority(interceptor);
+ }
+
+ @Override
+ public String[] getPatterns() {
+ return RestUtils.getPattens(interceptor);
+ }
+
+ public HandlerInterceptorRestFilter(HandlerInterceptor interceptor) {
+ this.interceptor = interceptor;
+ }
+
+ @Override
+ public void doFilter(HttpRequest request, HttpResponse response, FilterChain chain) throws Exception {
+ Object handler = request.attribute(RestConstants.HANDLER_ATTRIBUTE);
+ if (interceptor.preHandle((HttpServletRequest) request, (HttpServletResponse) response, handler)) {
+ chain.doFilter(request, response);
+ }
+ }
+
+ @Override
+ public void onResponse(Result result, HttpRequest request, HttpResponse response) throws Exception {
+ if (result.hasException()) {
+ onError(result.getException(), request, response);
+ return;
+ }
+ Object handler = request.attribute(RestConstants.HANDLER_ATTRIBUTE);
+ ModelAndView mv = new ModelAndView();
+ mv.addObject("result", result);
+ interceptor.postHandle((HttpServletRequest) request, (HttpServletResponse) response, handler, mv);
+ }
+
+ @Override
+ public void onError(Throwable t, HttpRequest request, HttpResponse response) throws Exception {
+ Object handler = request.attribute(RestConstants.HANDLER_ATTRIBUTE);
+ Exception ex = t instanceof Exception ? (Exception) t : new RpcException(t);
+ interceptor.afterCompletion((HttpServletRequest) request, (HttpServletResponse) response, handler, ex);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder("RestFilter{interceptor=");
+ sb.append(interceptor);
+ int priority = getPriority();
+ if (priority != 0) {
+ sb.append(", priority=").append(priority);
+ }
+ String[] patterns = getPatterns();
+ if (patterns != null) {
+ sb.append(", patterns=").append(Arrays.toString(patterns));
+ }
+ return sb.append('}').toString();
+ }
+ }
+}
diff --git a/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/Helper.java b/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/Helper.java
new file mode 100644
index 000000000000..223c3ab7e79c
--- /dev/null
+++ b/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/Helper.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.protocol.tri.rest.support.spring;
+
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.AnnotationMeta;
+
+import java.lang.annotation.Annotation;
+
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ValueConstants;
+
+final class Helper {
+
+ public static boolean IS_SPRING_6 = true;
+
+ private Helper() {}
+
+ public static boolean isRequired(AnnotationMeta annotation) {
+ return annotation.getBoolean("required");
+ }
+
+ public static String defaultValue(AnnotationMeta annotation) {
+ return defaultValue(annotation, "defaultValue");
+ }
+
+ public static String defaultValue(AnnotationMeta annotation, String name) {
+ return defaultValue(annotation.getString(name));
+ }
+
+ public static String defaultValue(String value) {
+ return ValueConstants.DEFAULT_NONE.equals(value) ? null : value;
+ }
+
+ public static int getStatusCode(ResponseEntity> entity) {
+ return entity.getStatusCode().value();
+ }
+}
diff --git a/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/MatrixVariableArgumentResolver.java b/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/MatrixVariableArgumentResolver.java
new file mode 100644
index 000000000000..b894cfde354b
--- /dev/null
+++ b/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/MatrixVariableArgumentResolver.java
@@ -0,0 +1,134 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.protocol.tri.rest.support.spring;
+
+import org.apache.dubbo.common.extension.Activate;
+import org.apache.dubbo.common.utils.CollectionUtils;
+import org.apache.dubbo.remoting.http12.HttpRequest;
+import org.apache.dubbo.remoting.http12.HttpResponse;
+import org.apache.dubbo.remoting.http12.rest.ParamType;
+import org.apache.dubbo.rpc.protocol.tri.rest.RestConstants;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.AnnotationMeta;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.NamedValueMeta;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.ParameterMeta;
+import org.apache.dubbo.rpc.protocol.tri.rest.util.RequestUtils;
+
+import java.lang.annotation.Annotation;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Activate(onClass = "org.springframework.web.bind.annotation.MatrixVariable")
+public class MatrixVariableArgumentResolver extends AbstractSpringArgumentResolver {
+
+ @Override
+ public Class accept() {
+ return Annotations.MatrixVariable.type();
+ }
+
+ @Override
+ protected ParamType getParamType(NamedValueMeta meta) {
+ return ParamType.MatrixVariable;
+ }
+
+ @Override
+ protected NamedValueMeta createNamedValueMeta(ParameterMeta param, AnnotationMeta anno) {
+ return new MatrixNamedValueMeta(
+ anno.getValue(),
+ Helper.isRequired(anno),
+ Helper.defaultValue(anno),
+ Helper.defaultValue(anno, "pathVar"));
+ }
+
+ @Override
+ protected Object resolveValue(NamedValueMeta meta, HttpRequest request, HttpResponse response) {
+ return CollectionUtils.first(doResolveCollectionValue(meta, request));
+ }
+
+ @Override
+ protected Object resolveCollectionValue(NamedValueMeta meta, HttpRequest request, HttpResponse response) {
+ return doResolveCollectionValue(meta, request);
+ }
+
+ private static List doResolveCollectionValue(NamedValueMeta meta, HttpRequest request) {
+ String name = meta.name();
+ Map variableMap = request.attribute(RestConstants.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
+ if (variableMap == null) {
+ return Collections.emptyList();
+ }
+ List result = null;
+ String pathVar = ((MatrixNamedValueMeta) meta).pathVar;
+ if (pathVar == null) {
+ result = RequestUtils.parseMatrixVariableValues(variableMap, name);
+ } else {
+ String value = variableMap.get(pathVar);
+ if (value != null) {
+ Map> matrixVariables = RequestUtils.parseMatrixVariables(value);
+ if (matrixVariables != null) {
+ List values = matrixVariables.get(name);
+ if (values != null) {
+ return values;
+ }
+ }
+ }
+ }
+ return result == null ? Collections.emptyList() : result;
+ }
+
+ @Override
+ protected Object resolveMapValue(NamedValueMeta meta, HttpRequest request, HttpResponse response) {
+ Map variableMap = request.attribute(RestConstants.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
+ if (variableMap == null) {
+ return Collections.emptyMap();
+ }
+ Map> result = null;
+ String pathVar = ((MatrixNamedValueMeta) meta).pathVar;
+ if (pathVar == null) {
+ for (Map.Entry entry : variableMap.entrySet()) {
+ Map> matrixVariables = RequestUtils.parseMatrixVariables(entry.getValue());
+ if (matrixVariables == null) {
+ continue;
+ }
+ if (result == null) {
+ result = new HashMap<>();
+ }
+ for (Map.Entry> matrixEntry : matrixVariables.entrySet()) {
+ result.computeIfAbsent(matrixEntry.getKey(), k -> new ArrayList<>())
+ .addAll(matrixEntry.getValue());
+ }
+ }
+ } else {
+ String value = variableMap.get(pathVar);
+ if (value != null) {
+ result = RequestUtils.parseMatrixVariables(value);
+ }
+ }
+ return result == null ? Collections.emptyMap() : result;
+ }
+
+ private static class MatrixNamedValueMeta extends NamedValueMeta {
+
+ private final String pathVar;
+
+ MatrixNamedValueMeta(String name, boolean required, String defaultValue, String pathVar) {
+ super(name, required, defaultValue);
+ this.pathVar = pathVar;
+ }
+ }
+}
diff --git a/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/ModelAttributeArgumentResolver.java b/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/ModelAttributeArgumentResolver.java
new file mode 100644
index 000000000000..56ce0f798f01
--- /dev/null
+++ b/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/ModelAttributeArgumentResolver.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.protocol.tri.rest.support.spring;
+
+import org.apache.dubbo.common.extension.Activate;
+import org.apache.dubbo.remoting.http12.HttpRequest;
+import org.apache.dubbo.remoting.http12.HttpResponse;
+import org.apache.dubbo.remoting.http12.rest.ParamType;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.NamedValueMeta;
+import org.apache.dubbo.rpc.protocol.tri.rest.util.RequestUtils;
+import org.apache.dubbo.rpc.protocol.tri.rest.util.TypeUtils;
+
+import java.lang.annotation.Annotation;
+
+@Activate(onClass = "org.springframework.web.bind.annotation.ModelAttribute")
+public final class ModelAttributeArgumentResolver extends AbstractSpringArgumentResolver {
+
+ @Override
+ public Class accept() {
+ return Annotations.ModelAttribute.type();
+ }
+
+ @Override
+ protected ParamType getParamType(NamedValueMeta meta) {
+ return ParamType.Param;
+ }
+
+ @Override
+ protected Object resolveValue(NamedValueMeta meta, HttpRequest request, HttpResponse response) {
+ if (meta.parameter().isSimple()) {
+ return request.parameter(meta.name());
+ }
+ return meta.parameter().bind(request, response);
+ }
+
+ @Override
+ protected Object resolveCollectionValue(NamedValueMeta meta, HttpRequest request, HttpResponse response) {
+ if (meta.parameter().isSimple()) {
+ return request.parameterValues(meta.name());
+ }
+ return meta.parameter().bind(request, response);
+ }
+
+ @Override
+ protected Object resolveMapValue(NamedValueMeta meta, HttpRequest request, HttpResponse response) {
+ if (TypeUtils.isSimpleProperty(meta.nestedType(1))) {
+ return RequestUtils.getParametersMap(request);
+ }
+ return meta.parameter().bind(request, response);
+ }
+}
diff --git a/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/MultiValueMapCreator.java b/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/MultiValueMapCreator.java
new file mode 100644
index 000000000000..1b21612e352d
--- /dev/null
+++ b/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/MultiValueMapCreator.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.protocol.tri.rest.support.spring;
+
+import org.apache.dubbo.common.extension.Activate;
+import org.apache.dubbo.rpc.protocol.tri.rest.argument.ArgumentConverter;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.ParameterMeta;
+
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+
+@Activate(onClass = "org.springframework.util.MultiValueMap")
+public class MultiValueMapCreator implements ArgumentConverter> {
+
+ @Override
+ public MultiValueMap, ?> convert(Integer value, ParameterMeta parameter) {
+ return new LinkedMultiValueMap<>(value);
+ }
+}
diff --git a/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/PathVariableArgumentResolver.java b/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/PathVariableArgumentResolver.java
new file mode 100644
index 000000000000..32cc2bbfc6f6
--- /dev/null
+++ b/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/PathVariableArgumentResolver.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.protocol.tri.rest.support.spring;
+
+import org.apache.dubbo.common.extension.Activate;
+import org.apache.dubbo.common.utils.StringUtils;
+import org.apache.dubbo.remoting.http12.HttpRequest;
+import org.apache.dubbo.remoting.http12.HttpResponse;
+import org.apache.dubbo.remoting.http12.rest.ParamType;
+import org.apache.dubbo.rpc.protocol.tri.rest.Messages;
+import org.apache.dubbo.rpc.protocol.tri.rest.RestConstants;
+import org.apache.dubbo.rpc.protocol.tri.rest.RestParameterException;
+import org.apache.dubbo.rpc.protocol.tri.rest.argument.AnnotationBaseArgumentResolver;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.AnnotationMeta;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.NamedValueMeta;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.ParameterMeta;
+import org.apache.dubbo.rpc.protocol.tri.rest.util.RequestUtils;
+
+import java.lang.annotation.Annotation;
+import java.util.Map;
+
+@Activate(onClass = "org.springframework.web.bind.annotation.PathVariable")
+public class PathVariableArgumentResolver implements AnnotationBaseArgumentResolver {
+
+ @Override
+ public Class accept() {
+ return Annotations.PathVariable.type();
+ }
+
+ @Override
+ public NamedValueMeta getNamedValueMeta(ParameterMeta parameter, AnnotationMeta annotation) {
+ return new NamedValueMeta(annotation.getValue(), true).setParamType(ParamType.PathVariable);
+ }
+
+ @Override
+ public Object resolve(
+ ParameterMeta parameter,
+ AnnotationMeta annotation,
+ HttpRequest request,
+ HttpResponse response) {
+ Map variableMap = request.attribute(RestConstants.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
+ String name = annotation.getValue();
+ if (StringUtils.isEmpty(name)) {
+ name = parameter.getRequiredName();
+ }
+ if (variableMap == null) {
+ if (Helper.isRequired(annotation)) {
+ throw new RestParameterException(Messages.ARGUMENT_VALUE_MISSING, name, parameter.getType());
+ }
+ return null;
+ }
+ String value = variableMap.get(name);
+ if (value == null) {
+ return null;
+ }
+ int index = value.indexOf(';');
+ return RequestUtils.decodeURL(index == -1 ? value : value.substring(0, index));
+ }
+}
diff --git a/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/RequestAttributeArgumentResolver.java b/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/RequestAttributeArgumentResolver.java
new file mode 100644
index 000000000000..d0523aa865f0
--- /dev/null
+++ b/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/RequestAttributeArgumentResolver.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.protocol.tri.rest.support.spring;
+
+import org.apache.dubbo.common.extension.Activate;
+import org.apache.dubbo.remoting.http12.HttpRequest;
+import org.apache.dubbo.remoting.http12.HttpResponse;
+import org.apache.dubbo.remoting.http12.rest.ParamType;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.NamedValueMeta;
+
+import java.lang.annotation.Annotation;
+
+@Activate(onClass = "org.springframework.web.bind.annotation.RequestAttribute")
+public class RequestAttributeArgumentResolver extends AbstractSpringArgumentResolver {
+
+ @Override
+ public Class accept() {
+ return Annotations.RequestAttribute.type();
+ }
+
+ @Override
+ protected ParamType getParamType(NamedValueMeta meta) {
+ return ParamType.Attribute;
+ }
+
+ @Override
+ protected Object resolveValue(NamedValueMeta meta, HttpRequest request, HttpResponse response) {
+ return request.attribute(meta.name());
+ }
+
+ @Override
+ protected Object resolveMapValue(NamedValueMeta meta, HttpRequest request, HttpResponse response) {
+ return request.attributes();
+ }
+}
diff --git a/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/RequestBodyArgumentResolver.java b/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/RequestBodyArgumentResolver.java
new file mode 100644
index 000000000000..9ae5a7a21586
--- /dev/null
+++ b/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/RequestBodyArgumentResolver.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.protocol.tri.rest.support.spring;
+
+import org.apache.dubbo.common.extension.Activate;
+import org.apache.dubbo.common.io.StreamUtils;
+import org.apache.dubbo.remoting.http12.HttpRequest;
+import org.apache.dubbo.remoting.http12.HttpResponse;
+import org.apache.dubbo.remoting.http12.rest.ParamType;
+import org.apache.dubbo.rpc.protocol.tri.rest.RestException;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.AnnotationMeta;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.NamedValueMeta;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.ParameterMeta;
+import org.apache.dubbo.rpc.protocol.tri.rest.util.RequestUtils;
+
+import java.io.IOException;
+import java.lang.annotation.Annotation;
+
+@Activate(onClass = "org.springframework.web.bind.annotation.RequestBody")
+public class RequestBodyArgumentResolver extends AbstractSpringArgumentResolver {
+
+ @Override
+ public Class accept() {
+ return Annotations.RequestBody.type();
+ }
+
+ @Override
+ protected ParamType getParamType(NamedValueMeta meta) {
+ return ParamType.Body;
+ }
+
+ @Override
+ protected NamedValueMeta createNamedValueMeta(ParameterMeta param, AnnotationMeta anno) {
+ return new NamedValueMeta(null, Helper.isRequired(anno));
+ }
+
+ @Override
+ protected Object resolveValue(NamedValueMeta meta, HttpRequest request, HttpResponse response) {
+ if (RequestUtils.isFormOrMultiPart(request)) {
+ if (meta.parameter().isSimple()) {
+ return request.formParameter(meta.name());
+ }
+ return meta.parameter().bind(request, response);
+ }
+ return RequestUtils.decodeBody(request, meta.genericType());
+ }
+
+ @Override
+ protected Object resolveCollectionValue(NamedValueMeta meta, HttpRequest request, HttpResponse response) {
+ Class> type = meta.type();
+ if (type == byte[].class) {
+ try {
+ return StreamUtils.readBytes(request.inputStream());
+ } catch (IOException e) {
+ throw new RestException(e);
+ }
+ }
+ if (RequestUtils.isFormOrMultiPart(request)) {
+ return request.formParameterValues(meta.name());
+ }
+ return RequestUtils.decodeBody(request, meta.genericType());
+ }
+
+ @Override
+ protected Object resolveMapValue(NamedValueMeta meta, HttpRequest request, HttpResponse response) {
+ if (RequestUtils.isFormOrMultiPart(request)) {
+ return RequestUtils.getFormParametersMap(request);
+ }
+ return RequestUtils.decodeBody(request, meta.genericType());
+ }
+}
diff --git a/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/RequestHeaderArgumentResolver.java b/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/RequestHeaderArgumentResolver.java
new file mode 100644
index 000000000000..c93ae084c9aa
--- /dev/null
+++ b/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/RequestHeaderArgumentResolver.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.protocol.tri.rest.support.spring;
+
+import org.apache.dubbo.common.extension.Activate;
+import org.apache.dubbo.remoting.http12.HttpRequest;
+import org.apache.dubbo.remoting.http12.HttpResponse;
+import org.apache.dubbo.remoting.http12.rest.ParamType;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.NamedValueMeta;
+
+import java.lang.annotation.Annotation;
+
+@Activate(onClass = "org.springframework.web.bind.annotation.RequestHeader")
+public final class RequestHeaderArgumentResolver extends AbstractSpringArgumentResolver {
+
+ @Override
+ public Class accept() {
+ return Annotations.RequestHeader.type();
+ }
+
+ @Override
+ protected ParamType getParamType(NamedValueMeta meta) {
+ return ParamType.Header;
+ }
+
+ @Override
+ protected Object resolveValue(NamedValueMeta meta, HttpRequest request, HttpResponse response) {
+ return request.header(meta.name());
+ }
+
+ @Override
+ protected Object resolveCollectionValue(NamedValueMeta meta, HttpRequest request, HttpResponse response) {
+ return request.headerValues(meta.name());
+ }
+
+ @Override
+ protected Object resolveMapValue(NamedValueMeta meta, HttpRequest request, HttpResponse response) {
+ return request.headers().asMap();
+ }
+}
diff --git a/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/RequestParamArgumentResolver.java b/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/RequestParamArgumentResolver.java
new file mode 100644
index 000000000000..21889890e8b3
--- /dev/null
+++ b/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/RequestParamArgumentResolver.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.protocol.tri.rest.support.spring;
+
+import org.apache.dubbo.common.extension.Activate;
+import org.apache.dubbo.remoting.http12.HttpRequest;
+import org.apache.dubbo.remoting.http12.HttpResponse;
+import org.apache.dubbo.remoting.http12.rest.ParamType;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.NamedValueMeta;
+import org.apache.dubbo.rpc.protocol.tri.rest.util.RequestUtils;
+
+import java.lang.annotation.Annotation;
+
+@Activate(onClass = "org.springframework.web.bind.annotation.RequestParam")
+public final class RequestParamArgumentResolver extends AbstractSpringArgumentResolver {
+
+ @Override
+ public Class accept() {
+ return Annotations.RequestParam.type();
+ }
+
+ @Override
+ protected ParamType getParamType(NamedValueMeta meta) {
+ return ParamType.Param;
+ }
+
+ @Override
+ protected Object resolveValue(NamedValueMeta meta, HttpRequest request, HttpResponse response) {
+ return request.parameter(meta.name());
+ }
+
+ @Override
+ protected Object resolveCollectionValue(NamedValueMeta meta, HttpRequest request, HttpResponse response) {
+ return request.parameterValues(meta.name());
+ }
+
+ @Override
+ protected Object resolveMapValue(NamedValueMeta meta, HttpRequest request, HttpResponse response) {
+ return RequestUtils.getParametersMap(request);
+ }
+}
diff --git a/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/RequestPartArgumentResolver.java b/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/RequestPartArgumentResolver.java
new file mode 100644
index 000000000000..834807d0f74e
--- /dev/null
+++ b/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/RequestPartArgumentResolver.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.protocol.tri.rest.support.spring;
+
+import org.apache.dubbo.common.extension.Activate;
+import org.apache.dubbo.remoting.http12.HttpRequest;
+import org.apache.dubbo.remoting.http12.HttpRequest.FileUpload;
+import org.apache.dubbo.remoting.http12.HttpResponse;
+import org.apache.dubbo.remoting.http12.rest.ParamType;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.AnnotationMeta;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.NamedValueMeta;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.ParameterMeta;
+
+import java.lang.annotation.Annotation;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+@Activate(onClass = "org.springframework.web.bind.annotation.RequestPart")
+public class RequestPartArgumentResolver extends AbstractSpringArgumentResolver {
+
+ @Override
+ public Class accept() {
+ return Annotations.RequestPart.type();
+ }
+
+ @Override
+ protected ParamType getParamType(NamedValueMeta meta) {
+ return ParamType.Part;
+ }
+
+ @Override
+ protected NamedValueMeta createNamedValueMeta(ParameterMeta param, AnnotationMeta anno) {
+ return new NamedValueMeta(anno.getValue(), Helper.isRequired(anno));
+ }
+
+ @Override
+ protected Object resolveValue(NamedValueMeta meta, HttpRequest request, HttpResponse response) {
+ return request.part(meta.name());
+ }
+
+ @Override
+ protected Object resolveCollectionValue(NamedValueMeta meta, HttpRequest request, HttpResponse response) {
+ return meta.type() == byte[].class ? request.part(meta.name()) : request.parts();
+ }
+
+ @Override
+ protected Object resolveMapValue(NamedValueMeta meta, HttpRequest request, HttpResponse response) {
+ Collection parts = request.parts();
+ if (parts.isEmpty()) {
+ return Collections.emptyMap();
+ }
+ Map result = new LinkedHashMap<>(parts.size());
+ for (FileUpload part : parts) {
+ result.put(part.name(), part);
+ }
+ return result;
+ }
+}
diff --git a/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/RestSpringScopeModelInitializer.java b/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/RestSpringScopeModelInitializer.java
new file mode 100644
index 000000000000..bca5c0dd4334
--- /dev/null
+++ b/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/RestSpringScopeModelInitializer.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.protocol.tri.rest.support.spring;
+
+import org.apache.dubbo.common.beans.factory.ScopeBeanFactory;
+import org.apache.dubbo.common.utils.DefaultParameterNameReader;
+import org.apache.dubbo.rpc.model.FrameworkModel;
+import org.apache.dubbo.rpc.model.ScopeModelInitializer;
+import org.apache.dubbo.rpc.protocol.tri.rest.argument.CompositeArgumentResolver;
+import org.apache.dubbo.rpc.protocol.tri.rest.argument.GeneralTypeConverter;
+
+public class RestSpringScopeModelInitializer implements ScopeModelInitializer {
+ @Override
+ public void initializeFrameworkModel(FrameworkModel frameworkModel) {
+ ScopeBeanFactory beanFactory = frameworkModel.getBeanFactory();
+ beanFactory.registerBean(GeneralTypeConverter.class);
+ beanFactory.registerBean(DefaultParameterNameReader.class);
+ beanFactory.registerBean(CompositeArgumentResolver.class);
+ }
+}
diff --git a/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/SpringMiscArgumentResolver.java b/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/SpringMiscArgumentResolver.java
new file mode 100644
index 000000000000..7de7d7fa5929
--- /dev/null
+++ b/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/SpringMiscArgumentResolver.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.protocol.tri.rest.support.spring;
+
+import org.apache.dubbo.common.extension.Activate;
+import org.apache.dubbo.remoting.http12.HttpRequest;
+import org.apache.dubbo.remoting.http12.HttpResponse;
+import org.apache.dubbo.rpc.protocol.tri.rest.argument.ArgumentResolver;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.ParameterMeta;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import jakarta.servlet.http.HttpServletRequest;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.util.CollectionUtils;
+import org.springframework.web.context.request.NativeWebRequest;
+import org.springframework.web.context.request.ServletWebRequest;
+import org.springframework.web.context.request.WebRequest;
+
+@Activate(onClass = {"org.springframework.web.context.request.WebRequest", "jakarta.servlet.http.HttpServletRequest"})
+public class SpringMiscArgumentResolver implements ArgumentResolver {
+
+ private static final Set> SUPPORTED_TYPES = new HashSet<>();
+
+ static {
+ SUPPORTED_TYPES.add(WebRequest.class);
+ SUPPORTED_TYPES.add(NativeWebRequest.class);
+ SUPPORTED_TYPES.add(HttpEntity.class);
+ SUPPORTED_TYPES.add(HttpHeaders.class);
+ }
+
+ @Override
+ public boolean accept(ParameterMeta parameter) {
+ return SUPPORTED_TYPES.contains(parameter.getActualType());
+ }
+
+ @Override
+ public Object resolve(ParameterMeta parameter, HttpRequest request, HttpResponse response) {
+ Class> type = parameter.getActualType();
+ if (type == WebRequest.class || type == NativeWebRequest.class) {
+ return new ServletWebRequest((HttpServletRequest) request);
+ }
+ if (type == HttpEntity.class) {
+ return new HttpEntity<>(
+ CollectionUtils.toMultiValueMap(request.headers().asMap()));
+ }
+ if (type == HttpHeaders.class) {
+ return new HttpHeaders(
+ CollectionUtils.toMultiValueMap(request.headers().asMap()));
+ }
+ return null;
+ }
+}
diff --git a/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/SpringMvcRequestMappingResolver.java b/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/SpringMvcRequestMappingResolver.java
new file mode 100644
index 000000000000..e1db874608af
--- /dev/null
+++ b/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/SpringMvcRequestMappingResolver.java
@@ -0,0 +1,155 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.protocol.tri.rest.support.spring;
+
+import org.apache.dubbo.common.constants.CommonConstants;
+import org.apache.dubbo.common.extension.Activate;
+import org.apache.dubbo.common.utils.StringUtils;
+import org.apache.dubbo.config.nested.RestConfig;
+import org.apache.dubbo.rpc.model.FrameworkModel;
+import org.apache.dubbo.rpc.protocol.tri.rest.cors.CorsUtils;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.RequestMapping;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.RequestMapping.Builder;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.RequestMappingResolver;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.AnnotationMeta;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.CorsMeta;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.MethodMeta;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.ServiceMeta;
+import org.apache.dubbo.rpc.protocol.tri.rest.util.RestToolKit;
+
+import org.springframework.http.HttpStatus;
+
+@Activate(onClass = "org.springframework.web.bind.annotation.RequestMapping")
+public class SpringMvcRequestMappingResolver implements RequestMappingResolver {
+
+ private final RestToolKit toolKit;
+ private RestConfig restConfig;
+ private CorsMeta globalCorsMeta;
+
+ public SpringMvcRequestMappingResolver(FrameworkModel frameworkModel) {
+ toolKit = new SpringRestToolKit(frameworkModel);
+ }
+
+ @Override
+ public void setRestConfig(RestConfig restConfig) {
+ this.restConfig = restConfig;
+ }
+
+ @Override
+ public RestToolKit getRestToolKit() {
+ return toolKit;
+ }
+
+ @Override
+ public RequestMapping resolve(ServiceMeta serviceMeta) {
+ AnnotationMeta> requestMapping = serviceMeta.findMergedAnnotation(Annotations.RequestMapping);
+ AnnotationMeta> httpExchange = serviceMeta.findMergedAnnotation(Annotations.HttpExchange);
+ if (requestMapping == null && httpExchange == null) {
+ return null;
+ }
+
+ String[] methods = requestMapping == null
+ ? httpExchange.getStringArray("method")
+ : requestMapping.getStringArray("method");
+ String[] paths = requestMapping == null ? httpExchange.getValueArray() : requestMapping.getValueArray();
+ return builder(requestMapping, httpExchange, serviceMeta.findMergedAnnotation(Annotations.ResponseStatus))
+ .method(methods)
+ .name(serviceMeta.getType().getSimpleName())
+ .path(paths)
+ .contextPath(serviceMeta.getContextPath())
+ .cors(buildCorsMeta(serviceMeta.findMergedAnnotation(Annotations.CrossOrigin), methods))
+ .build();
+ }
+
+ @Override
+ public RequestMapping resolve(MethodMeta methodMeta) {
+ AnnotationMeta> requestMapping = methodMeta.findMergedAnnotation(Annotations.RequestMapping);
+ AnnotationMeta> httpExchange = methodMeta.findMergedAnnotation(Annotations.HttpExchange);
+ if (requestMapping == null && httpExchange == null) {
+ AnnotationMeta> exceptionHandler = methodMeta.getAnnotation(Annotations.ExceptionHandler);
+ if (exceptionHandler != null) {
+ methodMeta.initParameters();
+ methodMeta.getServiceMeta().addExceptionHandler(methodMeta);
+ }
+ return null;
+ }
+
+ ServiceMeta serviceMeta = methodMeta.getServiceMeta();
+ String name = methodMeta.getMethodName();
+ String[] methods = requestMapping == null
+ ? httpExchange.getStringArray("method")
+ : requestMapping.getStringArray("method");
+ String[] paths = requestMapping == null ? httpExchange.getValueArray() : requestMapping.getValueArray();
+ if (paths.length == 0) {
+ paths = new String[] {'/' + name};
+ }
+ return builder(requestMapping, httpExchange, methodMeta.findMergedAnnotation(Annotations.ResponseStatus))
+ .method(methods)
+ .name(name)
+ .path(paths)
+ .contextPath(serviceMeta.getContextPath())
+ .service(serviceMeta.getServiceGroup(), serviceMeta.getServiceVersion())
+ .cors(buildCorsMeta(methodMeta.findMergedAnnotation(Annotations.CrossOrigin), methods))
+ .build();
+ }
+
+ private Builder builder(
+ AnnotationMeta> requestMapping, AnnotationMeta> httpExchange, AnnotationMeta> responseStatus) {
+ Builder builder = RequestMapping.builder();
+ if (responseStatus != null) {
+ HttpStatus value = responseStatus.getEnum("value");
+ builder.responseStatus(value.value());
+ String reason = responseStatus.getString("reason");
+ if (StringUtils.isNotEmpty(reason)) {
+ builder.responseReason(reason);
+ }
+ }
+ if (requestMapping == null) {
+ return builder.consume(httpExchange.getStringArray("contentType"))
+ .produce(httpExchange.getStringArray("accept"));
+ }
+ return builder.param(requestMapping.getStringArray("params"))
+ .header(requestMapping.getStringArray("headers"))
+ .consume(requestMapping.getStringArray("consumes"))
+ .produce(requestMapping.getStringArray("produces"));
+ }
+
+ private CorsMeta buildCorsMeta(AnnotationMeta> crossOrigin, String[] methods) {
+ if (globalCorsMeta == null) {
+ globalCorsMeta = CorsUtils.getGlobalCorsMeta(restConfig);
+ }
+ if (crossOrigin == null) {
+ return globalCorsMeta;
+ }
+ String[] allowedMethods = crossOrigin.getStringArray("methods");
+ if (allowedMethods.length == 0) {
+ allowedMethods = methods;
+ if (allowedMethods.length == 0) {
+ allowedMethods = new String[] {CommonConstants.ANY_VALUE};
+ }
+ }
+ CorsMeta corsMeta = CorsMeta.builder()
+ .allowedOrigins(crossOrigin.getStringArray("origins"))
+ .allowedMethods(allowedMethods)
+ .allowedHeaders(crossOrigin.getStringArray("allowedHeaders"))
+ .exposedHeaders(crossOrigin.getStringArray("exposedHeaders"))
+ .allowCredentials(crossOrigin.getString("allowCredentials"))
+ .maxAge(crossOrigin.getNumber("maxAge"))
+ .build();
+ return globalCorsMeta.combine(corsMeta);
+ }
+}
diff --git a/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/SpringResponseRestFilter.java b/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/SpringResponseRestFilter.java
new file mode 100644
index 000000000000..0610848b95c8
--- /dev/null
+++ b/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/SpringResponseRestFilter.java
@@ -0,0 +1,202 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.protocol.tri.rest.support.spring;
+
+import org.apache.dubbo.common.extension.Activate;
+import org.apache.dubbo.common.utils.CollectionUtils;
+import org.apache.dubbo.common.utils.Pair;
+import org.apache.dubbo.common.utils.StringUtils;
+import org.apache.dubbo.remoting.http12.HttpRequest;
+import org.apache.dubbo.remoting.http12.HttpResponse;
+import org.apache.dubbo.remoting.http12.HttpResult;
+import org.apache.dubbo.rpc.Result;
+import org.apache.dubbo.rpc.model.FrameworkModel;
+import org.apache.dubbo.rpc.protocol.tri.rest.RestConstants;
+import org.apache.dubbo.rpc.protocol.tri.rest.RestException;
+import org.apache.dubbo.rpc.protocol.tri.rest.argument.ArgumentResolver;
+import org.apache.dubbo.rpc.protocol.tri.rest.argument.CompositeArgumentResolver;
+import org.apache.dubbo.rpc.protocol.tri.rest.filter.RestFilter;
+import org.apache.dubbo.rpc.protocol.tri.rest.filter.RestFilter.Listener;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.RequestMapping;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.AnnotationMeta;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.HandlerMeta;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.MethodMeta;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.ParameterMeta;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.ResponseMeta;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.ServiceMeta;
+
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@Activate(order = -10000, onClass = "org.springframework.http.ResponseEntity")
+public class SpringResponseRestFilter implements RestFilter, Listener {
+
+ private final ArgumentResolver argumentResolver;
+ private final Map> cache = CollectionUtils.newConcurrentHashMap();
+
+ public SpringResponseRestFilter(FrameworkModel frameworkModel) {
+ argumentResolver = frameworkModel.getOrRegisterBean(CompositeArgumentResolver.class);
+ }
+
+ @Override
+ public void onResponse(Result result, HttpRequest request, HttpResponse response) {
+ if (result.hasException()) {
+ HandlerMeta handler = request.attribute(RestConstants.HANDLER_ATTRIBUTE);
+ if (handler == null) {
+ return;
+ }
+
+ Throwable t = result.getException();
+ Key key = new Key(handler.getMethod().getMethod(), t.getClass());
+ cache.computeIfAbsent(key, k -> findSuitableExceptionHandler(handler.getService(), k.type))
+ .ifPresent(m -> {
+ try {
+ result.setValue(invokeExceptionHandler(m, t, request, response));
+ result.setException(null);
+ } catch (Throwable th) {
+ result.setException(th);
+ }
+ });
+ return;
+ }
+
+ Object value = result.getValue();
+ if (value instanceof ResponseEntity) {
+ ResponseEntity> entity = (ResponseEntity>) value;
+ result.setValue(HttpResult.builder()
+ .body(entity.getBody())
+ .status(Helper.getStatusCode(entity))
+ .headers(copyHeaders(entity))
+ .build());
+ return;
+ }
+
+ RequestMapping mapping = request.attribute(RestConstants.MAPPING_ATTRIBUTE);
+ if (mapping == null) {
+ return;
+ }
+ ResponseMeta responseMeta = mapping.getResponse();
+ if (responseMeta == null) {
+ return;
+ }
+ String reason = responseMeta.getReason();
+ result.setValue(HttpResult.builder()
+ .body(reason == null ? result.getValue() : reason)
+ .status(responseMeta.getStatus())
+ .build());
+ }
+
+ private static Map> copyHeaders(ResponseEntity> entity) {
+ Map> headers = new LinkedHashMap<>();
+ entity.getHeaders().forEach(headers::put);
+ return headers;
+ }
+
+ private Optional findSuitableExceptionHandler(ServiceMeta serviceMeta, Class> exType) {
+ if (serviceMeta.getExceptionHandlers() == null) {
+ return Optional.empty();
+ }
+ List, MethodMeta>> candidates = new ArrayList<>();
+ for (MethodMeta methodMeta : serviceMeta.getExceptionHandlers()) {
+ ExceptionHandler anno = methodMeta.getMethod().getAnnotation(ExceptionHandler.class);
+ if (anno == null) {
+ continue;
+ }
+ for (Class> type : anno.value()) {
+ if (type.isAssignableFrom(exType)) {
+ candidates.add(Pair.of(type, methodMeta));
+ }
+ }
+ }
+ int size = candidates.size();
+ if (size == 0) {
+ return Optional.empty();
+ }
+ if (size > 1) {
+ candidates.sort((o1, o2) -> {
+ Class> c1 = o1.getLeft();
+ Class> c2 = o2.getLeft();
+ if (c1.equals(c2)) {
+ return 0;
+ }
+ return c1.isAssignableFrom(c2) ? 1 : -1;
+ });
+ }
+ return Optional.of(candidates.get(0).getRight());
+ }
+
+ private Object invokeExceptionHandler(MethodMeta meta, Throwable t, HttpRequest request, HttpResponse response) {
+ ParameterMeta[] parameters = meta.getParameters();
+ int len = parameters.length;
+ Object[] args = new Object[len];
+ for (int i = 0; i < len; i++) {
+ ParameterMeta parameter = parameters[i];
+ if (parameter.getType().isInstance(t)) {
+ args[i] = t;
+ } else {
+ args[i] = argumentResolver.resolve(parameter, request, response);
+ }
+ }
+ Object value;
+ try {
+ value = meta.getMethod().invoke(meta.getServiceMeta().getService(), args);
+ } catch (Exception e) {
+ throw new RestException("Failed to invoke exception handler method: " + meta.getMethod(), e);
+ }
+ AnnotationMeta> rs = meta.getAnnotation(ResponseStatus.class);
+ if (rs == null) {
+ return value;
+ }
+ HttpStatus status = rs.getEnum("value");
+ String reason = rs.getString("reason");
+ return HttpResult.builder()
+ .body(StringUtils.isEmpty(reason) ? value : reason)
+ .status(status.value())
+ .build();
+ }
+
+ private static final class Key {
+ private final Method method;
+ private final Class> type;
+
+ Key(Method method, Class> type) {
+ this.method = method;
+ this.type = type;
+ }
+
+ @Override
+ @SuppressWarnings({"EqualsWhichDoesntCheckParameterClass", "EqualsDoesntCheckParameterClass"})
+ public boolean equals(Object obj) {
+ Key other = (Key) obj;
+ return method.equals(other.method) && type.equals(other.type);
+ }
+
+ @Override
+ public int hashCode() {
+ return method.hashCode() * 31 + type.hashCode();
+ }
+ }
+}
diff --git a/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/SpringRestToolKit.java b/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/SpringRestToolKit.java
new file mode 100644
index 000000000000..38b20df54594
--- /dev/null
+++ b/dubbo-plugin/dubbo-rest-spring6/src/main/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/SpringRestToolKit.java
@@ -0,0 +1,176 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.protocol.tri.rest.support.spring;
+
+import org.apache.dubbo.common.logger.Logger;
+import org.apache.dubbo.common.logger.LoggerFactory;
+import org.apache.dubbo.common.utils.CollectionUtils;
+import org.apache.dubbo.common.utils.DefaultParameterNameReader;
+import org.apache.dubbo.common.utils.ParameterNameReader;
+import org.apache.dubbo.config.spring.extension.SpringExtensionInjector;
+import org.apache.dubbo.remoting.http12.HttpRequest;
+import org.apache.dubbo.remoting.http12.HttpResponse;
+import org.apache.dubbo.rpc.model.ApplicationModel;
+import org.apache.dubbo.rpc.model.FrameworkModel;
+import org.apache.dubbo.rpc.protocol.tri.rest.Messages;
+import org.apache.dubbo.rpc.protocol.tri.rest.RestConstants;
+import org.apache.dubbo.rpc.protocol.tri.rest.RestException;
+import org.apache.dubbo.rpc.protocol.tri.rest.argument.CompositeArgumentResolver;
+import org.apache.dubbo.rpc.protocol.tri.rest.argument.GeneralTypeConverter;
+import org.apache.dubbo.rpc.protocol.tri.rest.argument.TypeConverter;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.MethodParameterMeta;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.NamedValueMeta;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.ParameterMeta;
+import org.apache.dubbo.rpc.protocol.tri.rest.util.RestToolKit;
+import org.apache.dubbo.rpc.protocol.tri.rest.util.RestUtils;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.AnnotatedElement;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.util.Collection;
+import java.util.Map;
+
+import org.springframework.beans.factory.config.ConfigurableBeanFactory;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.core.MethodParameter;
+import org.springframework.core.annotation.AnnotatedElementUtils;
+import org.springframework.core.convert.ConversionService;
+import org.springframework.core.convert.TypeDescriptor;
+import org.springframework.core.convert.support.DefaultConversionService;
+import org.springframework.util.PropertyPlaceholderHelper;
+
+final class SpringRestToolKit implements RestToolKit {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(SpringRestToolKit.class);
+
+ private final Map cache = CollectionUtils.newConcurrentHashMap();
+ private final ConfigurableBeanFactory beanFactory;
+ private final PropertyPlaceholderHelper placeholderHelper;
+ private final ConfigurationWrapper configuration;
+ private final ConversionService conversionService;
+ private final TypeConverter typeConverter;
+ private final BeanArgumentBinder argumentBinder;
+ private final ParameterNameReader parameterNameReader;
+ private final CompositeArgumentResolver argumentResolver;
+
+ public SpringRestToolKit(FrameworkModel frameworkModel) {
+ ApplicationModel applicationModel = frameworkModel.defaultApplication();
+ SpringExtensionInjector injector = SpringExtensionInjector.get(applicationModel);
+ ApplicationContext context = injector.getContext();
+ if (context instanceof ConfigurableApplicationContext) {
+ beanFactory = ((ConfigurableApplicationContext) context).getBeanFactory();
+ placeholderHelper = null;
+ configuration = null;
+ } else {
+ beanFactory = null;
+ placeholderHelper = new PropertyPlaceholderHelper("${", "}", ":", true);
+ configuration = new ConfigurationWrapper(applicationModel);
+ }
+ if (context != null && context.containsBean("mvcConversionService")) {
+ conversionService = context.getBean("mvcConversionService", ConversionService.class);
+ } else {
+ conversionService = DefaultConversionService.getSharedInstance();
+ }
+ typeConverter = frameworkModel.getOrRegisterBean(GeneralTypeConverter.class);
+ parameterNameReader = frameworkModel.getOrRegisterBean(DefaultParameterNameReader.class);
+ argumentResolver = frameworkModel.getOrRegisterBean(CompositeArgumentResolver.class);
+ argumentBinder = new BeanArgumentBinder(argumentResolver, conversionService);
+ }
+
+ @Override
+ public int getDialect() {
+ return RestConstants.DIALECT_SPRING_MVC;
+ }
+
+ @Override
+ public String resolvePlaceholders(String text) {
+ if (!RestUtils.hasPlaceholder(text)) {
+ return text;
+ }
+ if (beanFactory != null) {
+ return beanFactory.resolveEmbeddedValue(text);
+ }
+ return placeholderHelper.replacePlaceholders(text, configuration);
+ }
+
+ @Override
+ public Object convert(Object value, ParameterMeta parameter) {
+ boolean tried = false;
+ if (value instanceof Collection || value instanceof Map) {
+ tried = true;
+ Object target = typeConverter.convert(value, parameter.getGenericType());
+ if (target != null) {
+ return target;
+ }
+ }
+ if (parameter instanceof MethodParameterMeta) {
+ TypeDescriptor targetType = cache.computeIfAbsent(
+ (MethodParameterMeta) parameter,
+ k -> new TypeDescriptor(new MethodParameter(k.getMethod(), k.getIndex())));
+ TypeDescriptor sourceType = TypeDescriptor.forObject(value);
+ if (conversionService.canConvert(sourceType, targetType)) {
+ try {
+ return conversionService.convert(value, sourceType, targetType);
+ } catch (Throwable t) {
+ LOGGER.debug(
+ "Spring convert value '{}' from type [{}] to type [{}] failed",
+ value,
+ value.getClass(),
+ parameter.getGenericType(),
+ t);
+ }
+ }
+ }
+ Object target = tried ? null : typeConverter.convert(value, parameter.getGenericType());
+ if (target == null && value != null) {
+ throw new RestException(
+ Messages.ARGUMENT_CONVERT_ERROR,
+ parameter.getName(),
+ value,
+ value.getClass(),
+ parameter.getGenericType());
+ }
+ return target;
+ }
+
+ @Override
+ public Object bind(ParameterMeta parameter, HttpRequest request, HttpResponse response) {
+ return argumentBinder.bind(parameter, request, response);
+ }
+
+ @Override
+ public NamedValueMeta getNamedValueMeta(ParameterMeta parameter) {
+ return argumentResolver.getNamedValueMeta(parameter);
+ }
+
+ @Override
+ public String[] getParameterNames(Method method) {
+ return parameterNameReader.readParameterNames(method);
+ }
+
+ @Override
+ public String[] getParameterNames(Constructor> ctor) {
+ return parameterNameReader.readParameterNames(ctor);
+ }
+
+ @Override
+ public Map getAttributes(AnnotatedElement element, Annotation annotation) {
+ return AnnotatedElementUtils.getMergedAnnotationAttributes(element, annotation.annotationType());
+ }
+}
diff --git a/dubbo-plugin/dubbo-rest-spring6/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.model.ScopeModelInitializer b/dubbo-plugin/dubbo-rest-spring6/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.model.ScopeModelInitializer
new file mode 100644
index 000000000000..e6c9fdc882e7
--- /dev/null
+++ b/dubbo-plugin/dubbo-rest-spring6/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.model.ScopeModelInitializer
@@ -0,0 +1 @@
+rest-spring=org.apache.dubbo.rpc.protocol.tri.rest.support.spring.RestSpringScopeModelInitializer
diff --git a/dubbo-plugin/dubbo-rest-spring6/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.protocol.tri.rest.argument.ArgumentConverter b/dubbo-plugin/dubbo-rest-spring6/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.protocol.tri.rest.argument.ArgumentConverter
new file mode 100644
index 000000000000..48db4f995899
--- /dev/null
+++ b/dubbo-plugin/dubbo-rest-spring6/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.protocol.tri.rest.argument.ArgumentConverter
@@ -0,0 +1 @@
+multi-value-map-creator=org.apache.dubbo.rpc.protocol.tri.rest.support.spring.MultiValueMapCreator
diff --git a/dubbo-plugin/dubbo-rest-spring6/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.protocol.tri.rest.argument.ArgumentResolver b/dubbo-plugin/dubbo-rest-spring6/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.protocol.tri.rest.argument.ArgumentResolver
new file mode 100644
index 000000000000..50f218d14923
--- /dev/null
+++ b/dubbo-plugin/dubbo-rest-spring6/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.protocol.tri.rest.argument.ArgumentResolver
@@ -0,0 +1,12 @@
+spring-path-variable=org.apache.dubbo.rpc.protocol.tri.rest.support.spring.PathVariableArgumentResolver
+spring-matrix-variable=org.apache.dubbo.rpc.protocol.tri.rest.support.spring.MatrixVariableArgumentResolver
+spring-request-param=org.apache.dubbo.rpc.protocol.tri.rest.support.spring.RequestParamArgumentResolver
+spring-request-header=org.apache.dubbo.rpc.protocol.tri.rest.support.spring.RequestHeaderArgumentResolver
+spring-cookie-value=org.apache.dubbo.rpc.protocol.tri.rest.support.spring.CookieValueArgumentResolver
+spring-request-attribute=org.apache.dubbo.rpc.protocol.tri.rest.support.spring.RequestAttributeArgumentResolver
+spring-request-part=org.apache.dubbo.rpc.protocol.tri.rest.support.spring.RequestPartArgumentResolver
+spring-request-body=org.apache.dubbo.rpc.protocol.tri.rest.support.spring.RequestBodyArgumentResolver
+spring-model-attribute=org.apache.dubbo.rpc.protocol.tri.rest.support.spring.ModelAttributeArgumentResolver
+spring-bind-param=org.apache.dubbo.rpc.protocol.tri.rest.support.spring.BindParamArgumentResolver
+spring-misc=org.apache.dubbo.rpc.protocol.tri.rest.support.spring.SpringMiscArgumentResolver
+spring-fallback=org.apache.dubbo.rpc.protocol.tri.rest.support.spring.FallbackArgumentResolver
diff --git a/dubbo-plugin/dubbo-rest-spring6/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.protocol.tri.rest.filter.RestExtension b/dubbo-plugin/dubbo-rest-spring6/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.protocol.tri.rest.filter.RestExtension
new file mode 100644
index 000000000000..9c41d294306e
--- /dev/null
+++ b/dubbo-plugin/dubbo-rest-spring6/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.protocol.tri.rest.filter.RestExtension
@@ -0,0 +1 @@
+spring-response=org.apache.dubbo.rpc.protocol.tri.rest.support.spring.SpringResponseRestFilter
diff --git a/dubbo-plugin/dubbo-rest-spring6/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.protocol.tri.rest.filter.RestExtensionAdapter b/dubbo-plugin/dubbo-rest-spring6/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.protocol.tri.rest.filter.RestExtensionAdapter
new file mode 100644
index 000000000000..fd87fb6a8cf1
--- /dev/null
+++ b/dubbo-plugin/dubbo-rest-spring6/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.protocol.tri.rest.filter.RestExtensionAdapter
@@ -0,0 +1 @@
+spring-interceptor=org.apache.dubbo.rpc.protocol.tri.rest.support.spring.HandlerInterceptorAdapter
diff --git a/dubbo-plugin/dubbo-rest-spring6/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.protocol.tri.rest.mapping.RequestMappingResolver b/dubbo-plugin/dubbo-rest-spring6/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.protocol.tri.rest.mapping.RequestMappingResolver
new file mode 100644
index 000000000000..968dbaf62a87
--- /dev/null
+++ b/dubbo-plugin/dubbo-rest-spring6/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.protocol.tri.rest.mapping.RequestMappingResolver
@@ -0,0 +1 @@
+spring=org.apache.dubbo.rpc.protocol.tri.rest.support.spring.SpringMvcRequestMappingResolver
diff --git a/dubbo-plugin/dubbo-rest-spring6/src/main/resources/META-INF/dubbo/rest-spring-jakarta.properties b/dubbo-plugin/dubbo-rest-spring6/src/main/resources/META-INF/dubbo/rest-spring-jakarta.properties
new file mode 100644
index 000000000000..a026da5a13cf
--- /dev/null
+++ b/dubbo-plugin/dubbo-rest-spring6/src/main/resources/META-INF/dubbo/rest-spring-jakarta.properties
@@ -0,0 +1,16 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+adapter=dubbo-rest-spring6
diff --git a/dubbo-plugin/dubbo-rest-spring6/src/test/groovy/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/RestProtocolTest.groovy b/dubbo-plugin/dubbo-rest-spring6/src/test/groovy/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/RestProtocolTest.groovy
new file mode 100644
index 000000000000..d718872ca5b4
--- /dev/null
+++ b/dubbo-plugin/dubbo-rest-spring6/src/test/groovy/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/RestProtocolTest.groovy
@@ -0,0 +1,31 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.dubbo.rpc.protocol.tri.rest.support.spring
+
+import org.apache.dubbo.rpc.protocol.tri.rest.support.spring.service.SpringDemoService
+import org.apache.dubbo.rpc.protocol.tri.rest.support.spring.service.SpringDemoServiceImpl
+import org.apache.dubbo.rpc.protocol.tri.rest.test.RestProtocolTestContract
+import org.apache.dubbo.rpc.protocol.tri.test.TestRunnerBuilder
+
+class RestProtocolTest extends RestProtocolTestContract {
+
+ @Override
+ void setupService(TestRunnerBuilder builder) {
+ builder.provider(SpringDemoService.class, new SpringDemoServiceImpl())
+ }
+}
diff --git a/dubbo-plugin/dubbo-rest-spring6/src/test/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/RestSpring6ApiParityTest.java b/dubbo-plugin/dubbo-rest-spring6/src/test/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/RestSpring6ApiParityTest.java
new file mode 100644
index 000000000000..2d7eb47a54f5
--- /dev/null
+++ b/dubbo-plugin/dubbo-rest-spring6/src/test/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/RestSpring6ApiParityTest.java
@@ -0,0 +1,328 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.protocol.tri.rest.support.spring;
+
+import org.apache.dubbo.common.extension.Activate;
+import org.apache.dubbo.rpc.model.FrameworkModel;
+import org.apache.dubbo.rpc.protocol.tri.rest.argument.ArgumentConverter;
+import org.apache.dubbo.rpc.protocol.tri.rest.argument.ArgumentResolver;
+import org.apache.dubbo.rpc.protocol.tri.rest.filter.RestExtension;
+import org.apache.dubbo.rpc.protocol.tri.rest.filter.RestExtensionAdapter;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.RequestMappingResolver;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HexFormat;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+class RestSpring6ApiParityTest {
+
+ private static final String PACKAGE_NAME = "org.apache.dubbo.rpc.protocol.tri.rest.support.spring.";
+
+ private static final List API_CLASS_NAMES = List.of(
+ "AbstractSpringArgumentResolver",
+ "Annotations",
+ "BeanArgumentBinder",
+ "BindParamArgumentResolver",
+ "ConfigurationWrapper",
+ "CookieValueArgumentResolver",
+ "FallbackArgumentResolver",
+ "HandlerInterceptorAdapter",
+ "Helper",
+ "MatrixVariableArgumentResolver",
+ "ModelAttributeArgumentResolver",
+ "MultiValueMapCreator",
+ "PathVariableArgumentResolver",
+ "RequestAttributeArgumentResolver",
+ "RequestBodyArgumentResolver",
+ "RequestHeaderArgumentResolver",
+ "RequestParamArgumentResolver",
+ "RequestPartArgumentResolver",
+ "RestSpringScopeModelInitializer",
+ "SpringMiscArgumentResolver",
+ "SpringMvcRequestMappingResolver",
+ "SpringResponseRestFilter",
+ "SpringRestToolKit");
+
+ @Test
+ void exposesExactlyTheGoldenPublicApiAndActivationContract() throws Exception {
+ assertEquals(readGolden(), apiGolden());
+ }
+
+ @Test
+ void keepsAllTwentyThreeContractClassesTopLevel() throws ClassNotFoundException {
+ assertEquals(23, API_CLASS_NAMES.size());
+ assertEquals(23, API_CLASS_NAMES.stream().distinct().count());
+ for (String simpleName : API_CLASS_NAMES) {
+ Class> type = loadClass(simpleName);
+ assertNull(type.getEnclosingClass(), type.getName());
+ assertFalse(type.isSynthetic(), type.getName());
+ }
+ }
+
+ @Test
+ void activatesAllSixteenSpringExtensionsOnTheJakartaClasspath() {
+ FrameworkModel frameworkModel = new FrameworkModel();
+ try {
+ assertEquals(
+ 12,
+ springActivationCount(frameworkModel
+ .getExtensionLoader(ArgumentResolver.class)
+ .getActivateExtensions()));
+ assertEquals(
+ 1,
+ springActivationCount(frameworkModel
+ .getExtensionLoader(ArgumentConverter.class)
+ .getActivateExtensions()));
+ assertEquals(
+ 1,
+ springActivationCount(frameworkModel
+ .getExtensionLoader(RestExtension.class)
+ .getActivateExtensions()));
+ assertEquals(
+ 1,
+ springActivationCount(frameworkModel
+ .getExtensionLoader(RestExtensionAdapter.class)
+ .getActivateExtensions()));
+ assertEquals(
+ 1,
+ springActivationCount(frameworkModel
+ .getExtensionLoader(RequestMappingResolver.class)
+ .getActivateExtensions()));
+ } finally {
+ frameworkModel.destroy();
+ }
+ }
+
+ private static long springActivationCount(List> extensions) {
+ return extensions.stream()
+ .filter(extension -> extension
+ .getClass()
+ .getPackageName()
+ .equals(PACKAGE_NAME.substring(0, PACKAGE_NAME.length() - 1)))
+ .count();
+ }
+
+ private static String apiPayload() throws ClassNotFoundException {
+ List entries = new ArrayList<>();
+ for (String simpleName : API_CLASS_NAMES) {
+ Class> type = loadClass(simpleName);
+ entries.add(classEntry(type));
+ addActivationEntry(entries, type);
+ Arrays.stream(type.getDeclaredFields())
+ .filter(field -> isApi(field.getModifiers()))
+ .map(RestSpring6ApiParityTest::fieldEntry)
+ .forEach(entries::add);
+ Arrays.stream(type.getDeclaredConstructors())
+ .filter(constructor -> isApi(constructor.getModifiers()))
+ .map(RestSpring6ApiParityTest::constructorEntry)
+ .forEach(entries::add);
+ Arrays.stream(type.getDeclaredMethods())
+ .filter(method -> isApi(method.getModifiers()))
+ .map(RestSpring6ApiParityTest::methodEntry)
+ .forEach(entries::add);
+ }
+ Collections.sort(entries);
+ return String.join("\n", entries) + "\n";
+ }
+
+ private static String apiGolden() throws ClassNotFoundException, NoSuchAlgorithmException {
+ String payload = apiPayload();
+ String digest = HexFormat.of()
+ .formatHex(MessageDigest.getInstance("SHA-256").digest(payload.getBytes(StandardCharsets.UTF_8)));
+ return "format=rest-spring6-public-api-v1\nclasses="
+ + API_CLASS_NAMES.size()
+ + "\nentries="
+ + payload.lines().count()
+ + "\nsha256="
+ + digest
+ + "\n";
+ }
+
+ private static Class> loadClass(String simpleName) throws ClassNotFoundException {
+ return Class.forName(PACKAGE_NAME + simpleName, false, RestSpring6ApiParityTest.class.getClassLoader());
+ }
+
+ private static boolean isApi(int modifiers) {
+ return Modifier.isPublic(modifiers) || Modifier.isProtected(modifiers);
+ }
+
+ private static String classEntry(Class> type) {
+ Type superType = type.getGenericSuperclass();
+ String interfaces = Arrays.stream(type.getGenericInterfaces())
+ .map(Type::getTypeName)
+ .sorted()
+ .collect(Collectors.joining(","));
+ String typeParameters = Arrays.stream(type.getTypeParameters())
+ .map(RestSpring6ApiParityTest::typeParameter)
+ .collect(Collectors.joining(","));
+ return "class "
+ + type.getName()
+ + " modifiers="
+ + Modifier.toString(type.getModifiers() & Modifier.classModifiers())
+ + " superclass="
+ + (superType == null ? "" : superType.getTypeName())
+ + " interfaces="
+ + interfaces
+ + " typeParameters="
+ + typeParameters;
+ }
+
+ private static String typeParameter(TypeVariable> variable) {
+ return variable.getName() + ":"
+ + Arrays.stream(variable.getBounds()).map(Type::getTypeName).collect(Collectors.joining("&"));
+ }
+
+ private static void addActivationEntry(List entries, Class> type) {
+ Activate activate = type.getAnnotation(Activate.class);
+ if (activate == null) {
+ return;
+ }
+ entries.add("activate "
+ + type.getName()
+ + " group="
+ + Arrays.toString(activate.group())
+ + " value="
+ + Arrays.toString(activate.value())
+ + " before="
+ + Arrays.toString(activate.before())
+ + " after="
+ + Arrays.toString(activate.after())
+ + " order="
+ + activate.order()
+ + " onClass="
+ + Arrays.toString(activate.onClass()));
+ }
+
+ private static String fieldEntry(Field field) {
+ return "field "
+ + field.getDeclaringClass().getName()
+ + "#"
+ + field.getName()
+ + " "
+ + descriptor(field.getType())
+ + " modifiers="
+ + Modifier.toString(field.getModifiers() & Modifier.fieldModifiers())
+ + " generic="
+ + field.getGenericType().getTypeName();
+ }
+
+ private static String constructorEntry(Constructor> constructor) {
+ return "constructor "
+ + constructor.getDeclaringClass().getName()
+ + descriptor(constructor.getParameterTypes(), void.class)
+ + " modifiers="
+ + Modifier.toString(constructor.getModifiers() & Modifier.constructorModifiers())
+ + " genericParameters="
+ + typeNames(constructor.getGenericParameterTypes())
+ + " throws="
+ + typeNames(constructor.getGenericExceptionTypes());
+ }
+
+ private static String methodEntry(Method method) {
+ return "method "
+ + method.getDeclaringClass().getName()
+ + "#"
+ + method.getName()
+ + descriptor(method.getParameterTypes(), method.getReturnType())
+ + " modifiers="
+ + Modifier.toString(method.getModifiers() & Modifier.methodModifiers())
+ + " genericReturn="
+ + method.getGenericReturnType().getTypeName()
+ + " genericParameters="
+ + typeNames(method.getGenericParameterTypes())
+ + " throws="
+ + typeNames(method.getGenericExceptionTypes())
+ + " bridge="
+ + method.isBridge()
+ + " synthetic="
+ + method.isSynthetic();
+ }
+
+ private static String typeNames(Type[] types) {
+ return Arrays.stream(types).map(Type::getTypeName).collect(Collectors.joining(","));
+ }
+
+ private static String descriptor(Class>[] parameterTypes, Class> returnType) {
+ return Arrays.stream(parameterTypes)
+ .map(RestSpring6ApiParityTest::descriptor)
+ .collect(Collectors.joining("", "(", ")"))
+ + descriptor(returnType);
+ }
+
+ private static String descriptor(Class> type) {
+ if (type.isPrimitive()) {
+ if (type == void.class) {
+ return "V";
+ }
+ if (type == boolean.class) {
+ return "Z";
+ }
+ if (type == byte.class) {
+ return "B";
+ }
+ if (type == char.class) {
+ return "C";
+ }
+ if (type == short.class) {
+ return "S";
+ }
+ if (type == int.class) {
+ return "I";
+ }
+ if (type == long.class) {
+ return "J";
+ }
+ if (type == float.class) {
+ return "F";
+ }
+ return "D";
+ }
+ if (type.isArray()) {
+ return type.getName().replace('.', '/');
+ }
+ return "L" + type.getName().replace('.', '/') + ";";
+ }
+
+ private static String readGolden() throws IOException {
+ try (InputStream input = RestSpring6ApiParityTest.class.getResourceAsStream("/rest-spring6-api-golden.txt")) {
+ if (input == null) {
+ throw new IOException("Missing rest-spring6-api-golden.txt");
+ }
+ return new String(input.readAllBytes(), StandardCharsets.UTF_8).replace("\r\n", "\n");
+ }
+ }
+}
diff --git a/dubbo-plugin/dubbo-rest-spring6/src/test/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/RestSpring6ServletNamespaceTest.java b/dubbo-plugin/dubbo-rest-spring6/src/test/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/RestSpring6ServletNamespaceTest.java
new file mode 100644
index 000000000000..34e218656a3b
--- /dev/null
+++ b/dubbo-plugin/dubbo-rest-spring6/src/test/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/RestSpring6ServletNamespaceTest.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.protocol.tri.rest.support.spring;
+
+import org.apache.dubbo.common.extension.Activate;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+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.assertTrue;
+
+class RestSpring6ServletNamespaceTest {
+
+ private static final String JAKARTA_REQUEST = "jakarta/servlet/http/HttpServletRequest";
+ private static final String JAKARTA_RESPONSE = "jakarta/servlet/http/HttpServletResponse";
+ private static final String JAVAX_SERVLET = "javax/servlet";
+
+ @Test
+ void servletSensitiveActivationGatesRequireJakartaServlet() {
+ Activate interceptorActivation = HandlerInterceptorAdapter.class.getAnnotation(Activate.class);
+ Activate argumentResolverActivation = SpringMiscArgumentResolver.class.getAnnotation(Activate.class);
+
+ assertNotNull(interceptorActivation);
+ assertNotNull(argumentResolverActivation);
+ assertArrayEquals(
+ new String[] {
+ "org.springframework.web.servlet.HandlerInterceptor", "jakarta.servlet.http.HttpServletRequest"
+ },
+ interceptorActivation.onClass());
+ assertArrayEquals(
+ new String[] {
+ "org.springframework.web.context.request.WebRequest", "jakarta.servlet.http.HttpServletRequest"
+ },
+ argumentResolverActivation.onClass());
+ }
+
+ @Test
+ void servletSensitiveClassesContainOnlyJakartaServletDescriptors() throws IOException {
+ Class> interceptorFilter = Arrays.stream(HandlerInterceptorAdapter.class.getDeclaredClasses())
+ .filter(type -> type.getSimpleName().equals("HandlerInterceptorRestFilter"))
+ .findFirst()
+ .orElseThrow();
+
+ byte[] interceptorBytes = classBytes(interceptorFilter);
+ byte[] argumentResolverBytes = classBytes(SpringMiscArgumentResolver.class);
+ String interceptorClassFile = new String(interceptorBytes, StandardCharsets.ISO_8859_1);
+ String argumentResolverClassFile = new String(argumentResolverBytes, StandardCharsets.ISO_8859_1);
+
+ assertTrue(interceptorClassFile.contains(JAKARTA_REQUEST));
+ assertTrue(interceptorClassFile.contains(JAKARTA_RESPONSE));
+ assertTrue(argumentResolverClassFile.contains(JAKARTA_REQUEST));
+ assertFalse(interceptorClassFile.contains(JAVAX_SERVLET));
+ assertFalse(argumentResolverClassFile.contains(JAVAX_SERVLET));
+ assertEquals(61, majorVersion(interceptorBytes));
+ assertEquals(61, majorVersion(argumentResolverBytes));
+ }
+
+ private static byte[] classBytes(Class> type) throws IOException {
+ String resourceName = "/" + type.getName().replace('.', '/') + ".class";
+ try (InputStream input = type.getResourceAsStream(resourceName)) {
+ assertNotNull(input, resourceName);
+ return input.readAllBytes();
+ }
+ }
+
+ private static int majorVersion(byte[] classBytes) {
+ return ((classBytes[6] & 0xff) << 8) | (classBytes[7] & 0xff);
+ }
+}
diff --git a/dubbo-plugin/dubbo-rest-spring6/src/test/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/RestSpringBoot3ClasspathTest.java b/dubbo-plugin/dubbo-rest-spring6/src/test/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/RestSpringBoot3ClasspathTest.java
new file mode 100644
index 000000000000..bc92fabcbbba
--- /dev/null
+++ b/dubbo-plugin/dubbo-rest-spring6/src/test/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/RestSpringBoot3ClasspathTest.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.protocol.tri.rest.support.spring;
+
+import org.apache.dubbo.common.extension.Activate;
+
+import java.net.URL;
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.core.SpringVersion;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+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.assertTrue;
+
+class RestSpringBoot3ClasspathTest {
+
+ private static final String ADAPTER_RESOURCE =
+ "org/apache/dubbo/rpc/protocol/tri/rest/support/spring/HandlerInterceptorAdapter.class";
+ private static final String COMMON_AUTO_CONFIGURATION_RESOURCE =
+ "org/apache/dubbo/spring/boot/autoconfigure/DubboAutoConfiguration.class";
+ private static final String BOOT3_AUTO_CONFIGURATION_RESOURCE =
+ "org/apache/dubbo/spring/boot/autoconfigure/DubboTriple3AutoConfiguration.class";
+ private static final String LEGACY_MARKER = "META-INF/dubbo/rest-spring-javax.properties";
+ private static final String SPRING6_MARKER = "META-INF/dubbo/rest-spring-jakarta.properties";
+
+ @Test
+ void starterBoot3AutoconfigureAndExplicitAdapterSelectOnlySpring6JakartaImplementation() throws Exception {
+ ClassLoader classLoader = RestSpringBoot3ClasspathTest.class.getClassLoader();
+ assertNotNull(classLoader.getResource(COMMON_AUTO_CONFIGURATION_RESOURCE));
+ assertNotNull(classLoader.getResource(BOOT3_AUTO_CONFIGURATION_RESOURCE));
+
+ List adapters = Collections.list(classLoader.getResources(ADAPTER_RESOURCE));
+ assertEquals(1, adapters.size(), () -> "Expected one REST Spring adapter but found " + adapters);
+ assertTrue(SpringVersion.getVersion().startsWith("6."));
+ assertNull(classLoader.getResource(LEGACY_MARKER));
+ assertNotNull(classLoader.getResource(SPRING6_MARKER));
+
+ Activate activation = HandlerInterceptorAdapter.class.getAnnotation(Activate.class);
+ assertNotNull(activation);
+ assertArrayEquals(
+ new String[] {
+ "org.springframework.web.servlet.HandlerInterceptor", "jakarta.servlet.http.HttpServletRequest"
+ },
+ activation.onClass());
+ }
+}
diff --git a/dubbo-plugin/dubbo-rest-spring6/src/test/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/service/SpringDemoService.java b/dubbo-plugin/dubbo-rest-spring6/src/test/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/service/SpringDemoService.java
new file mode 100644
index 000000000000..6838b0d3ecc5
--- /dev/null
+++ b/dubbo-plugin/dubbo-rest-spring6/src/test/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/service/SpringDemoService.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.protocol.tri.rest.support.spring.service;
+
+import org.apache.dubbo.common.stream.StreamObserver;
+import org.apache.dubbo.rpc.protocol.tri.rest.service.Book;
+import org.apache.dubbo.rpc.protocol.tri.rest.service.User.Group;
+import org.apache.dubbo.rpc.protocol.tri.rest.service.User.UserEx;
+
+import java.util.List;
+import java.util.Map;
+
+import io.grpc.health.v1.HealthCheckRequest;
+import io.grpc.health.v1.HealthCheckResponse;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+
+@RequestMapping("/spring")
+public interface SpringDemoService {
+
+ @RequestMapping
+ String hello(String name);
+
+ @RequestMapping
+ String argTest(String name, int age);
+
+ @RequestMapping
+ Book beanArgTest(@RequestBody(required = false) Book book, Integer quote);
+
+ @RequestMapping
+ Book beanArgTest2(@RequestBody Book book);
+
+ @RequestMapping("/bean")
+ UserEx advanceBeanArgTest(UserEx user);
+
+ @RequestMapping
+ List listArgBodyTest(@RequestBody List list, int age);
+
+ @RequestMapping
+ List listArgBodyTest2(List list, int age);
+
+ @RequestMapping
+ Map> mapArgBodyTest(@RequestBody Map> map, int age);
+
+ @RequestMapping
+ Map> mapArgBodyTest2(Map> map, int age);
+
+ @RequestMapping
+ List beanBodyTest(@RequestBody List groups, int age);
+
+ @RequestMapping
+ List beanBodyTest2(List groups, int age);
+
+ @RequestMapping
+ MultiValueMap multiValueMapTest(MultiValueMap params);
+
+ @RequestMapping
+ Book buy(Book book);
+
+ @RequestMapping("/buy2")
+ Book buy(Book book, int count);
+
+ @RequestMapping
+ String say(String name, Long count);
+
+ @RequestMapping
+ String say(String name);
+
+ @RequestMapping
+ String argNameTest(String name);
+
+ @RequestMapping
+ void pbServerStream(@RequestBody HealthCheckRequest request, StreamObserver responseObserver);
+}
diff --git a/dubbo-plugin/dubbo-rest-spring6/src/test/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/service/SpringDemoServiceImpl.java b/dubbo-plugin/dubbo-rest-spring6/src/test/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/service/SpringDemoServiceImpl.java
new file mode 100644
index 000000000000..99200f741046
--- /dev/null
+++ b/dubbo-plugin/dubbo-rest-spring6/src/test/java/org/apache/dubbo/rpc/protocol/tri/rest/support/spring/service/SpringDemoServiceImpl.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rpc.protocol.tri.rest.support.spring.service;
+
+import org.apache.dubbo.rpc.protocol.tri.rest.service.DemoServiceImpl;
+
+import org.springframework.util.MultiValueMap;
+
+public class SpringDemoServiceImpl extends DemoServiceImpl implements SpringDemoService {
+
+ @Override
+ public MultiValueMap multiValueMapTest(MultiValueMap params) {
+ return params;
+ }
+}
diff --git a/dubbo-plugin/dubbo-rest-spring6/src/test/resources/log4j2-test.xml b/dubbo-plugin/dubbo-rest-spring6/src/test/resources/log4j2-test.xml
new file mode 100644
index 000000000000..637a10ad76d1
--- /dev/null
+++ b/dubbo-plugin/dubbo-rest-spring6/src/test/resources/log4j2-test.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dubbo-plugin/dubbo-rest-spring6/src/test/resources/rest-spring6-api-golden.txt b/dubbo-plugin/dubbo-rest-spring6/src/test/resources/rest-spring6-api-golden.txt
new file mode 100644
index 000000000000..62f6f552f745
--- /dev/null
+++ b/dubbo-plugin/dubbo-rest-spring6/src/test/resources/rest-spring6-api-golden.txt
@@ -0,0 +1,4 @@
+format=rest-spring6-public-api-v1
+classes=23
+entries=166
+sha256=1d3e786afb0af8a7f2df0cdbcd6efe6187ad33df903aa5318f71cab4ef9538f2
diff --git a/dubbo-rpc/dubbo-rpc-triple/src/test/groovy/org/apache/dubbo/rpc/protocol/tri/rest/test/RestProtocolTestContract.groovy b/dubbo-rpc/dubbo-rpc-triple/src/test/groovy/org/apache/dubbo/rpc/protocol/tri/rest/test/RestProtocolTestContract.groovy
new file mode 100644
index 000000000000..5f80de61f2f4
--- /dev/null
+++ b/dubbo-rpc/dubbo-rpc-triple/src/test/groovy/org/apache/dubbo/rpc/protocol/tri/rest/test/RestProtocolTestContract.groovy
@@ -0,0 +1,153 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.dubbo.rpc.protocol.tri.rest.test
+
+import org.apache.dubbo.remoting.http12.message.MediaType
+import org.apache.dubbo.rpc.protocol.tri.rest.service.Book
+import org.apache.dubbo.rpc.protocol.tri.test.TestRequest
+
+abstract class RestProtocolTestContract extends BaseServiceTest {
+
+ def "hello world"() {
+ expect:
+ runner.get(path) == output
+ where:
+ path | output
+ '/spring/hello?name=world' | 'hello world'
+ '/spring/hello/?name=world' | 'hello world'
+ '/spring/hello.yml?name=world' | 'hello world'
+ }
+
+ def "list argument body test"() {
+ expect:
+ runner.post(path, body) contains output
+ where:
+ path | body | output
+ '/spring/listArgBodyTest?age=2' | '[1,2]' | '[1,2]'
+ }
+
+ def "map argument body test"() {
+ expect:
+ def actual = runner.post(path, body)
+ actual.contains('1:[2,3]') && actual.contains('4:[5,6]')
+ where:
+ path | body | output
+ '/spring/mapArgBodyTest?age=2' | '{"1":["2",3],"4":[5,"6"]}' | '{4:[5,6],1:[2,3]}'
+ }
+
+ def "bean argument get test"() {
+ expect:
+ runner.get(path, Book.class).price == output
+ where:
+ path | output
+ '/spring/beanArgTest' | 0
+ '/spring/beanArgTest?quote=5' | 5
+ }
+
+ def "bean argument post test"() {
+ expect:
+ runner.post(path, body, Book.class).price == output
+ where:
+ path | body | output
+ '/spring/beanArgTest' | [:] | 0
+ '/spring/beanArgTest' | [price: 6] | 6
+ '/spring/beanArgTest2' | '{"price": 5}' | 5
+ }
+
+ def "spring bean argument test"() {
+ expect:
+ runner.get(path) contains output
+ where:
+ path | output
+ '/spring/bean?id=1&name=sam' | '"id":1,"name":"sam"'
+ '/spring/bean?name=sam&phone=123&email=a@b.com' | '"email":"a@b.com","name":"sam","phone":"123"'
+ '/spring/bean?group.name=g1&group.owner.name=jack' | '"group":{"id":0,"name":"g1","owner":{"name":"jack"'
+ '/spring/bean?group.parent.parent.children[0].name=xx' | '"group":{"id":0,"parent":{"id":0,"parent":{"children":[{"id":0,"name":"xx"}],"id":0}}}'
+ '/spring/bean?ids=3&ids=4' | '"ids":[3,4]'
+ '/spring/bean?ids[]=3&ids[]=4' | '"ids":[3,4]'
+ '/spring/bean?ids[1]=3&ids[2]=4' | '"ids":[0,3,4]'
+ '/spring/bean?scores=3&scores=4' | '"scores":[3,4]'
+ '/spring/bean?scores[]=3&scores[]=4' | '"scores":[3,4]'
+ '/spring/bean?scores[1]=3&scores[2]=4' | '"scores":[null,3,4]'
+ '/spring/bean?tags[0].name=a&tags[0].value=b&tags[1].name=c&tags[1].value=d' | '"tags":[{"name":"a","value":"b"},{"name":"c","value":"d"}]'
+ '/spring/bean?tagsA[0].name=a&tagsA[0].value=b&tagsA[1].name=c&tagsA[1].value=d' | '"tagsA":[{"name":"a","value":"b"},{"name":"c","value":"d"}]'
+ '/spring/bean?tagsB[0].name=e&tagsB[1].name=c&tagsB[1].value=d' | '"tagsB":[{"name":"e","value":"b"},{"name":"c","value":"d"}]'
+ '/spring/bean?tagsC[0].name=e&tagsC[1].name=c&tagsC[1].value=d' | '"tagsC":[{"name":"e","value":"b"},{"name":"c","value":"d"}]'
+ '/spring/bean?id=1&features[a]=xx&features[b]=2' | '"features":{"a":"xx","b":"2"}'
+ '/spring/bean?tagMapB[2].name=a&tagMapB[2].value=b&tagMapB[3].name=c' | '"tagMapB":{2:{"name":"a","value":"b"},3:{"name":"c"}}'
+ }
+
+ def "bean body test"() {
+ expect:
+ runner.post(path, body) contains output
+ where:
+ path | body | output
+ '/spring/beanBodyTest?age=2' | '[{"id":1,"name":"g1"},{"id":2,"name":"g2"}]' | '[{"id":1,"name":"g1"},{"id":2,"name":"g2"}]'
+ }
+
+ def "multiValueMap test"() {
+ expect:
+ runner.get(path) contains output
+ where:
+ path | output
+ '/spring/multiValueMapTest?name=1&name=2&age=8' | '{"name":[1,2],"age":[8]}'
+ }
+
+ def "urlEncodeForm test"() {
+ given:
+ def request = new TestRequest(
+ path: path,
+ contentType: MediaType.APPLICATION_FROM_URLENCODED,
+ params: [
+ 'name': 'Sam',
+ 'age' : 8
+ ]
+ )
+ expect:
+ runner.post(request) == output
+ where:
+ path | output
+ '/spring/argTest' | 'Sam is 8 years old'
+ }
+
+ def "no interface method test"() {
+ expect:
+ runner.get(path) contains output
+ where:
+ path | output
+ '/spring/noInterface' | 'ok'
+ '/spring/noInterfaceAndMapping' | '404'
+ }
+
+ def "use interface argument name test"() {
+ expect:
+ runner.get(path) contains output
+ where:
+ path | output
+ '/spring/argNameTest?name=Sam' | 'Sam'
+ }
+
+ def "pb server stream test"() {
+ expect:
+ runner.posts(path, body).size() == output
+ where:
+ path | body | output
+ '/spring/pbServerStream' | '{"service": "3"}' | 3
+ '/spring/pbServerStream' | '{}' | 0
+ }
+}
diff --git a/dubbo-test/dubbo-test-modules/src/test/java/org/apache/dubbo/dependency/FileTest.java b/dubbo-test/dubbo-test-modules/src/test/java/org/apache/dubbo/dependency/FileTest.java
index fdc0d7d95944..09729fd0fe3a 100644
--- a/dubbo-test/dubbo-test-modules/src/test/java/org/apache/dubbo/dependency/FileTest.java
+++ b/dubbo-test/dubbo-test-modules/src/test/java/org/apache/dubbo/dependency/FileTest.java
@@ -55,8 +55,10 @@ class FileTest {
ignoredModules.add(Pattern.compile("dubbo-demo.*"));
ignoredModules.add(Pattern.compile("dubbo-annotation-processor"));
ignoredModules.add(Pattern.compile("dubbo-config-spring6"));
+ ignoredModules.add(Pattern.compile("dubbo-rest-spring6"));
ignoredModules.add(Pattern.compile("dubbo-spring6-security"));
ignoredModules.add(Pattern.compile("dubbo-spring-boot-3-autoconfigure"));
+ ignoredModules.add(Pattern.compile("dubbo-test-rest-spring6\\.1"));
ignoredModules.add(Pattern.compile("dubbo-plugin-loom.*"));
ignoredModules.add(Pattern.compile("dubbo-mutiny.*"));
ignoredModules.add(Pattern.compile("dubbo-mcp"));
@@ -74,6 +76,7 @@ class FileTest {
ignoredModulesInDubboAll.add(Pattern.compile("dubbo-metadata-processor"));
ignoredModulesInDubboAll.add(Pattern.compile("dubbo-native.*"));
ignoredModulesInDubboAll.add(Pattern.compile("dubbo-config-spring6.*"));
+ ignoredModulesInDubboAll.add(Pattern.compile("dubbo-rest-spring"));
ignoredModulesInDubboAll.add(Pattern.compile(".*spring-boot.*"));
ignoredModulesInDubboAll.add(Pattern.compile("dubbo-maven-plugin"));
diff --git a/dubbo-test/dubbo-test-rest-spring6.1/pom.xml b/dubbo-test/dubbo-test-rest-spring6.1/pom.xml
new file mode 100644
index 000000000000..1e181009fa16
--- /dev/null
+++ b/dubbo-test/dubbo-test-rest-spring6.1/pom.xml
@@ -0,0 +1,73 @@
+
+
+
+ 4.0.0
+
+ org.apache.dubbo
+ dubbo-test
+ ${revision}
+ ../pom.xml
+
+
+ dubbo-test-rest-spring6.1
+
+
+ true
+ 6.1.5
+
+
+
+
+
+ org.springframework
+ spring-framework-bom
+ ${spring_6_1_version}
+ pom
+ import
+
+
+
+
+
+
+ org.apache.dubbo
+ dubbo-rest-spring6
+ ${project.version}
+ test
+
+
+ org.springframework
+ spring-webmvc
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+ 17
+ 17
+ 17
+
+
+
+
+
diff --git a/dubbo-test/dubbo-test-rest-spring6.1/src/test/java/org/apache/dubbo/rest/spring/compatibility/RequestMappingSpring61CompatibilityTest.java b/dubbo-test/dubbo-test-rest-spring6.1/src/test/java/org/apache/dubbo/rest/spring/compatibility/RequestMappingSpring61CompatibilityTest.java
new file mode 100644
index 000000000000..eabe8cf4cd72
--- /dev/null
+++ b/dubbo-test/dubbo-test-rest-spring6.1/src/test/java/org/apache/dubbo/rest/spring/compatibility/RequestMappingSpring61CompatibilityTest.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.dubbo.rest.spring.compatibility;
+
+import org.apache.dubbo.common.URL;
+import org.apache.dubbo.config.nested.RestConfig;
+import org.apache.dubbo.rpc.model.FrameworkModel;
+import org.apache.dubbo.rpc.model.MethodDescriptor;
+import org.apache.dubbo.rpc.model.ReflectionServiceDescriptor;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.RequestMapping;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.MethodMeta;
+import org.apache.dubbo.rpc.protocol.tri.rest.mapping.meta.ServiceMeta;
+import org.apache.dubbo.rpc.protocol.tri.rest.support.spring.SpringMvcRequestMappingResolver;
+
+import java.lang.reflect.Method;
+import java.util.Collections;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.core.SpringVersion;
+import org.springframework.web.bind.annotation.RequestMethod;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class RequestMappingSpring61CompatibilityTest {
+
+ private FrameworkModel frameworkModel;
+ private SpringMvcRequestMappingResolver resolver;
+
+ @BeforeEach
+ void setUp() {
+ assertTrue(SpringVersion.getVersion().startsWith("6.1."));
+ frameworkModel = new FrameworkModel();
+ resolver = new SpringMvcRequestMappingResolver(frameworkModel);
+ resolver.setRestConfig(new RestConfig());
+ }
+
+ @AfterEach
+ void tearDown() {
+ frameworkModel.destroy();
+ }
+
+ @Test
+ void resolvesRequestMappingWithSpring61() throws Exception {
+ Class serviceType = RequestMappingService.class;
+ Method method = serviceType.getMethod("getItem");
+ ReflectionServiceDescriptor serviceDescriptor = new ReflectionServiceDescriptor(serviceType);
+ MethodDescriptor methodDescriptor = serviceDescriptor.getMethod("getItem", method.getParameterTypes());
+ URL url = URL.valueOf("tri://localhost/" + serviceType.getName());
+ ServiceMeta serviceMeta = new ServiceMeta(
+ Collections.singletonList(serviceType), serviceDescriptor, null, url, resolver.getRestToolKit());
+ MethodMeta methodMeta = new MethodMeta(Collections.singletonList(method), methodDescriptor, serviceMeta);
+
+ RequestMapping mapping = resolver.resolve(methodMeta);
+
+ assertEquals(Collections.singleton("GET"), mapping.getMethodsCondition().getMethods());
+ assertEquals(
+ "/items/{id}",
+ mapping.getPathCondition().getExpressions().get(0).getPath());
+ assertEquals(
+ "application/json",
+ mapping.getProducesCondition().getMediaTypes().get(0).getName());
+ }
+
+ interface RequestMappingService {
+
+ @org.springframework.web.bind.annotation.RequestMapping(
+ path = "/items/{id}",
+ method = RequestMethod.GET,
+ produces = "application/json")
+ String getItem();
+ }
+}
diff --git a/dubbo-test/pom.xml b/dubbo-test/pom.xml
index 80807b77ad9a..edd7d34ecb1c 100644
--- a/dubbo-test/pom.xml
+++ b/dubbo-test/pom.xml
@@ -41,4 +41,16 @@
true
+
+
+ jdk-version-ge-17
+
+ [17,)
+
+
+ dubbo-test-rest-spring6.1
+
+
+
+
diff --git a/pom.xml b/pom.xml
index a1f0dd40599d..2654761e1e1a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -498,6 +498,7 @@
dubbo-spring-boot-project/dubbo-spring-boot-3-autoconfigure
+ dubbo-plugin/dubbo-rest-spring6
dubbo-plugin/dubbo-spring6-security
dubbo-plugin/dubbo-mutiny
@@ -649,6 +650,7 @@
release
dubbo-spring-boot-project/dubbo-spring-boot-3-autoconfigure
+ dubbo-plugin/dubbo-rest-spring6
dubbo-plugin/dubbo-spring6-security
dubbo-plugin/dubbo-plugin-loom
dubbo-plugin/dubbo-mutiny