diff --git a/dubbo-common/src/main/java/org/apache/dubbo/common/constants/CommonConstants.java b/dubbo-common/src/main/java/org/apache/dubbo/common/constants/CommonConstants.java index fcf113302f..3380621774 100644 --- a/dubbo-common/src/main/java/org/apache/dubbo/common/constants/CommonConstants.java +++ b/dubbo-common/src/main/java/org/apache/dubbo/common/constants/CommonConstants.java @@ -128,6 +128,8 @@ public interface CommonConstants { String THREAD_POOL_EXHAUSTED_LISTENERS_KEY = "thread-pool-exhausted-listeners"; + String JSON_CHECK_LEVEL_KEY = "jsonCheckLevel"; + String THREADS_KEY = "threads"; String QUEUES_KEY = "queues"; diff --git a/dubbo-common/src/main/java/org/apache/dubbo/config/ProtocolConfig.java b/dubbo-common/src/main/java/org/apache/dubbo/config/ProtocolConfig.java index c6be90b3db..20220945ae 100644 --- a/dubbo-common/src/main/java/org/apache/dubbo/config/ProtocolConfig.java +++ b/dubbo-common/src/main/java/org/apache/dubbo/config/ProtocolConfig.java @@ -26,6 +26,7 @@ import java.util.Map; import java.util.Optional; import static org.apache.dubbo.common.constants.CommonConstants.DUBBO_PROTOCOL; +import static org.apache.dubbo.common.constants.CommonConstants.JSON_CHECK_LEVEL_KEY; import static org.apache.dubbo.common.constants.CommonConstants.SSL_ENABLED_KEY; import static org.apache.dubbo.common.constants.CommonConstants.THREAD_POOL_EXHAUSTED_LISTENERS_KEY; import static org.apache.dubbo.common.constants.LoggerCodeConstants.COMMON_UNEXPECTED_EXCEPTION; @@ -229,6 +230,8 @@ public class ProtocolConfig extends AbstractConfig { */ private String extProtocol; + private String jsonCheckLevel; + public ProtocolConfig() { } @@ -331,6 +334,15 @@ public class ProtocolConfig extends AbstractConfig { this.threadname = threadname; } + @Parameter(key = JSON_CHECK_LEVEL_KEY) + public String getJsonCheckLevel() { + return jsonCheckLevel; + } + + public void setJsonCheckLevel(String jsonCheckLevel) { + this.jsonCheckLevel = jsonCheckLevel; + } + @Parameter(key = THREAD_POOL_EXHAUSTED_LISTENERS_KEY) public String getThreadPoolExhaustedListeners() { return threadPoolExhaustedListeners; diff --git a/dubbo-config/dubbo-config-spring/src/main/resources/META-INF/dubbo.xsd b/dubbo-config/dubbo-config-spring/src/main/resources/META-INF/dubbo.xsd index ea3e643132..029bffc592 100644 --- a/dubbo-config/dubbo-config-spring/src/main/resources/META-INF/dubbo.xsd +++ b/dubbo-config/dubbo-config-spring/src/main/resources/META-INF/dubbo.xsd @@ -1692,6 +1692,11 @@ + + + + + diff --git a/dubbo-rpc/dubbo-rpc-rest/src/main/java/org/apache/dubbo/rpc/protocol/rest/RestProtocol.java b/dubbo-rpc/dubbo-rpc-rest/src/main/java/org/apache/dubbo/rpc/protocol/rest/RestProtocol.java index d2d5d917a7..9c1fc67f95 100644 --- a/dubbo-rpc/dubbo-rpc-rest/src/main/java/org/apache/dubbo/rpc/protocol/rest/RestProtocol.java +++ b/dubbo-rpc/dubbo-rpc-rest/src/main/java/org/apache/dubbo/rpc/protocol/rest/RestProtocol.java @@ -18,6 +18,7 @@ package org.apache.dubbo.rpc.protocol.rest; import org.apache.dubbo.common.URL; import org.apache.dubbo.common.utils.ConcurrentHashMapUtils; +import org.apache.dubbo.common.utils.JsonCompatibilityUtil; import org.apache.dubbo.metadata.rest.ServiceRestMetadata; import org.apache.dubbo.remoting.api.pu.DefaultPuHandler; import org.apache.dubbo.remoting.exchange.PortUnificationExchanger; @@ -37,6 +38,7 @@ import org.apache.dubbo.rpc.protocol.rest.deploy.ServiceDeployerManager; import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -47,6 +49,9 @@ import static org.apache.dubbo.common.constants.CommonConstants.INTERFACE_KEY; import static org.apache.dubbo.common.constants.CommonConstants.REST_SERVICE_DEPLOYER_URL_ATTRIBUTE_KEY; import static org.apache.dubbo.common.constants.LoggerCodeConstants.PROTOCOL_ERROR_CLOSE_CLIENT; import static org.apache.dubbo.common.constants.LoggerCodeConstants.PROTOCOL_ERROR_CLOSE_SERVER; +import static org.apache.dubbo.rpc.protocol.rest.constans.RestConstant.JSON_CHECK_LEVEL; +import static org.apache.dubbo.rpc.protocol.rest.constans.RestConstant.JSON_CHECK_LEVEL_STRICT; +import static org.apache.dubbo.rpc.protocol.rest.constans.RestConstant.JSON_CHECK_LEVEL_WARN; import static org.apache.dubbo.rpc.protocol.rest.constans.RestConstant.PATH_SEPARATOR; public class RestProtocol extends AbstractProtocol { @@ -90,6 +95,10 @@ public class RestProtocol extends AbstractProtocol { MetadataResolver.resolveProviderServiceMetadata(url.getServiceModel().getProxyObject().getClass(), url, getContextPath(url)); + // check json compatibility + String jsonCheckLevel = url.getUrlParam().getParameter(JSON_CHECK_LEVEL); + checkJsonCompatibility(invoker.getInterface(), jsonCheckLevel); + // deploy service URL newURL = ServiceDeployerManager.deploy(url, serviceRestMetadata, invoker); @@ -113,6 +122,29 @@ public class RestProtocol extends AbstractProtocol { return exporter; } + private void checkJsonCompatibility(Class clazz, String jsonCheckLevel) throws RpcException { + + if (jsonCheckLevel == null || JSON_CHECK_LEVEL_WARN.equals(jsonCheckLevel)) { + boolean compatibility = JsonCompatibilityUtil.checkClassCompatibility(clazz); + if (!compatibility) { + List unsupportedMethods = JsonCompatibilityUtil.getUnsupportedMethods(clazz); + assert unsupportedMethods != null; + logger.warn("", "", "", String.format("Interface %s does not support json serialization, the specific methods are %s.", clazz.getName(), unsupportedMethods)); + } else { + logger.debug("Check json compatibility complete, all methods of {} can be serialized using json.", clazz.getName()); + } + } else if (JSON_CHECK_LEVEL_STRICT.equals(jsonCheckLevel)) { + boolean compatibility = JsonCompatibilityUtil.checkClassCompatibility(clazz); + if (!compatibility) { + List unsupportedMethods = JsonCompatibilityUtil.getUnsupportedMethods(clazz); + assert unsupportedMethods != null; + throw new IllegalStateException(String.format("Interface %s does not support json serialization, the specific methods are %s.", clazz.getName(), unsupportedMethods)); + } else { + logger.debug("Check json compatibility complete, all methods of {} can be serialized using json.", clazz.getName()); + } + } + } + @Override protected Invoker protocolBindingRefer(final Class type, final URL url) throws RpcException { diff --git a/dubbo-rpc/dubbo-rpc-rest/src/main/java/org/apache/dubbo/rpc/protocol/rest/constans/RestConstant.java b/dubbo-rpc/dubbo-rpc-rest/src/main/java/org/apache/dubbo/rpc/protocol/rest/constans/RestConstant.java index fc2edc66ad..2c62f5cec0 100644 --- a/dubbo-rpc/dubbo-rpc-rest/src/main/java/org/apache/dubbo/rpc/protocol/rest/constans/RestConstant.java +++ b/dubbo-rpc/dubbo-rpc-rest/src/main/java/org/apache/dubbo/rpc/protocol/rest/constans/RestConstant.java @@ -57,6 +57,11 @@ public interface RestConstant { String IDLE_TIMEOUT_PARAM = "idle.timeout"; String KEEP_ALIVE_TIMEOUT_PARAM = "keep.alive.timeout"; + String JSON_CHECK_LEVEL = "jsonCheckLevel"; + String JSON_CHECK_LEVEL_DISABLED = "disabled"; + String JSON_CHECK_LEVEL_WARN = "warn"; + String JSON_CHECK_LEVEL_STRICT = "strict"; + int MAX_REQUEST_SIZE = 1024 * 1024 * 10; int MAX_INITIAL_LINE_LENGTH = 4096; int MAX_HEADER_SIZE = 8192; diff --git a/dubbo-rpc/dubbo-rpc-rest/src/test/java/org/apache/dubbo/rpc/protocol/rest/JsonCompatibilityCheckTest.java b/dubbo-rpc/dubbo-rpc-rest/src/test/java/org/apache/dubbo/rpc/protocol/rest/JsonCompatibilityCheckTest.java new file mode 100644 index 0000000000..2f871f2078 --- /dev/null +++ b/dubbo-rpc/dubbo-rpc-rest/src/test/java/org/apache/dubbo/rpc/protocol/rest/JsonCompatibilityCheckTest.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.rest; + +import org.apache.dubbo.common.URL; +import org.apache.dubbo.common.extension.ExtensionLoader; +import org.apache.dubbo.common.utils.NetUtils; +import org.apache.dubbo.rpc.Exporter; +import org.apache.dubbo.rpc.Protocol; +import org.apache.dubbo.rpc.ProxyFactory; +import org.apache.dubbo.rpc.RpcException; +import org.apache.dubbo.rpc.model.ApplicationModel; +import org.apache.dubbo.rpc.model.ModuleServiceRepository; +import org.apache.dubbo.rpc.model.ProviderModel; +import org.apache.dubbo.rpc.model.ServiceDescriptor; +import org.apache.dubbo.rpc.protocol.rest.compatibility.RestDemoService; +import org.apache.dubbo.rpc.protocol.rest.compatibility.RestDemoServiceImpl; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static org.apache.dubbo.rpc.protocol.rest.constans.RestConstant.JSON_CHECK_LEVEL; +import static org.apache.dubbo.rpc.protocol.rest.constans.RestConstant.JSON_CHECK_LEVEL_DISABLED; +import static org.apache.dubbo.rpc.protocol.rest.constans.RestConstant.JSON_CHECK_LEVEL_STRICT; +import static org.apache.dubbo.rpc.protocol.rest.constans.RestConstant.JSON_CHECK_LEVEL_WARN; + + +public class JsonCompatibilityCheckTest { + + private final int availablePort = NetUtils.getAvailablePort(); + private final URL exportUrl = URL.valueOf("rest://127.0.0.1:" + availablePort + "/rest?interface=org.apache.dubbo.rpc.protocol.rest.compatibility.RestDemoService"); + private final ModuleServiceRepository repository = ApplicationModel.defaultModel().getDefaultModule().getServiceRepository(); + + private final Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getExtension("rest"); + private final ProxyFactory proxy = ExtensionLoader.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension(); + + + @Test + public void testJsonCheckDisabled() { + + RestDemoService server = new RestDemoServiceImpl(); + + URL url = this.registerProvider(exportUrl, server, RestDemoService.class); + + url = url.addParameter(JSON_CHECK_LEVEL, JSON_CHECK_LEVEL_DISABLED); + + Exporter exporter = protocol.export(proxy.getInvoker(server, RestDemoService.class, url)); + + exporter.unexport(); + } + + @Test + public void testJsonCheckWarn() { + RestDemoService server = new RestDemoServiceImpl(); + + URL url = this.registerProvider(exportUrl, server, RestDemoService.class); + + url = url.addParameter(JSON_CHECK_LEVEL, JSON_CHECK_LEVEL_WARN); + + Exporter exporter = protocol.export(proxy.getInvoker(server, RestDemoService.class, url)); + + exporter.unexport(); + } + + @Test + public void testJsonCheckStrict() { + RestDemoService server = new RestDemoServiceImpl(); + + URL url = this.registerProvider(exportUrl, server, RestDemoService.class); + + URL newUrl = url.addParameter(JSON_CHECK_LEVEL, JSON_CHECK_LEVEL_STRICT); + + Assertions.assertThrowsExactly(IllegalStateException.class, () -> { + Exporter exporter = null; + try { + exporter = protocol.export(proxy.getInvoker(server, RestDemoService.class, newUrl)); + } finally { + if (exporter != null) exporter.unexport(); + } + }); + } + + private URL registerProvider(URL url, Object impl, Class interfaceClass) { + ServiceDescriptor serviceDescriptor = repository.registerService(interfaceClass); + ProviderModel providerModel = new ProviderModel( + url.getServiceKey(), + impl, + serviceDescriptor, + null, + null); + repository.registerProvider(providerModel); + return url.setServiceModel(providerModel); + } +} diff --git a/dubbo-rpc/dubbo-rpc-rest/src/test/java/org/apache/dubbo/rpc/protocol/rest/compatibility/Apple.java b/dubbo-rpc/dubbo-rpc-rest/src/test/java/org/apache/dubbo/rpc/protocol/rest/compatibility/Apple.java new file mode 100644 index 0000000000..2122660f6a --- /dev/null +++ b/dubbo-rpc/dubbo-rpc-rest/src/test/java/org/apache/dubbo/rpc/protocol/rest/compatibility/Apple.java @@ -0,0 +1,26 @@ +/* + * 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.rest.compatibility; + +public class Apple implements Fruit { + + @Override + public String sayName() { + return "This is apple"; + } +} diff --git a/dubbo-rpc/dubbo-rpc-rest/src/test/java/org/apache/dubbo/rpc/protocol/rest/compatibility/Fruit.java b/dubbo-rpc/dubbo-rpc-rest/src/test/java/org/apache/dubbo/rpc/protocol/rest/compatibility/Fruit.java new file mode 100644 index 0000000000..c7c36aa4c3 --- /dev/null +++ b/dubbo-rpc/dubbo-rpc-rest/src/test/java/org/apache/dubbo/rpc/protocol/rest/compatibility/Fruit.java @@ -0,0 +1,22 @@ +/* + * 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.rest.compatibility; + +public interface Fruit { + String sayName(); +} diff --git a/dubbo-rpc/dubbo-rpc-rest/src/test/java/org/apache/dubbo/rpc/protocol/rest/compatibility/RestDemoService.java b/dubbo-rpc/dubbo-rpc-rest/src/test/java/org/apache/dubbo/rpc/protocol/rest/compatibility/RestDemoService.java new file mode 100644 index 0000000000..a487b37a47 --- /dev/null +++ b/dubbo-rpc/dubbo-rpc-rest/src/test/java/org/apache/dubbo/rpc/protocol/rest/compatibility/RestDemoService.java @@ -0,0 +1,46 @@ +/* + * 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.rest.compatibility; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.core.MediaType; + +@Path("rest") +public interface RestDemoService { + @GET + @Path("hello") + @Consumes({MediaType.APPLICATION_JSON}) + String sayHello(String name); + + @GET + @Path("hi") + @Consumes({MediaType.APPLICATION_JSON}) + String sayHi(); + + @GET + @Path("fruit") + @Consumes({MediaType.APPLICATION_JSON}) + Fruit sayFruit(); + + @GET + @Path("apple") + @Consumes({MediaType.APPLICATION_JSON}) + Apple sayApple(); +} diff --git a/dubbo-rpc/dubbo-rpc-rest/src/test/java/org/apache/dubbo/rpc/protocol/rest/compatibility/RestDemoServiceImpl.java b/dubbo-rpc/dubbo-rpc-rest/src/test/java/org/apache/dubbo/rpc/protocol/rest/compatibility/RestDemoServiceImpl.java new file mode 100644 index 0000000000..f1b94f4455 --- /dev/null +++ b/dubbo-rpc/dubbo-rpc-rest/src/test/java/org/apache/dubbo/rpc/protocol/rest/compatibility/RestDemoServiceImpl.java @@ -0,0 +1,39 @@ +/* + * 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.rest.compatibility; + +public class RestDemoServiceImpl implements RestDemoService { + @Override + public String sayHello(String name) { + return "hello"; + } + + @Override + public String sayHi() { + return "hi"; + } + + @Override + public Fruit sayFruit() { + return new Apple(); + } + + @Override + public Apple sayApple() { + return new Apple(); + } +}