From 8c33ed02b33c0b708958cf05919740bef9a94e16 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 2 Nov 2017 20:53:26 -0400 Subject: [PATCH] ServletHttpHandlerAdapter supports Serlvet path mapping Issue: SPR-16155 --- .../reactive/ServletHttpHandlerAdapter.java | 79 +++++++++++++---- .../reactive/ServletServerHttpRequest.java | 4 +- .../reactive/TomcatHttpHandlerAdapter.java | 18 ++-- .../reactive/ServerHttpRequestTests.java | 2 +- .../reactive/bootstrap/TomcatHttpServer.java | 17 +++- .../ContextPathIntegrationTests.java | 86 +++++++++++-------- 6 files changed, 142 insertions(+), 64 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java index 0d889968c7..bac10b0ef4 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletHttpHandlerAdapter.java @@ -17,11 +17,13 @@ package org.springframework.http.server.reactive; import java.io.IOException; +import java.util.Collection; import javax.servlet.AsyncContext; import javax.servlet.AsyncEvent; import javax.servlet.AsyncListener; import javax.servlet.Servlet; import javax.servlet.ServletConfig; +import javax.servlet.ServletRegistration; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.annotation.WebServlet; @@ -62,9 +64,9 @@ public class ServletHttpHandlerAdapter implements Servlet { 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); @@ -90,6 +92,18 @@ public class ServletHttpHandlerAdapter implements Servlet { 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) { Assert.notNull(dataBufferFactory, "DataBufferFactory must not be null"); 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 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 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); } - protected ServerHttpRequest createRequest(HttpServletRequest request, AsyncContext context) throws IOException { - return new ServletServerHttpRequest( - request, context, getDataBufferFactory(), getBufferSize()); - } + protected ServerHttpRequest createRequest(HttpServletRequest request, AsyncContext context) + throws IOException { - protected ServerHttpResponse createResponse(HttpServletResponse response, AsyncContext context) throws IOException { - return new ServletServerHttpResponse( - response, context, getDataBufferFactory(), getBufferSize()); + Assert.notNull(this.servletPath, "servletPath is not initialized."); + + return new ServletServerHttpRequest( + request, context, this.servletPath, getDataBufferFactory(), getBufferSize()); } + protected ServerHttpResponse createResponse(HttpServletResponse response, AsyncContext context) + throws IOException { - // Other Servlet methods... + return new ServletServerHttpResponse(response, context, getDataBufferFactory(), getBufferSize()); + } @Override - public void init(ServletConfig config) { + public String getServletInfo() { + return ""; } @Override @@ -144,11 +194,6 @@ public class ServletHttpHandlerAdapter implements Servlet { return null; } - @Override - public String getServletInfo() { - return ""; - } - @Override public void destroy() { } diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java index e230f203fe..4932b72e9d 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServletServerHttpRequest.java @@ -70,9 +70,9 @@ class ServletServerHttpRequest extends AbstractServerHttpRequest { 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.isTrue(bufferSize > 0, "'bufferSize' must be higher than 0"); diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/TomcatHttpHandlerAdapter.java b/spring-web/src/main/java/org/springframework/http/server/reactive/TomcatHttpHandlerAdapter.java index 9c028a045e..e9dbb9642b 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/TomcatHttpHandlerAdapter.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/TomcatHttpHandlerAdapter.java @@ -31,6 +31,7 @@ import org.apache.catalina.connector.CoyoteOutputStream; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.util.Assert; /** * {@link ServletHttpHandlerAdapter} extension that uses Tomcat APIs for reading @@ -42,18 +43,25 @@ import org.springframework.core.io.buffer.DataBufferUtils; @WebServlet(asyncSupported = true) public class TomcatHttpHandlerAdapter extends ServletHttpHandlerAdapter { + public TomcatHttpHandlerAdapter(HttpHandler httpHandler) { super(httpHandler); } @Override - protected ServerHttpRequest createRequest(HttpServletRequest request, AsyncContext cxt) throws IOException { - return new TomcatServerHttpRequest(request, cxt, getDataBufferFactory(), getBufferSize()); + protected ServerHttpRequest createRequest(HttpServletRequest request, AsyncContext asyncContext) + throws IOException { + + Assert.notNull(getServletPath(), "servletPath is not initialized."); + return new TomcatServerHttpRequest(request, asyncContext, getServletPath(), + getDataBufferFactory(), getBufferSize()); } @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()); } @@ -61,9 +69,9 @@ public class TomcatHttpHandlerAdapter extends ServletHttpHandlerAdapter { private final class TomcatServerHttpRequest extends ServletServerHttpRequest { 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 diff --git a/spring-web/src/test/java/org/springframework/http/server/reactive/ServerHttpRequestTests.java b/spring-web/src/test/java/org/springframework/http/server/reactive/ServerHttpRequestTests.java index 4c81ece754..c8be82f1cf 100644 --- a/spring-web/src/test/java/org/springframework/http/server/reactive/ServerHttpRequestTests.java +++ b/spring-web/src/test/java/org/springframework/http/server/reactive/ServerHttpRequestTests.java @@ -93,7 +93,7 @@ public class ServerHttpRequestTests { } }; 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 { diff --git a/spring-web/src/test/java/org/springframework/http/server/reactive/bootstrap/TomcatHttpServer.java b/spring-web/src/test/java/org/springframework/http/server/reactive/bootstrap/TomcatHttpServer.java index 7b9bcbd7dc..a11fb1961a 100644 --- a/spring-web/src/test/java/org/springframework/http/server/reactive/bootstrap/TomcatHttpServer.java +++ b/spring-web/src/test/java/org/springframework/http/server/reactive/bootstrap/TomcatHttpServer.java @@ -35,6 +35,10 @@ public class TomcatHttpServer extends AbstractHttpServer { private final Class wsListener; + private String contextPath = ""; + + private String servletMapping = "/"; + 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 protected void initServer() throws Exception { this.tomcatServer = new Tomcat(); @@ -59,9 +72,9 @@ public class TomcatHttpServer extends AbstractHttpServer { ServletHttpHandlerAdapter servlet = initServletAdapter(); 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); - rootContext.addServletMappingDecoded("/", "httpHandlerServlet"); + rootContext.addServletMappingDecoded(this.servletMapping, "httpHandlerServlet"); if (wsListener != null) { rootContext.addApplicationListener(wsListener.getName()); } diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ContextPathIntegrationTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ContextPathIntegrationTests.java index 532a30b521..193a4b408f 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ContextPathIntegrationTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/ContextPathIntegrationTests.java @@ -15,6 +15,8 @@ */ package org.springframework.web.reactive.result.method.annotation; +import java.io.File; + import org.junit.After; import org.junit.Before; 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.ServerHttpRequest; 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.RestController; import org.springframework.web.client.RestTemplate; @@ -34,76 +37,85 @@ import org.springframework.web.server.adapter.WebHttpHandlerBuilder; import static org.junit.Assert.assertEquals; /** - * Integration tests that demonstrate running multiple applications under - * different context paths. + * Integration tests related to the use of context paths. * * @author Rossen Stoyanchev */ @SuppressWarnings({"unused", "WeakerAccess"}) public class ContextPathIntegrationTests { - private ReactorHttpServer server; - - @Before - public void setup() throws Exception { + @Test + public void multipleWebFluxApps() throws Exception { AnnotationConfigApplicationContext context1 = new AnnotationConfigApplicationContext(); - context1.register(WebApp1Config.class); + context1.register(WebAppConfig.class); context1.refresh(); AnnotationConfigApplicationContext context2 = new AnnotationConfigApplicationContext(); - context2.register(WebApp2Config.class); + context2.register(WebAppConfig.class); context2.refresh(); HttpHandler webApp1Handler = WebHttpHandlerBuilder.applicationContext(context1).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); - this.server.registerHttpHandler("/webApp2", webApp2Handler); + try { + RestTemplate restTemplate = new RestTemplate(); + String actual; - this.server.afterPropertiesSet(); - this.server.start(); - } + String url = "http://localhost:" + server.getPort() + "/webApp1/test"; + actual = restTemplate.getForObject(url, String.class); + assertEquals("Tested in /webApp1", actual); - @After - public void shutdown() throws Exception { - this.server.stop(); + url = "http://localhost:" + server.getPort() + "/webApp2/test"; + actual = restTemplate.getForObject(url, String.class); + assertEquals("Tested in /webApp2", actual); + } + finally { + server.stop(); + } } - @Test - public void basic() throws Exception { - RestTemplate restTemplate = new RestTemplate(); - String actual; + public void servletPathMapping() throws Exception { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.register(WebAppConfig.class); + context.refresh(); - actual = restTemplate.getForObject(createUrl("/webApp1/test"), String.class); - assertEquals("Tested in /webApp1", actual); + File base = new File(System.getProperty("java.io.tmpdir")); + TomcatHttpServer server = new TomcatHttpServer(base.getAbsolutePath()); + server.setContextPath("/app"); + server.setServletMapping("/api/*"); - actual = restTemplate.getForObject(createUrl("/webApp2/test"), String.class); - assertEquals("Tested in /webApp2", actual); - } + HttpHandler httpHandler = WebHttpHandlerBuilder.applicationContext(context).build(); + server.setHandler(httpHandler); - private String createUrl(String path) { - return "http://localhost:" + this.server.getPort() + path; - } + server.afterPropertiesSet(); + server.start(); + try { + RestTemplate restTemplate = new RestTemplate(); + String actual; - @EnableWebFlux - @Configuration - static class WebApp1Config { - - @Bean - public TestController testController() { - return new TestController(); + String url = "http://localhost:" + server.getPort() + "/app/api/test"; + actual = restTemplate.getForObject(url, String.class); + assertEquals("Tested in /app/api", actual); + } + finally { + server.stop(); } } + @EnableWebFlux @Configuration - static class WebApp2Config { + static class WebAppConfig { @Bean public TestController testController() {