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