ServletHttpHandlerAdapter supports Serlvet path mapping

Issue: SPR-16155
This commit is contained in:
Rossen Stoyanchev 2017-11-02 20:53:26 -04:00
parent aa653b23bc
commit 8c33ed02b3
6 changed files with 143 additions and 65 deletions

View File

@ -17,11 +17,13 @@
package org.springframework.http.server.reactive; package org.springframework.http.server.reactive;
import java.io.IOException; import java.io.IOException;
import java.util.Collection;
import javax.servlet.AsyncContext; import javax.servlet.AsyncContext;
import javax.servlet.AsyncEvent; import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener; import javax.servlet.AsyncListener;
import javax.servlet.Servlet; import javax.servlet.Servlet;
import javax.servlet.ServletConfig; import javax.servlet.ServletConfig;
import javax.servlet.ServletRegistration;
import javax.servlet.ServletRequest; import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse; import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebServlet; import javax.servlet.annotation.WebServlet;
@ -62,9 +64,9 @@ public class ServletHttpHandlerAdapter implements Servlet {
private int bufferSize = DEFAULT_BUFFER_SIZE; private int bufferSize = DEFAULT_BUFFER_SIZE;
@Nullable
private String servletPath;
// Servlet is based on blocking I/O, hence the usage of non-direct, heap-based buffers
// (i.e. 'false' as constructor argument)
private DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory(false); private DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory(false);
@ -90,6 +92,18 @@ public class ServletHttpHandlerAdapter implements Servlet {
return this.bufferSize; return this.bufferSize;
} }
/**
* Return the Servlet path under which the Servlet is deployed by checking
* the Servlet registration from {@link #init(ServletConfig)}.
* @return the path, or an empty string if the Servlet is deployed without
* a prefix (i.e. "/" or "/*"), or {@code null} if this method is invoked
* before the {@link #init(ServletConfig)} Servlet container callback.
*/
@Nullable
public String getServletPath() {
return this.servletPath;
}
public void setDataBufferFactory(DataBufferFactory dataBufferFactory) { public void setDataBufferFactory(DataBufferFactory dataBufferFactory) {
Assert.notNull(dataBufferFactory, "DataBufferFactory must not be null"); Assert.notNull(dataBufferFactory, "DataBufferFactory must not be null");
this.dataBufferFactory = dataBufferFactory; this.dataBufferFactory = dataBufferFactory;
@ -100,7 +114,40 @@ public class ServletHttpHandlerAdapter implements Servlet {
} }
// The Servlet.service method // Servlet methods...
@Override
public void init(ServletConfig config) {
this.servletPath = getServletPath(config);
}
@Nullable
private String getServletPath(ServletConfig config) {
String name = config.getServletName();
ServletRegistration registration = config.getServletContext().getServletRegistration(name);
Assert.notNull(registration, "ServletRegistration not found for Servlet '" + name + "'.");
Collection<String> mappings = registration.getMappings();
if (mappings.size() == 1) {
String mapping = mappings.iterator().next();
if (mapping.equals("/")) {
return "";
}
if (mapping.endsWith("/*")) {
String path = mapping.substring(0, mapping.length() - 2);
if (!path.isEmpty()) {
logger.info("Found Servlet mapping '" + path + "' for Servlet '" + name + "'.");
}
return path;
}
}
throw new IllegalArgumentException("Expected a single Servlet mapping -- " +
"either the default Servlet mapping (i.e. '/'), " +
"or a path based mapping (e.g. '/*', '/foo/*'). " +
"Actual mappings: " + mappings + " for Servlet '" + name + "'.");
}
@Override @Override
public void service(ServletRequest request, ServletResponse response) throws IOException { public void service(ServletRequest request, ServletResponse response) throws IOException {
@ -121,21 +168,24 @@ public class ServletHttpHandlerAdapter implements Servlet {
this.httpHandler.handle(httpRequest, httpResponse).subscribe(subscriber); this.httpHandler.handle(httpRequest, httpResponse).subscribe(subscriber);
} }
protected ServerHttpRequest createRequest(HttpServletRequest request, AsyncContext context) throws IOException { protected ServerHttpRequest createRequest(HttpServletRequest request, AsyncContext context)
throws IOException {
Assert.notNull(this.servletPath, "servletPath is not initialized.");
return new ServletServerHttpRequest( return new ServletServerHttpRequest(
request, context, getDataBufferFactory(), getBufferSize()); request, context, this.servletPath, getDataBufferFactory(), getBufferSize());
} }
protected ServerHttpResponse createResponse(HttpServletResponse response, AsyncContext context) throws IOException { protected ServerHttpResponse createResponse(HttpServletResponse response, AsyncContext context)
return new ServletServerHttpResponse( throws IOException {
response, context, getDataBufferFactory(), getBufferSize());
return new ServletServerHttpResponse(response, context, getDataBufferFactory(), getBufferSize());
} }
// Other Servlet methods...
@Override @Override
public void init(ServletConfig config) { public String getServletInfo() {
return "";
} }
@Override @Override
@ -144,11 +194,6 @@ public class ServletHttpHandlerAdapter implements Servlet {
return null; return null;
} }
@Override
public String getServletInfo() {
return "";
}
@Override @Override
public void destroy() { public void destroy() {
} }

View File

@ -70,9 +70,9 @@ class ServletServerHttpRequest extends AbstractServerHttpRequest {
public ServletServerHttpRequest(HttpServletRequest request, AsyncContext asyncContext, public ServletServerHttpRequest(HttpServletRequest request, AsyncContext asyncContext,
DataBufferFactory bufferFactory, int bufferSize) throws IOException { String servletPath, DataBufferFactory bufferFactory, int bufferSize) throws IOException {
super(initUri(request), request.getContextPath(), initHeaders(request)); super(initUri(request), request.getContextPath() + servletPath, initHeaders(request));
Assert.notNull(bufferFactory, "'bufferFactory' must not be null"); Assert.notNull(bufferFactory, "'bufferFactory' must not be null");
Assert.isTrue(bufferSize > 0, "'bufferSize' must be higher than 0"); Assert.isTrue(bufferSize > 0, "'bufferSize' must be higher than 0");

View File

@ -31,6 +31,7 @@ import org.apache.catalina.connector.CoyoteOutputStream;
import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.util.Assert;
/** /**
* {@link ServletHttpHandlerAdapter} extension that uses Tomcat APIs for reading * {@link ServletHttpHandlerAdapter} extension that uses Tomcat APIs for reading
@ -42,18 +43,25 @@ import org.springframework.core.io.buffer.DataBufferUtils;
@WebServlet(asyncSupported = true) @WebServlet(asyncSupported = true)
public class TomcatHttpHandlerAdapter extends ServletHttpHandlerAdapter { public class TomcatHttpHandlerAdapter extends ServletHttpHandlerAdapter {
public TomcatHttpHandlerAdapter(HttpHandler httpHandler) { public TomcatHttpHandlerAdapter(HttpHandler httpHandler) {
super(httpHandler); super(httpHandler);
} }
@Override @Override
protected ServerHttpRequest createRequest(HttpServletRequest request, AsyncContext cxt) throws IOException { protected ServerHttpRequest createRequest(HttpServletRequest request, AsyncContext asyncContext)
return new TomcatServerHttpRequest(request, cxt, getDataBufferFactory(), getBufferSize()); throws IOException {
Assert.notNull(getServletPath(), "servletPath is not initialized.");
return new TomcatServerHttpRequest(request, asyncContext, getServletPath(),
getDataBufferFactory(), getBufferSize());
} }
@Override @Override
protected ServerHttpResponse createResponse(HttpServletResponse response, AsyncContext cxt) throws IOException { protected ServerHttpResponse createResponse(HttpServletResponse response, AsyncContext cxt)
throws IOException {
return new TomcatServerHttpResponse(response, cxt, getDataBufferFactory(), getBufferSize()); return new TomcatServerHttpResponse(response, cxt, getDataBufferFactory(), getBufferSize());
} }
@ -61,9 +69,9 @@ public class TomcatHttpHandlerAdapter extends ServletHttpHandlerAdapter {
private final class TomcatServerHttpRequest extends ServletServerHttpRequest { private final class TomcatServerHttpRequest extends ServletServerHttpRequest {
public TomcatServerHttpRequest(HttpServletRequest request, AsyncContext context, public TomcatServerHttpRequest(HttpServletRequest request, AsyncContext context,
DataBufferFactory factory, int bufferSize) throws IOException { String servletPath, DataBufferFactory factory, int bufferSize) throws IOException {
super(request, context, factory, bufferSize); super(request, context, servletPath, factory, bufferSize);
} }
@Override @Override

View File

@ -93,7 +93,7 @@ public class ServerHttpRequestTests {
} }
}; };
AsyncContext asyncContext = new MockAsyncContext(request, new MockHttpServletResponse()); AsyncContext asyncContext = new MockAsyncContext(request, new MockHttpServletResponse());
return new ServletServerHttpRequest(request, asyncContext, new DefaultDataBufferFactory(), 1024); return new ServletServerHttpRequest(request, asyncContext, "", new DefaultDataBufferFactory(), 1024);
} }
private static class TestServletInputStream extends DelegatingServletInputStream { private static class TestServletInputStream extends DelegatingServletInputStream {

View File

@ -35,6 +35,10 @@ public class TomcatHttpServer extends AbstractHttpServer {
private final Class<?> wsListener; private final Class<?> wsListener;
private String contextPath = "";
private String servletMapping = "/";
private Tomcat tomcatServer; private Tomcat tomcatServer;
@ -49,6 +53,15 @@ public class TomcatHttpServer extends AbstractHttpServer {
} }
public void setContextPath(String contextPath) {
this.contextPath = contextPath;
}
public void setServletMapping(String servletMapping) {
this.servletMapping = servletMapping;
}
@Override @Override
protected void initServer() throws Exception { protected void initServer() throws Exception {
this.tomcatServer = new Tomcat(); this.tomcatServer = new Tomcat();
@ -59,9 +72,9 @@ public class TomcatHttpServer extends AbstractHttpServer {
ServletHttpHandlerAdapter servlet = initServletAdapter(); ServletHttpHandlerAdapter servlet = initServletAdapter();
File base = new File(System.getProperty("java.io.tmpdir")); File base = new File(System.getProperty("java.io.tmpdir"));
Context rootContext = tomcatServer.addContext("", base.getAbsolutePath()); Context rootContext = tomcatServer.addContext(this.contextPath, base.getAbsolutePath());
Tomcat.addServlet(rootContext, "httpHandlerServlet", servlet); Tomcat.addServlet(rootContext, "httpHandlerServlet", servlet);
rootContext.addServletMappingDecoded("/", "httpHandlerServlet"); rootContext.addServletMappingDecoded(this.servletMapping, "httpHandlerServlet");
if (wsListener != null) { if (wsListener != null) {
rootContext.addApplicationListener(wsListener.getName()); rootContext.addApplicationListener(wsListener.getName());
} }

View File

@ -15,6 +15,8 @@
*/ */
package org.springframework.web.reactive.result.method.annotation; package org.springframework.web.reactive.result.method.annotation;
import java.io.File;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@ -25,6 +27,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.http.server.reactive.HttpHandler; import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.bootstrap.ReactorHttpServer; import org.springframework.http.server.reactive.bootstrap.ReactorHttpServer;
import org.springframework.http.server.reactive.bootstrap.TomcatHttpServer;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
@ -34,76 +37,85 @@ import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
/** /**
* Integration tests that demonstrate running multiple applications under * Integration tests related to the use of context paths.
* different context paths.
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
*/ */
@SuppressWarnings({"unused", "WeakerAccess"}) @SuppressWarnings({"unused", "WeakerAccess"})
public class ContextPathIntegrationTests { public class ContextPathIntegrationTests {
private ReactorHttpServer server;
@Test
@Before public void multipleWebFluxApps() throws Exception {
public void setup() throws Exception {
AnnotationConfigApplicationContext context1 = new AnnotationConfigApplicationContext(); AnnotationConfigApplicationContext context1 = new AnnotationConfigApplicationContext();
context1.register(WebApp1Config.class); context1.register(WebAppConfig.class);
context1.refresh(); context1.refresh();
AnnotationConfigApplicationContext context2 = new AnnotationConfigApplicationContext(); AnnotationConfigApplicationContext context2 = new AnnotationConfigApplicationContext();
context2.register(WebApp2Config.class); context2.register(WebAppConfig.class);
context2.refresh(); context2.refresh();
HttpHandler webApp1Handler = WebHttpHandlerBuilder.applicationContext(context1).build(); HttpHandler webApp1Handler = WebHttpHandlerBuilder.applicationContext(context1).build();
HttpHandler webApp2Handler = WebHttpHandlerBuilder.applicationContext(context2).build(); HttpHandler webApp2Handler = WebHttpHandlerBuilder.applicationContext(context2).build();
this.server = new ReactorHttpServer(); ReactorHttpServer server = new ReactorHttpServer();
server.registerHttpHandler("/webApp1", webApp1Handler);
server.registerHttpHandler("/webApp2", webApp2Handler);
server.afterPropertiesSet();
server.start();
this.server.registerHttpHandler("/webApp1", webApp1Handler); try {
this.server.registerHttpHandler("/webApp2", webApp2Handler); RestTemplate restTemplate = new RestTemplate();
String actual;
this.server.afterPropertiesSet(); String url = "http://localhost:" + server.getPort() + "/webApp1/test";
this.server.start(); actual = restTemplate.getForObject(url, String.class);
assertEquals("Tested in /webApp1", actual);
url = "http://localhost:" + server.getPort() + "/webApp2/test";
actual = restTemplate.getForObject(url, String.class);
assertEquals("Tested in /webApp2", actual);
}
finally {
server.stop();
}
} }
@After
public void shutdown() throws Exception {
this.server.stop();
}
@Test @Test
public void basic() throws Exception { public void servletPathMapping() throws Exception {
RestTemplate restTemplate = new RestTemplate(); AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
String actual; context.register(WebAppConfig.class);
context.refresh();
actual = restTemplate.getForObject(createUrl("/webApp1/test"), String.class); File base = new File(System.getProperty("java.io.tmpdir"));
assertEquals("Tested in /webApp1", actual); TomcatHttpServer server = new TomcatHttpServer(base.getAbsolutePath());
server.setContextPath("/app");
server.setServletMapping("/api/*");
actual = restTemplate.getForObject(createUrl("/webApp2/test"), String.class); HttpHandler httpHandler = WebHttpHandlerBuilder.applicationContext(context).build();
assertEquals("Tested in /webApp2", actual); server.setHandler(httpHandler);
}
private String createUrl(String path) { server.afterPropertiesSet();
return "http://localhost:" + this.server.getPort() + path; server.start();
}
try {
RestTemplate restTemplate = new RestTemplate();
String actual;
@EnableWebFlux String url = "http://localhost:" + server.getPort() + "/app/api/test";
@Configuration actual = restTemplate.getForObject(url, String.class);
static class WebApp1Config { assertEquals("Tested in /app/api", actual);
}
@Bean finally {
public TestController testController() { server.stop();
return new TestController();
} }
} }
@EnableWebFlux @EnableWebFlux
@Configuration @Configuration
static class WebApp2Config { static class WebAppConfig {
@Bean @Bean
public TestController testController() { public TestController testController() {