Add contextPath support for reactive web applications

This commit introduces support for running multiple HttpHandler's under
distinct context paths which effectively allows running multiple
applications on the same server. ContextPathIntegrationTests contains
an example of two applications with different context paths.

In order to support this the HttpHandler adapters for all supported
runtimes now have a common base class HttpHandlerAdapterSupport
which has two constructor choices -- one with a single HttpHandler and
another with a Map<String, HttpHandler>.

Note that in addition to the contextPath under which an HttpHandler is
configured there may also be a "native" contextPath under which the
native runtime adapter is configured (e.g. Servlet containers). In such
cases the contextPath is a combination of the native contextPath and
the contextPath assigned to the HttpHandler. See for example
HttpHandlerAdapterSupportTests.

Issue: SPR-14726
This commit is contained in:
Rossen Stoyanchev 2016-10-20 17:26:41 -04:00
parent c2fdc9103b
commit b92d541ba0
17 changed files with 718 additions and 75 deletions

View File

@ -0,0 +1,126 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed 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.springframework.web.reactive.result.method.annotation;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
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.util.SocketUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.reactive.DispatcherHandler;
import org.springframework.web.reactive.config.EnableWebReactive;
import static org.junit.Assert.assertEquals;
/**
* Integration tests that demonstrate running multiple applications under
* different context paths.
*
* @author Rossen Stoyanchev
*/
@SuppressWarnings({"unused", "WeakerAccess"})
public class ContextPathIntegrationTests {
private ReactorHttpServer server;
@Before
public void setUp() throws Exception {
AnnotationConfigApplicationContext context1 = new AnnotationConfigApplicationContext();
context1.register(WebApp1Config.class);
context1.refresh();
AnnotationConfigApplicationContext context2 = new AnnotationConfigApplicationContext();
context2.register(WebApp2Config.class);
context2.refresh();
HttpHandler webApp1Handler = DispatcherHandler.toHttpHandler(context1);
HttpHandler webApp2Handler = DispatcherHandler.toHttpHandler(context2);
this.server = new ReactorHttpServer();
this.server.setPort(SocketUtils.findAvailableTcpPort());
this.server.registerHttpHandler("/webApp1", webApp1Handler);
this.server.registerHttpHandler("/webApp2", webApp2Handler);
this.server.afterPropertiesSet();
this.server.start();
}
@After
public void tearDown() throws Exception {
this.server.stop();
}
@Test
public void basic() throws Exception {
RestTemplate restTemplate = new RestTemplate();
String actual;
actual = restTemplate.getForObject(createUrl("/webApp1/test"), String.class);
assertEquals("Tested in /webApp1", actual);
actual = restTemplate.getForObject(createUrl("/webApp2/test"), String.class);
assertEquals("Tested in /webApp2", actual);
}
private String createUrl(String path) {
return "http://localhost:" + this.server.getPort() + path;
}
@EnableWebReactive
@Configuration
static class WebApp1Config {
@Bean
public TestController testController() {
return new TestController();
}
}
@EnableWebReactive
@Configuration
static class WebApp2Config {
@Bean
public TestController testController() {
return new TestController();
}
}
@RestController
static class TestController {
@GetMapping("/test")
public String handle(ServerHttpRequest request) {
return "Tested in " + request.getContextPath();
}
}
}

View File

@ -0,0 +1,152 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed 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.springframework.http.server.reactive;
import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Mono;
import org.springframework.http.HttpStatus;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Base class for adapters from native runtime HTTP request handlers to a
* reactive {@link HttpHandler} contract.
*
* <p>Provides support for delegating incoming requests to a single or multiple
* {@link HttpHandler}s each mapped to a distinct context path. In either case
* sub-classes simply use {@link #getHttpHandler()} to access the handler to
* delegate incoming requests to.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
public abstract class HttpHandlerAdapterSupport {
protected final Log logger = LogFactory.getLog(getClass());
private final HttpHandler httpHandler;
/**
* Constructor with a single {@code HttpHandler} to use for all requests.
* @param httpHandler the handler to use
*/
public HttpHandlerAdapterSupport(HttpHandler httpHandler) {
Assert.notNull(httpHandler, "'httpHandler' is required");
this.httpHandler = httpHandler;
}
/**
* Constructor with {@code HttpHandler}s mapped to distinct context paths.
* Context paths must start but not end with "/" and must be encoded.
*
* <p>At request time context paths are compared against the "raw" path of
* the request URI in the order in which they are provided. The first one
* to match is chosen. If none match the response status is set to 404.
*
* @param handlerMap map with context paths and {@code HttpHandler}s.
* @see ServerHttpRequest#getContextPath()
*/
public HttpHandlerAdapterSupport(Map<String, HttpHandler> handlerMap) {
this.httpHandler = new CompositeHttpHandler(handlerMap);
}
/**
* Return the {@link HttpHandler} to delegate incoming requests to.
*/
public HttpHandler getHttpHandler() {
return this.httpHandler;
}
/**
* Composite HttpHandler that selects the handler to use by context path.
*/
private static class CompositeHttpHandler implements HttpHandler {
private final Map<String, HttpHandler> handlerMap;
public CompositeHttpHandler(Map<String, HttpHandler> handlerMap) {
Assert.notEmpty(handlerMap);
this.handlerMap = initHandlerMap(handlerMap);
}
private static Map<String, HttpHandler> initHandlerMap(Map<String, HttpHandler> inputMap) {
inputMap.keySet().stream().forEach(CompositeHttpHandler::validateContextPath);
return new LinkedHashMap<>(inputMap);
}
private static void validateContextPath(String contextPath) {
Assert.hasText(contextPath, "contextPath must not be empty");
if (!contextPath.equals("/")) {
Assert.isTrue(contextPath.startsWith("/"), "contextPath must begin with '/'");
Assert.isTrue(!contextPath.endsWith("/"), "contextPath must not end with '/'");
}
}
@Override
public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response) {
String path = getPathToUse(request);
return this.handlerMap.entrySet().stream()
.filter(entry -> path.startsWith(entry.getKey()))
.findFirst()
.map(entry -> {
HttpHandler handler = entry.getValue();
ServerHttpRequest req = new ContextPathRequestDecorator(request, entry.getKey());
return handler.handle(req, response);
})
.orElseGet(() -> {
response.setStatusCode(HttpStatus.NOT_FOUND);
response.setComplete();
return Mono.empty();
});
}
/** Strip the context path from native request if any */
private String getPathToUse(ServerHttpRequest request) {
String path = request.getURI().getRawPath();
String contextPath = request.getContextPath();
if (!StringUtils.hasText(contextPath)) {
return path;
}
int contextLength = contextPath.length();
return (path.length() > contextLength ? path.substring(contextLength) : "");
}
}
private static class ContextPathRequestDecorator extends ServerHttpRequestDecorator {
private final String contextPath;
public ContextPathRequestDecorator(ServerHttpRequest delegate, String contextPath) {
super(delegate);
this.contextPath = delegate.getContextPath() + contextPath;
}
@Override
public String getContextPath() {
return this.contextPath;
}
}
}

View File

@ -16,16 +16,14 @@
package org.springframework.http.server.reactive;
import java.util.Map;
import java.util.function.Function;
import io.netty.handler.codec.http.HttpResponseStatus;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Mono;
import reactor.ipc.netty.http.HttpChannel;
import org.springframework.core.io.buffer.NettyDataBufferFactory;
import org.springframework.util.Assert;
/**
* Adapt {@link HttpHandler} to the Reactor Netty channel handling function.
@ -33,26 +31,27 @@ import org.springframework.util.Assert;
* @author Stephane Maldini
* @since 5.0
*/
public class ReactorHttpHandlerAdapter implements Function<HttpChannel, Mono<Void>> {
private static final Log logger = LogFactory.getLog(ReactorHttpHandlerAdapter.class);
private final HttpHandler delegate;
public class ReactorHttpHandlerAdapter extends HttpHandlerAdapterSupport
implements Function<HttpChannel, Mono<Void>> {
public ReactorHttpHandlerAdapter(HttpHandler delegate) {
Assert.notNull(delegate, "HttpHandler delegate is required");
this.delegate = delegate;
public ReactorHttpHandlerAdapter(HttpHandler httpHandler) {
super(httpHandler);
}
public ReactorHttpHandlerAdapter(Map<String, HttpHandler> handlerMap) {
super(handlerMap);
}
@Override
public Mono<Void> apply(HttpChannel channel) {
NettyDataBufferFactory bufferFactory = new NettyDataBufferFactory(channel.delegate().alloc());
ReactorServerHttpRequest adaptedRequest = new ReactorServerHttpRequest(channel, bufferFactory);
ReactorServerHttpResponse adaptedResponse = new ReactorServerHttpResponse(channel, bufferFactory);
return this.delegate.handle(adaptedRequest, adaptedResponse)
NettyDataBufferFactory bufferFactory = new NettyDataBufferFactory(channel.delegate().alloc());
ReactorServerHttpRequest request = new ReactorServerHttpRequest(channel, bufferFactory);
ReactorServerHttpResponse response = new ReactorServerHttpResponse(channel, bufferFactory);
return getHttpHandler().handle(request, response)
.otherwise(ex -> {
logger.error("Could not complete request", ex);
channel.status(HttpResponseStatus.INTERNAL_SERVER_ERROR);

View File

@ -16,7 +16,10 @@
package org.springframework.http.server.reactive;
import java.util.Map;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.reactivex.netty.protocol.http.server.HttpServerRequest;
import io.reactivex.netty.protocol.http.server.HttpServerResponse;
@ -37,29 +40,33 @@ import org.springframework.util.Assert;
* @author Rossen Stoyanchev
* @since 5.0
*/
public class RxNettyHttpHandlerAdapter implements RequestHandler<ByteBuf, ByteBuf> {
private static final Log logger = LogFactory.getLog(RxNettyHttpHandlerAdapter.class);
private final HttpHandler delegate;
public class RxNettyHttpHandlerAdapter extends HttpHandlerAdapterSupport
implements RequestHandler<ByteBuf, ByteBuf> {
public RxNettyHttpHandlerAdapter(HttpHandler delegate) {
Assert.notNull(delegate, "HttpHandler delegate is required");
this.delegate = delegate;
public RxNettyHttpHandlerAdapter(HttpHandler httpHandler) {
super(httpHandler);
}
public RxNettyHttpHandlerAdapter(Map<String, HttpHandler> handlerMap) {
super(handlerMap);
}
@Override
public Observable<Void> handle(HttpServerRequest<ByteBuf> request, HttpServerResponse<ByteBuf> response) {
NettyDataBufferFactory bufferFactory = new NettyDataBufferFactory(response.unsafeNettyChannel().alloc());
RxNettyServerHttpRequest adaptedRequest = new RxNettyServerHttpRequest(request, bufferFactory);
RxNettyServerHttpResponse adaptedResponse = new RxNettyServerHttpResponse(response, bufferFactory);
public Observable<Void> handle(HttpServerRequest<ByteBuf> nativeRequest,
HttpServerResponse<ByteBuf> nativeResponse) {
Publisher<Void> result = this.delegate.handle(adaptedRequest, adaptedResponse)
ByteBufAllocator allocator = nativeResponse.unsafeNettyChannel().alloc();
NettyDataBufferFactory bufferFactory = new NettyDataBufferFactory(allocator);
RxNettyServerHttpRequest request = new RxNettyServerHttpRequest(nativeRequest, bufferFactory);
RxNettyServerHttpResponse response = new RxNettyServerHttpResponse(nativeResponse, bufferFactory);
Publisher<Void> result = getHttpHandler().handle(request, response)
.otherwise(ex -> {
logger.error("Could not complete request", ex);
response.setStatus(HttpResponseStatus.INTERNAL_SERVER_ERROR);
nativeResponse.setStatus(HttpResponseStatus.INTERNAL_SERVER_ERROR);
return Mono.empty();
})
.doOnSuccess(aVoid -> logger.debug("Successfully completed request"));

View File

@ -30,9 +30,6 @@ import org.springframework.util.MultiValueMap;
*/
public interface ServerHttpRequest extends HttpRequest, ReactiveHttpInputMessage {
// TODO: https://jira.spring.io/browse/SPR-14726
/**
* Returns the portion of the URL path that represents the context path for
* the current {@link HttpHandler}. The context path is always at the

View File

@ -0,0 +1,90 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed 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.springframework.http.server.reactive;
import java.net.URI;
import reactor.core.publisher.Flux;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.util.Assert;
import org.springframework.util.MultiValueMap;
/**
* Wraps another {@link ServerHttpRequest} and delegates all methods to it.
* Sub-classes can override specific methods selectively.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
public class ServerHttpRequestDecorator implements ServerHttpRequest {
private final ServerHttpRequest delegate;
public ServerHttpRequestDecorator(ServerHttpRequest delegate) {
Assert.notNull(delegate, "'delegate' is required.");
this.delegate = delegate;
}
public ServerHttpRequest getDelegate() {
return this.delegate;
}
// ServerHttpRequest delegation methods...
@Override
public HttpMethod getMethod() {
return getDelegate().getMethod();
}
@Override
public URI getURI() {
return getDelegate().getURI();
}
@Override
public MultiValueMap<String, String> getQueryParams() {
return getDelegate().getQueryParams();
}
@Override
public HttpHeaders getHeaders() {
return getDelegate().getHeaders();
}
@Override
public MultiValueMap<String, HttpCookie> getCookies() {
return getDelegate().getCookies();
}
@Override
public Flux<DataBuffer> getBody() {
return getDelegate().getBody();
}
@Override
public String toString() {
return getClass().getSimpleName() + " [delegate=" + getDelegate() + "]";
}
}

View File

@ -17,17 +17,19 @@
package org.springframework.http.server.reactive;
import java.io.IOException;
import java.util.Map;
import javax.servlet.AsyncContext;
import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;
import javax.servlet.ServletException;
import javax.servlet.Servlet;
import javax.servlet.ServletConfig;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
@ -45,15 +47,12 @@ import org.springframework.util.Assert;
*/
@WebServlet(asyncSupported = true)
@SuppressWarnings("serial")
public class ServletHttpHandlerAdapter extends HttpServlet {
public class ServletHttpHandlerAdapter extends HttpHandlerAdapterSupport
implements Servlet {
private static final int DEFAULT_BUFFER_SIZE = 8192;
private static final Log logger = LogFactory.getLog(ServletHttpHandlerAdapter.class);
private final HttpHandler handler;
// 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);
@ -61,13 +60,12 @@ public class ServletHttpHandlerAdapter extends HttpServlet {
private int bufferSize = DEFAULT_BUFFER_SIZE;
/**
* Create a new {@code ServletHttpHandlerAdapter} with the given HTTP handler.
* @param handler the handler
*/
public ServletHttpHandlerAdapter(HttpHandler handler) {
Assert.notNull(handler, "HttpHandler must not be null");
this.handler = handler;
public ServletHttpHandlerAdapter(HttpHandler httpHandler) {
super(httpHandler);
}
public ServletHttpHandlerAdapter(Map<String, HttpHandler> handlerMap) {
super(handlerMap);
}
@ -76,28 +74,56 @@ public class ServletHttpHandlerAdapter extends HttpServlet {
this.dataBufferFactory = dataBufferFactory;
}
public DataBufferFactory getDataBufferFactory() {
return this.dataBufferFactory;
}
public void setBufferSize(int bufferSize) {
Assert.isTrue(bufferSize > 0);
this.bufferSize = bufferSize;
}
public int getBufferSize() {
return this.bufferSize;
}
@Override
protected void service(HttpServletRequest servletRequest, HttpServletResponse servletResponse)
throws ServletException, IOException {
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws IOException {
ServletServerHttpRequest request = new ServletServerHttpRequest(
((HttpServletRequest) servletRequest), getDataBufferFactory(), getBufferSize());
ServletServerHttpResponse response = new ServletServerHttpResponse(
((HttpServletResponse) servletResponse), getDataBufferFactory(), getBufferSize());
AsyncContext asyncContext = servletRequest.startAsync();
ServletServerHttpRequest request = new ServletServerHttpRequest(
servletRequest, this.dataBufferFactory, this.bufferSize);
ServletServerHttpResponse response = new ServletServerHttpResponse(
servletResponse, this.dataBufferFactory, this.bufferSize);
asyncContext.addListener(new EventHandlingAsyncListener(request, response));
HandlerResultSubscriber resultSubscriber = new HandlerResultSubscriber(asyncContext);
this.handler.handle(request, response).subscribe(resultSubscriber);
getHttpHandler().handle(request, response).subscribe(resultSubscriber);
}
// Other Servlet methods...
@Override
public void init(ServletConfig config) {
}
@Override
public ServletConfig getServletConfig() {
return null;
}
@Override
public String getServletInfo() {
return "";
}
@Override
public void destroy() {
}
private static class HandlerResultSubscriber implements Subscriber<Void> {
private class HandlerResultSubscriber implements Subscriber<Void> {
private final AsyncContext asyncContext;

View File

@ -16,9 +16,9 @@
package org.springframework.http.server.reactive;
import java.util.Map;
import io.undertow.server.HttpServerExchange;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
@ -34,19 +34,18 @@ import org.springframework.util.Assert;
* @author Arjen Poutsma
* @since 5.0
*/
public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandler {
private static final Log logger = LogFactory.getLog(UndertowHttpHandlerAdapter.class);
private final HttpHandler delegate;
public class UndertowHttpHandlerAdapter extends HttpHandlerAdapterSupport
implements io.undertow.server.HttpHandler {
private DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory(false);
public UndertowHttpHandlerAdapter(HttpHandler delegate) {
Assert.notNull(delegate, "HttpHandler delegate is required");
this.delegate = delegate;
public UndertowHttpHandlerAdapter(HttpHandler httpHandler) {
super(httpHandler);
}
public UndertowHttpHandlerAdapter(Map<String, HttpHandler> handlerMap) {
super(handlerMap);
}
@ -58,10 +57,11 @@ public class UndertowHttpHandlerAdapter implements io.undertow.server.HttpHandle
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
ServerHttpRequest request = new UndertowServerHttpRequest(exchange, this.dataBufferFactory);
ServerHttpResponse response = new UndertowServerHttpResponse(exchange, this.dataBufferFactory);
this.delegate.handle(request, response).subscribe(new Subscriber<Void>() {
getHttpHandler().handle(request, response).subscribe(new Subscriber<Void>() {
@Override
public void onSubscribe(Subscription subscription) {
subscription.request(Long.MAX_VALUE);

View File

@ -32,6 +32,7 @@ import org.springframework.util.Assert;
* return the authenticated user for the request.
*
* @author Rossen Stoyanchev
* @since 5.0
*/
public class ServerWebExchangeDecorator implements ServerWebExchange {

View File

@ -66,7 +66,8 @@ public class HttpRequestPathHelper {
if (!StringUtils.hasText(contextPath)) {
return path;
}
return (path.length() > contextPath.length() ? path.substring(contextPath.length()) : "");
int contextLength = contextPath.length();
return (path.length() > contextLength ? path.substring(contextLength) : "");
}
private String decode(ServerWebExchange exchange, String path) {

View File

@ -0,0 +1,181 @@
/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed 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.springframework.http.server.reactive;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
import org.junit.Test;
import reactor.core.publisher.Mono;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
import static junit.framework.TestCase.assertFalse;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
/**
* Unit tests for {@link HttpHandlerAdapterSupport}.
* @author Rossen Stoyanchev
*/
public class HttpHandlerAdapterSupportTests {
@Test
public void invalidContextPath() throws Exception {
testInvalidContextPath(" ", "contextPath must not be empty");
testInvalidContextPath("path", "contextPath must begin with '/'");
testInvalidContextPath("/path/", "contextPath must not end with '/'");
}
private void testInvalidContextPath(String contextPath, String errorMessage) {
try {
new TestHttpHandlerAdapter(new TestHttpHandler(contextPath));
fail();
}
catch (IllegalArgumentException ex) {
assertEquals(errorMessage, ex.getMessage());
}
}
@Test
public void match() throws Exception {
TestHttpHandler handler1 = new TestHttpHandler("/path");
TestHttpHandler handler2 = new TestHttpHandler("/another/path");
TestHttpHandler handler3 = new TestHttpHandler("/yet/another/path");
testPath("/another/path/and/more", handler1, handler2, handler3);
assertInvoked(handler2);
assertNotInvoked(handler1, handler3);
}
@Test
public void matchWithContextPathEqualToPath() throws Exception {
TestHttpHandler handler1 = new TestHttpHandler("/path");
TestHttpHandler handler2 = new TestHttpHandler("/another/path");
TestHttpHandler handler3 = new TestHttpHandler("/yet/another/path");
testPath("/path", handler1, handler2, handler3);
assertInvoked(handler1);
assertNotInvoked(handler2, handler3);
}
@Test
public void matchWithNativeContextPath() throws Exception {
MockServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, "/yet/another/path");
request.setContextPath("/yet");
TestHttpHandler handler = new TestHttpHandler("/another/path");
new TestHttpHandlerAdapter(handler).handle(request);
assertTrue(handler.wasInvoked());
assertEquals("/yet/another/path", handler.getRequest().getContextPath());
}
@Test
public void notFound() throws Exception {
TestHttpHandler handler1 = new TestHttpHandler("/path");
TestHttpHandler handler2 = new TestHttpHandler("/another/path");
ServerHttpResponse response = testPath("/yet/another/path", handler1, handler2);
assertNotInvoked(handler1, handler2);
assertEquals(HttpStatus.NOT_FOUND, response.getStatusCode());
}
private ServerHttpResponse testPath(String path, TestHttpHandler... handlers) {
TestHttpHandlerAdapter adapter = new TestHttpHandlerAdapter(handlers);
return adapter.handle(path);
}
private void assertInvoked(TestHttpHandler handler) {
assertTrue(handler.wasInvoked());
assertEquals(handler.getContextPath(), handler.getRequest().getContextPath());
}
private void assertNotInvoked(TestHttpHandler... handlers) {
Arrays.stream(handlers).forEach(handler -> assertFalse(handler.wasInvoked()));
}
@SuppressWarnings("WeakerAccess")
private static class TestHttpHandlerAdapter extends HttpHandlerAdapterSupport {
public TestHttpHandlerAdapter(TestHttpHandler... handlers) {
super(initHandlerMap(handlers));
}
private static Map<String, HttpHandler> initHandlerMap(TestHttpHandler... testHandlers) {
Map<String, HttpHandler> result = new LinkedHashMap<>();
Arrays.stream(testHandlers).forEachOrdered(h -> result.put(h.getContextPath(), h));
return result;
}
public ServerHttpResponse handle(String path) {
ServerHttpRequest request = new MockServerHttpRequest(HttpMethod.GET, path);
return handle(request);
}
public ServerHttpResponse handle(ServerHttpRequest request) {
ServerHttpResponse response = new MockServerHttpResponse();
getHttpHandler().handle(request, response);
return response;
}
}
@SuppressWarnings("WeakerAccess")
private static class TestHttpHandler implements HttpHandler {
private final String contextPath;
private ServerHttpRequest request;
public TestHttpHandler(String contextPath) {
this.contextPath = contextPath;
}
public String getContextPath() {
return this.contextPath;
}
public boolean wasInvoked() {
return this.request != null;
}
public ServerHttpRequest getRequest() {
return this.request;
}
@Override
public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response) {
this.request = request;
return Mono.empty();
}
}
}

View File

@ -16,6 +16,9 @@
package org.springframework.http.server.reactive.bootstrap;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.util.SocketUtils;
@ -30,6 +33,9 @@ public class HttpServerSupport {
private HttpHandler httpHandler;
private Map<String, HttpHandler> handlerMap;
public void setHost(String host) {
this.host = host;
}
@ -57,4 +63,15 @@ public class HttpServerSupport {
return this.httpHandler;
}
public void registerHttpHandler(String contextPath, HttpHandler handler) {
if (this.handlerMap == null) {
this.handlerMap = new LinkedHashMap<>();
}
this.handlerMap.put(contextPath, handler);
}
public Map<String, HttpHandler> getHttpHandlerMap() {
return this.handlerMap;
}
}

View File

@ -20,6 +20,7 @@ import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.servlet.ServletContextHandler;
import org.eclipse.jetty.servlet.ServletHolder;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.http.server.reactive.ServletHttpHandlerAdapter;
@ -39,7 +40,7 @@ public class JettyHttpServer extends HttpServerSupport implements HttpServer, In
public void afterPropertiesSet() throws Exception {
this.jettyServer = new Server();
ServletHttpHandlerAdapter servlet = new ServletHttpHandlerAdapter(getHttpHandler());
ServletHttpHandlerAdapter servlet = initServletHttpHandlerAdapter();
ServletHolder servletHolder = new ServletHolder(servlet);
ServletContextHandler contextHandler = new ServletContextHandler(this.jettyServer, "", false, false);
@ -51,6 +52,17 @@ public class JettyHttpServer extends HttpServerSupport implements HttpServer, In
this.jettyServer.addConnector(connector);
}
@NotNull
private ServletHttpHandlerAdapter initServletHttpHandlerAdapter() {
if (getHttpHandlerMap() != null) {
return new ServletHttpHandlerAdapter(getHttpHandlerMap());
}
else {
Assert.notNull(getHttpHandler());
return new ServletHttpHandlerAdapter(getHttpHandler());
}
}
@Override
public void start() {
if (!this.running) {

View File

@ -35,9 +35,13 @@ public class ReactorHttpServer extends HttpServerSupport implements HttpServer,
@Override
public void afterPropertiesSet() throws Exception {
Assert.notNull(getHttpHandler());
this.reactorHandler = new ReactorHttpHandlerAdapter(getHttpHandler());
if (getHttpHandlerMap() != null) {
this.reactorHandler = new ReactorHttpHandlerAdapter(getHttpHandlerMap());
}
else {
Assert.notNull(getHttpHandler());
this.reactorHandler = new ReactorHttpHandlerAdapter(getHttpHandler());
}
this.reactorServer = reactor.ipc.netty.http.HttpServer.create(getHost(), getPort());
}

View File

@ -37,8 +37,14 @@ public class RxNettyHttpServer extends HttpServerSupport implements HttpServer {
@Override
public void afterPropertiesSet() throws Exception {
Assert.notNull(getHttpHandler());
this.rxNettyHandler = new RxNettyHttpHandlerAdapter(getHttpHandler());
if (getHttpHandlerMap() != null) {
this.rxNettyHandler = new RxNettyHttpHandlerAdapter(getHttpHandlerMap());
}
else {
Assert.notNull(getHttpHandler());
this.rxNettyHandler = new RxNettyHttpHandlerAdapter(getHttpHandler());
}
this.rxNettyServer = io.reactivex.netty.protocol.http.server.HttpServer
.newServer(new InetSocketAddress(getHost(), getPort()));

View File

@ -21,9 +21,11 @@ import java.io.File;
import org.apache.catalina.Context;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.startup.Tomcat;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.http.server.reactive.ServletHttpHandlerAdapter;
import org.springframework.util.Assert;
/**
* @author Rossen Stoyanchev
@ -54,7 +56,7 @@ public class TomcatHttpServer extends HttpServerSupport implements HttpServer, I
this.tomcatServer.setHostname(getHost());
this.tomcatServer.setPort(getPort());
ServletHttpHandlerAdapter servlet = new ServletHttpHandlerAdapter(getHttpHandler());
ServletHttpHandlerAdapter servlet = initServletHttpHandlerAdapter();
File base = new File(System.getProperty("java.io.tmpdir"));
Context rootContext = tomcatServer.addContext("", base.getAbsolutePath());
@ -62,6 +64,17 @@ public class TomcatHttpServer extends HttpServerSupport implements HttpServer, I
rootContext.addServletMappingDecoded("/", "httpHandlerServlet");
}
@NotNull
private ServletHttpHandlerAdapter initServletHttpHandlerAdapter() {
if (getHttpHandlerMap() != null) {
return new ServletHttpHandlerAdapter(getHttpHandlerMap());
}
else {
Assert.notNull(getHttpHandler());
return new ServletHttpHandlerAdapter(getHttpHandler());
}
}
@Override
public void start() {

View File

@ -42,6 +42,8 @@ public class MockServerHttpRequest implements ServerHttpRequest {
private URI url;
private String contextPath = "";
private final MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
private final HttpHeaders headers = new HttpHeaders();
@ -99,6 +101,15 @@ public class MockServerHttpRequest implements ServerHttpRequest {
return this.url;
}
public void setContextPath(String contextPath) {
this.contextPath = contextPath;
}
@Override
public String getContextPath() {
return this.contextPath;
}
public MockServerHttpRequest addHeader(String name, String value) {
getHeaders().add(name, value);
return this;