Repository: apache/dubbo (Java) — Bug Fix

Issue:
[Bug]Webservice consumer cannot invoke webservice provider when provider container has contextPath and servlet has url pattern
- [ ] I have searched the [issues](https://github.com/apache/dubbo/issues) of this repository and believe that this is not a duplicate.
- [ ] I have checked the [FAQ](https://github.com/apache/dubbo/blob/master/FAQ.md) of this repository and believe that this is not a duplicate.

### Environment

* Dubbo version: xxx
* Operating System version: xxx
* Java version: xxx

### Steps to reproduce this issue

1. 
Use a web container, for example tomcat, and use a context path, for example set the container context path "dubbo_samples_webservice_provider_war"

2. 
config web.xml
```xml
    <servlet>
        <servlet-name>dubbo</servlet-name>
        <servlet-class>org.apache.dubbo.remoting.http.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>dubbo</servlet-name>
        <url-pattern>/services/*</url-pattern>
    </servlet-mapping>
```

3. 
config provider
```xml
    <dubbo:application name="dubbo-webservice-app" />

    <dubbo:registry address="zookeeper://127.0.0.1:2181" />

    <dubbo:protocol name="webservice" port="8083" server="servlet" contextpath="dubbo_samples_webservice_provider_war/services"/>

    <dubbo:service interface="org.apache.dubbo.samples.webservice.DemoService" ref="demoService" />

    <bean id="demoService" class="org.apache.dubbo.samples.webservice.provider.impl.DemoServiceImpl" />
```

4.
start provider
start consumer

Pls. provide [GitHub address] to reproduce this issue.

### Expected Result

The consumer can invoke provider successfully.



### Actual Result

The invoke fails

If there is an exception, please attach the exception trace:

```
org.apache.cxf.interceptor.Fault: Could not send Message.
	at org.apache.cxf.interceptor.MessageSenderInterceptor$MessageSenderEndingInterceptor.handleMessage(MessageSenderInterceptor.java:67)
	at org.apache.cxf.phase.PhaseInterceptorChain.doIntercept(PhaseInterceptorChain.java:308)
	at org.apache.cxf.endpoint.ClientImpl.doInvoke(ClientImpl.java:530)
	at org.apache.cxf.endpoint.ClientImpl.invoke(ClientImpl.java:441)
	at org.apache.cxf.endpoint.ClientImpl.invoke(ClientImpl.java:356)
	at org.apache.cxf.endpoint.ClientImpl.invoke(ClientImpl.java:314)
	at org.apache.cxf.frontend.ClientProxy.invokeSync(ClientProxy.java:96)
	at org.apache.cxf.frontend.ClientProxy.invoke(ClientProxy.java:81)
	at com.sun.proxy.$Proxy32.hello(Unknown Source)
	at org.apache.dubbo.common.bytecode.Wrapper0.invokeMethod(Wrapper0.java)
	at org.apache.dubbo.rpc.proxy.javassist.JavassistProxyFactory$1.doInvoke(JavassistProxyFactory.java:47)
	at org.apache.dubbo.rpc.proxy.AbstractProxyInvoker.invoke(AbstractProxyInvoker.java:84)
	at org.apache.dubbo.rpc.protocol.AbstractProxyProtocol$2.doInvoke(AbstractProxyProtocol.java:113)
	at org.apache.dubbo.rpc.protocol.AbstractInvoker.invoke(AbstractInvoker.java:163)
	at org.apache.dubbo.rpc.protocol.AsyncToSyncInvoker.invoke(AsyncToSyncInvoker.java:52)
	at org.apache.dubbo.rpc.listener.ListenerInvokerWrapper.invoke(ListenerInvokerWrapper.java:78)
	at org.apache.dubbo.monitor.support.MonitorFilter.invoke(MonitorFilter.java:89)
	at org.apache.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:81)
	at org.apache.dubbo.rpc.protocol.dubbo.filter.FutureFilter.invoke(FutureFilter.java:51)
	at org.apache.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:81)
	at org.apache.dubbo.rpc.filter.ConsumerContextFilter.invoke(ConsumerContextFilter.java:69)
	at org.apache.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:81)
	at org.apache.dubbo.rpc.protocol.InvokerWrapper.invoke(InvokerWrapper.java:56)
	at org.apache.dubbo.rpc.cluster.support.FailoverClusterInvoker.doInvoke(FailoverClusterInvoker.java:82)
	at org.apache.dubbo.rpc.cluster.support.AbstractClusterInvoker.invoke(AbstractClusterInvoker.java:259)
	at org.apache.dubbo.rpc.cluster.interceptor.ClusterInterceptor.intercept(ClusterInterceptor.java:47)
	at org.apache.dubbo.rpc.cluster.support.wrapper.AbstractCluster$InterceptorInvokerNode.invoke(AbstractCluster.java:92)
	at org.apache.dubbo.rpc.cluster.support.wrapper.MockClusterInvoker.invoke(MockClusterInvoker.java:82)
	at org.apache.dubbo.rpc.proxy.InvokerInvocationHandler.invoke(InvokerInvocationHandler.java:74)
	at org.apache.dubbo.common.bytecode.proxy0.hello(proxy0.java)
	at org.apache.dubbo.samples.webservice.consumer.Application.main(Application.java:27)
Caused by: org.apache.cxf.transport.http.HTTPException: HTTP response '404: null' when communicating with http://127.0.0.1:8083/dubbo_samples_webservice_provider_war/services/org.apache.dubbo.samples.webservice.DemoService
	at org.apache.cxf.transport.http.HTTPConduit$WrappedOutputStream.doProcessResponseCode(HTTPConduit.java:1618)
	at org.apache.cxf.transport.http.HTTPConduit$WrappedOutputStream.handleResponseInternal(HTTPConduit.java:1625)
	at org.apache.cxf.transport.http.HTTPConduit$WrappedOutputStream.handleResponse(HTTPConduit.java:1570)
	at org.apache.cxf.transport.http.HTTPConduit$WrappedOutputStream.close(HTTPConduit.java:1371)
	at org.apache.cxf.transport.AbstractConduit.close(AbstractConduit.java:56)
	at org.apache.cxf.transport.http.HTTPConduit.close(HTTPConduit.java:671)
	at org.apache.cxf.interceptor.MessageSenderInterceptor$MessageSenderEndingInterceptor.handleMessage(MessageSenderInterceptor.java:63)
	... 30 more
```
In fact, the wsdl url is
http://localhost:8083/dubbo_samples_webservice_provider_war/services/dubbo_samples_webservice_provider_war/services/org.apache.dubbo.samples.webservice.DemoService?wsdl

**It's because the consumer cannot get the provider container context path and the servlet pattern defined in the provider.**


Discussion:

Resolving patch:
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
--- 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
@@ -329,6 +329,11 @@ public interface CommonConstants {
 
     String SSL_ENABLED_KEY = "ssl-enabled";
 
+    String SERVICE_PATH_PREFIX = "service.path.prefix";
+
+    String PROTOCOL_SERVER_SERVLET = "servlet";
+
+    String PROTOCOL_SERVER = "server";
 
     /**
      * The parameter key for the class path of the ServiceNameMapping {@link Properties} file
diff --git a/dubbo-rpc/dubbo-rpc-webservice/src/main/java/org/apache/dubbo/rpc/protocol/webservice/WebServiceProtocol.java b/dubbo-rpc/dubbo-rpc-webservice/src/main/java/org/apache/dubbo/rpc/protocol/webservice/WebServiceProtocol.java
--- a/dubbo-rpc/dubbo-rpc-webservice/src/main/java/org/apache/dubbo/rpc/protocol/webservice/WebServiceProtocol.java
+++ b/dubbo-rpc/dubbo-rpc-webservice/src/main/java/org/apache/dubbo/rpc/protocol/webservice/WebServiceProtocol.java
@@ -57,7 +57,10 @@
 import java.lang.reflect.Method;
 import java.net.SocketTimeoutException;
 
+import static org.apache.dubbo.common.constants.CommonConstants.SERVICE_PATH_PREFIX;
 import static org.apache.dubbo.common.constants.CommonConstants.DEFAULT_TIMEOUT;
+import static org.apache.dubbo.common.constants.CommonConstants.PROTOCOL_SERVER;
+import static org.apache.dubbo.common.constants.CommonConstants.PROTOCOL_SERVER_SERVLET;
 import static org.apache.dubbo.common.constants.CommonConstants.TIMEOUT_KEY;
 
 /**
@@ -132,8 +135,12 @@ public void run() {
 
     @Override
     @SuppressWarnings("unchecked")
-    protected <T> T doRefer(final Class<T> serviceType, final URL url) throws RpcException {
+    protected <T> T doRefer(final Class<T> serviceType, URL url) throws RpcException {
         ClientProxyFactoryBean proxyFactoryBean = new ClientProxyFactoryBean();
+        String servicePathPrefix = url.getParameter(SERVICE_PATH_PREFIX);
+        if (!StringUtils.isEmpty(servicePathPrefix) && PROTOCOL_SERVER_SERVLET.equals(url.getParameter(PROTOCOL_SERVER))) {
+            url = url.setPath(servicePathPrefix + "/" + url.getPath());
+        }
         proxyFactoryBean.setAddress(url.setProtocol("http").toIdentityString());
         proxyFactoryBean.setServiceClass(serviceType);
         proxyFactoryBean.setBus(bus);


Test patch:
diff --git a/dubbo-rpc/dubbo-rpc-webservice/src/test/java/org/apache/dubbo/rpc/protocol/webservice/WebserviceProtocolTest.java b/dubbo-rpc/dubbo-rpc-webservice/src/test/java/org/apache/dubbo/rpc/protocol/webservice/WebserviceProtocolTest.java
--- a/dubbo-rpc/dubbo-rpc-webservice/src/test/java/org/apache/dubbo/rpc/protocol/webservice/WebserviceProtocolTest.java
+++ b/dubbo-rpc/dubbo-rpc-webservice/src/test/java/org/apache/dubbo/rpc/protocol/webservice/WebserviceProtocolTest.java
@@ -16,15 +16,25 @@
  */
 package org.apache.dubbo.rpc.protocol.webservice;
 
+import org.apache.catalina.Context;
+import org.apache.catalina.LifecycleException;
+import org.apache.catalina.connector.Connector;
+import org.apache.catalina.startup.Tomcat;
 import org.apache.dubbo.common.URL;
+import org.apache.dubbo.common.URLBuilder;
 import org.apache.dubbo.common.extension.ExtensionLoader;
 import org.apache.dubbo.common.utils.NetUtils;
+import org.apache.dubbo.remoting.http.servlet.DispatcherServlet;
+import org.apache.dubbo.remoting.http.servlet.ServletManager;
 import org.apache.dubbo.rpc.Exporter;
 import org.apache.dubbo.rpc.Protocol;
 import org.apache.dubbo.rpc.ProxyFactory;
 
 import org.junit.jupiter.api.Test;
 
+import java.io.File;
+
+import static org.apache.dubbo.common.constants.CommonConstants.SERVICE_PATH_PREFIX;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
 
@@ -73,5 +83,85 @@ public void testWebserviceProtocol() throws Exception {
 //        assertEquals(echo.$echo(1234), 1234);
     }
 
+    @Test
+    public void testWebserviceServlet() throws LifecycleException {
+        int port = 55065;
+        Tomcat tomcat = buildTomcat("/dubbo-webservice", "/services/*", port);
+        DemoService service = new DemoServiceImpl();
+
+
+        URLBuilder builder = new URLBuilder()
+                .setProtocol("webservice")
+                .setHost("127.0.0.1")
+                .setPort(port)
+                .setPath("dubbo-webservice2/" + DemoService.class.getName())
+                .addParameter("server", "servlet")
+                .addParameter("bind.port", 55065)
+                .addParameter("contextpath", "dubbo-webservice2")
+                .addParameter(SERVICE_PATH_PREFIX, "dubbo-webservice/services")
+                .addParameter("codec", "exchange")
+                .addParameter("timeout", 600000);
+        URL url = builder.build();
+
+        tomcat.start();
+        Exporter<DemoService> exporter = protocol.export(proxy.getInvoker(service, DemoService.class, url));
+        service = proxy.getProxy(protocol.refer(DemoService.class, url));
+        assertEquals(service.getSize(new String[]{"", "", ""}), 3);
+        exporter.unexport();
+        tomcat.stop();
+        tomcat.destroy();
+    }
+
+    @Test
+    public void testWebserviceJetty() throws LifecycleException {
+        Tomcat tomcat = buildTomcat("/dubbo-webservice", "/services/*", 55065);
+        DemoService service = new DemoServiceImpl();
+        int port = 55066;
 
+        URLBuilder builder = new URLBuilder()
+                .setProtocol("webservice")
+                .setHost("127.0.0.1")
+                .setPort(port)
+                .setPath("dubbo-webservice3/" + DemoService.class.getName())
+                .addParameter("server", "jetty")
+                .addParameter("bind.port", 55066)
+                .addParameter("contextpath", "dubbo-webservice2")
+                .addParameter("codec", "exchange")
+                .addParameter("timeout", 3000);
+        URL url = builder.build();
+
+        tomcat.start();
+        Exporter<DemoService> exporter = protocol.export(proxy.getInvoker(service, DemoService.class, url));
+        service = proxy.getProxy(protocol.refer(DemoService.class, url));
+        assertEquals(service.getSize(new String[]{"", "", ""}), 3);
+        exporter.unexport();
+        tomcat.stop();
+        tomcat.destroy();
+    }
+
+    private Tomcat buildTomcat(String servicePathPrefix, String servletPattern, int port) {
+        String baseDir = new File(System.getProperty("java.io.tmpdir")).getAbsolutePath();
+
+        Tomcat tomcat = new Tomcat();
+        Connector connector = tomcat.getConnector();
+        connector.setPort(port);
+        connector.setProperty("maxThreads", "5");
+        connector.setProperty("maxConnections", "-1");
+        connector.setProperty("URIEncoding", "UTF-8");
+        connector.setProperty("connectionTimeout", "60000");
+        connector.setProperty("maxKeepAliveRequests", "-1");
+
+        tomcat.setBaseDir(baseDir);
+        tomcat.setPort(port);
+
+        Context context = tomcat.addContext(servicePathPrefix, baseDir);
+        Tomcat.addServlet(context, "dispatcher", new DispatcherServlet());
+
+        context.addServletMappingDecoded(servletPattern, "dispatcher");
+        ServletManager.getInstance().addServletContext(port, context.getServletContext());
+
+        // tell tomcat to fail on startup failures.
+        System.setProperty("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE", "true");
+        return tomcat;
+    }
 }