Refactor Servlet 3 async support
As a result of the refactoring, the AsyncContext dispatch mechanism is used much more centrally. Effectively every asynchronously processed request involves one initial (container) thread, a second thread to produce the handler return value asynchronously, and a third thread as a result of a dispatch back to the container to resume processing of the asynchronous resuilt. Other updates include the addition of a MockAsyncContext and support of related request method in the test packages of spring-web and spring-webmvc. Also an upgrade of a Jetty test dependency required to make tests pass. Issue: SPR-9433
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* Copyright 2002-2012 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.mock.web;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.AsyncContext;
|
||||
import javax.servlet.AsyncEvent;
|
||||
import javax.servlet.AsyncListener;
|
||||
import javax.servlet.DispatcherType;
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.ServletRequest;
|
||||
import javax.servlet.ServletResponse;
|
||||
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.web.util.WebUtils;
|
||||
|
||||
/**
|
||||
* Mock implementation of the {@link AsyncContext} interface.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 3.2
|
||||
*/
|
||||
public class MockAsyncContext implements AsyncContext {
|
||||
|
||||
private final ServletRequest request;
|
||||
|
||||
private final ServletResponse response;
|
||||
|
||||
private final MockHttpServletRequest mockRequest;
|
||||
|
||||
private final List<AsyncListener> listeners = new ArrayList<AsyncListener>();
|
||||
|
||||
private String dispatchPath;
|
||||
|
||||
private long timeout = 10 * 60 * 1000L;
|
||||
|
||||
public MockAsyncContext(ServletRequest request, ServletResponse response) {
|
||||
this.request = request;
|
||||
this.response = response;
|
||||
this.mockRequest = WebUtils.getNativeRequest(request, MockHttpServletRequest.class);
|
||||
}
|
||||
|
||||
public ServletRequest getRequest() {
|
||||
return this.request;
|
||||
}
|
||||
|
||||
public ServletResponse getResponse() {
|
||||
return this.response;
|
||||
}
|
||||
|
||||
public boolean hasOriginalRequestAndResponse() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public String getDispatchPath() {
|
||||
return this.dispatchPath;
|
||||
}
|
||||
|
||||
public void dispatch() {
|
||||
dispatch(null);
|
||||
}
|
||||
|
||||
public void dispatch(String path) {
|
||||
dispatch(null, path);
|
||||
}
|
||||
|
||||
public void dispatch(ServletContext context, String path) {
|
||||
this.dispatchPath = path;
|
||||
if (this.mockRequest != null) {
|
||||
this.mockRequest.setDispatcherType(DispatcherType.ASYNC);
|
||||
this.mockRequest.setAsyncStarted(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void complete() {
|
||||
if (this.mockRequest != null) {
|
||||
this.mockRequest.setAsyncStarted(false);
|
||||
}
|
||||
for (AsyncListener listener : this.listeners) {
|
||||
try {
|
||||
listener.onComplete(new AsyncEvent(this, this.request, this.response));
|
||||
}
|
||||
catch (IOException e) {
|
||||
throw new IllegalStateException("AsyncListener failure", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void start(Runnable run) {
|
||||
}
|
||||
|
||||
public List<AsyncListener> getListeners() {
|
||||
return this.listeners;
|
||||
}
|
||||
|
||||
public void addListener(AsyncListener listener) {
|
||||
this.listeners.add(listener);
|
||||
}
|
||||
|
||||
public void addListener(AsyncListener listener, ServletRequest request, ServletResponse response) {
|
||||
this.listeners.add(listener);
|
||||
}
|
||||
|
||||
public <T extends AsyncListener> T createListener(Class<T> clazz) throws ServletException {
|
||||
return BeanUtils.instantiateClass(clazz);
|
||||
}
|
||||
|
||||
public long getTimeout() {
|
||||
return this.timeout;
|
||||
}
|
||||
|
||||
public void setTimeout(long timeout) {
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -102,7 +102,7 @@ public class MockHttpServletRequest implements HttpServletRequest {
|
||||
public static final String DEFAULT_REMOTE_HOST = "localhost";
|
||||
|
||||
private static final String CONTENT_TYPE_HEADER = "Content-Type";
|
||||
|
||||
|
||||
private static final String CHARSET_PREFIX = "charset=";
|
||||
|
||||
|
||||
@@ -190,6 +190,14 @@ public class MockHttpServletRequest implements HttpServletRequest {
|
||||
|
||||
private boolean requestedSessionIdFromURL = false;
|
||||
|
||||
private boolean asyncSupported = false;
|
||||
|
||||
private boolean asyncStarted = false;
|
||||
|
||||
private MockAsyncContext asyncContext;
|
||||
|
||||
private DispatcherType dispatcherType = DispatcherType.REQUEST;
|
||||
|
||||
|
||||
//---------------------------------------------------------------------
|
||||
// Constructors
|
||||
@@ -312,7 +320,7 @@ public class MockHttpServletRequest implements HttpServletRequest {
|
||||
this.characterEncoding = characterEncoding;
|
||||
updateContentTypeHeader();
|
||||
}
|
||||
|
||||
|
||||
private void updateContentTypeHeader() {
|
||||
if (this.contentType != null) {
|
||||
StringBuilder sb = new StringBuilder(this.contentType);
|
||||
@@ -679,7 +687,7 @@ public class MockHttpServletRequest implements HttpServletRequest {
|
||||
}
|
||||
doAddHeaderValue(name, value, false);
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
private void doAddHeaderValue(String name, Object value, boolean replace) {
|
||||
HeaderValueHolder header = HeaderValueHolder.getByName(this.headers, name);
|
||||
@@ -898,33 +906,54 @@ public class MockHttpServletRequest implements HttpServletRequest {
|
||||
//---------------------------------------------------------------------
|
||||
|
||||
public AsyncContext getAsyncContext() {
|
||||
throw new UnsupportedOperationException();
|
||||
return this.asyncContext;
|
||||
}
|
||||
|
||||
public void setAsyncContext(MockAsyncContext asyncContext) {
|
||||
this.asyncContext = asyncContext;
|
||||
}
|
||||
|
||||
public DispatcherType getDispatcherType() {
|
||||
throw new UnsupportedOperationException();
|
||||
return this.dispatcherType;
|
||||
}
|
||||
|
||||
public void setDispatcherType(DispatcherType dispatcherType) {
|
||||
this.dispatcherType = dispatcherType;
|
||||
}
|
||||
|
||||
public void setAsyncSupported(boolean asyncSupported) {
|
||||
this.asyncSupported = asyncSupported;
|
||||
}
|
||||
|
||||
public boolean isAsyncSupported() {
|
||||
throw new UnsupportedOperationException();
|
||||
return this.asyncSupported;
|
||||
}
|
||||
|
||||
public AsyncContext startAsync() {
|
||||
throw new UnsupportedOperationException();
|
||||
return startAsync(this, null);
|
||||
}
|
||||
|
||||
public AsyncContext startAsync(ServletRequest arg0, ServletResponse arg1) {
|
||||
throw new UnsupportedOperationException();
|
||||
public AsyncContext startAsync(ServletRequest request, ServletResponse response) {
|
||||
if (!this.asyncSupported) {
|
||||
throw new IllegalStateException("Async not supported");
|
||||
}
|
||||
this.asyncStarted = true;
|
||||
this.asyncContext = new MockAsyncContext(request, response);
|
||||
return this.asyncContext;
|
||||
}
|
||||
|
||||
public void setAsyncStarted(boolean asyncStarted) {
|
||||
this.asyncStarted = asyncStarted;
|
||||
}
|
||||
|
||||
public boolean isAsyncStarted() {
|
||||
throw new UnsupportedOperationException();
|
||||
return this.asyncStarted;
|
||||
}
|
||||
|
||||
public boolean authenticate(HttpServletResponse arg0) throws IOException, ServletException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
|
||||
public void addPart(Part part) {
|
||||
parts.put(part.getName(), part);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@ import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.web.context.request.async.AbstractDelegatingCallable;
|
||||
|
||||
/**
|
||||
* A test fixture with HandlerExecutionChain and mock handler interceptors.
|
||||
@@ -74,10 +73,6 @@ public class HandlerExecutionChainTests {
|
||||
expect(this.interceptor2.preHandle(this.request, this.response, this.handler)).andReturn(true);
|
||||
expect(this.interceptor3.preHandle(this.request, this.response, this.handler)).andReturn(true);
|
||||
|
||||
expect(this.interceptor1.getAsyncCallable(request, response, this.handler)).andReturn(new TestAsyncCallable());
|
||||
expect(this.interceptor2.getAsyncCallable(request, response, this.handler)).andReturn(new TestAsyncCallable());
|
||||
expect(this.interceptor3.getAsyncCallable(request, response, this.handler)).andReturn(new TestAsyncCallable());
|
||||
|
||||
this.interceptor1.postHandle(this.request, this.response, this.handler, mav);
|
||||
this.interceptor2.postHandle(this.request, this.response, this.handler, mav);
|
||||
this.interceptor3.postHandle(this.request, this.response, this.handler, mav);
|
||||
@@ -89,7 +84,6 @@ public class HandlerExecutionChainTests {
|
||||
replay(this.interceptor1, this.interceptor2, this.interceptor3);
|
||||
|
||||
this.chain.applyPreHandle(request, response);
|
||||
this.chain.pushInterceptorCallables(request, response);
|
||||
this.chain.applyPostHandle(request, response, mav);
|
||||
this.chain.triggerAfterCompletion(this.request, this.response, null);
|
||||
|
||||
@@ -104,28 +98,14 @@ public class HandlerExecutionChainTests {
|
||||
expect(this.interceptor2.preHandle(this.request, this.response, this.handler)).andReturn(true);
|
||||
expect(this.interceptor3.preHandle(this.request, this.response, this.handler)).andReturn(true);
|
||||
|
||||
expect(this.interceptor1.getAsyncCallable(request, response, this.handler)).andReturn(new TestAsyncCallable());
|
||||
expect(this.interceptor2.getAsyncCallable(request, response, this.handler)).andReturn(new TestAsyncCallable());
|
||||
expect(this.interceptor3.getAsyncCallable(request, response, this.handler)).andReturn(new TestAsyncCallable());
|
||||
|
||||
this.interceptor1.postHandleAfterAsyncStarted(request, response, this.handler);
|
||||
this.interceptor2.postHandleAfterAsyncStarted(request, response, this.handler);
|
||||
this.interceptor3.postHandleAfterAsyncStarted(request, response, this.handler);
|
||||
|
||||
this.interceptor1.postHandle(this.request, this.response, this.handler, mav);
|
||||
this.interceptor2.postHandle(this.request, this.response, this.handler, mav);
|
||||
this.interceptor3.postHandle(this.request, this.response, this.handler, mav);
|
||||
|
||||
this.interceptor3.afterCompletion(this.request, this.response, this.handler, null);
|
||||
this.interceptor2.afterCompletion(this.request, this.response, this.handler, null);
|
||||
this.interceptor1.afterCompletion(this.request, this.response, this.handler, null);
|
||||
this.interceptor1.afterConcurrentHandlingStarted(request, response);
|
||||
this.interceptor2.afterConcurrentHandlingStarted(request, response);
|
||||
this.interceptor3.afterConcurrentHandlingStarted(request, response);
|
||||
|
||||
replay(this.interceptor1, this.interceptor2, this.interceptor3);
|
||||
|
||||
this.chain.applyPreHandle(request, response);
|
||||
this.chain.pushInterceptorCallables(request, response);
|
||||
this.chain.popInterceptorCallables(request, response, true);
|
||||
this.chain.applyPostHandle(request, response, mav);
|
||||
this.chain.applyAfterConcurrentHandlingStarted(request, response);
|
||||
this.chain.triggerAfterCompletion(this.request, this.response, null);
|
||||
|
||||
verify(this.interceptor1, this.interceptor2, this.interceptor3);
|
||||
@@ -196,12 +176,4 @@ public class HandlerExecutionChainTests {
|
||||
verify(this.interceptor1, this.interceptor2, this.interceptor3);
|
||||
}
|
||||
|
||||
|
||||
private static class TestAsyncCallable extends AbstractDelegatingCallable {
|
||||
|
||||
public Object call() throws Exception {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -21,14 +21,14 @@ import static org.junit.Assert.assertEquals;
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.servlet.ServletContextHandler;
|
||||
import org.eclipse.jetty.servlet.ServletHolder;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.mortbay.jetty.Server;
|
||||
import org.mortbay.jetty.servlet.Context;
|
||||
import org.mortbay.jetty.servlet.ServletHolder;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
@@ -59,8 +59,8 @@ import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
|
||||
|
||||
/**
|
||||
* Test access to parts of a multipart request with {@link RequestPart}.
|
||||
*
|
||||
* Test access to parts of a multipart request with {@link RequestPart}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class RequestPartIntegrationTests {
|
||||
@@ -71,39 +71,41 @@ public class RequestPartIntegrationTests {
|
||||
|
||||
private static String baseUrl;
|
||||
|
||||
|
||||
|
||||
@BeforeClass
|
||||
public static void startServer() throws Exception {
|
||||
|
||||
|
||||
int port = FreePortScanner.getFreePort();
|
||||
baseUrl = "http://localhost:" + port;
|
||||
|
||||
server = new Server(port);
|
||||
Context context = new Context(server, "/");
|
||||
ServletContextHandler handler = new ServletContextHandler();
|
||||
handler.setContextPath("/");
|
||||
|
||||
Class<?> config = CommonsMultipartResolverTestConfig.class;
|
||||
ServletHolder commonsResolverServlet = new ServletHolder(DispatcherServlet.class);
|
||||
commonsResolverServlet.setInitParameter("contextConfigLocation", config.getName());
|
||||
commonsResolverServlet.setInitParameter("contextClass", AnnotationConfigWebApplicationContext.class.getName());
|
||||
context.addServlet(commonsResolverServlet, "/commons-resolver/*");
|
||||
handler.addServlet(commonsResolverServlet, "/commons-resolver/*");
|
||||
|
||||
config = StandardMultipartResolverTestConfig.class;
|
||||
ServletHolder standardResolverServlet = new ServletHolder(DispatcherServlet.class);
|
||||
standardResolverServlet.setInitParameter("contextConfigLocation", config.getName());
|
||||
standardResolverServlet.setInitParameter("contextClass", AnnotationConfigWebApplicationContext.class.getName());
|
||||
context.addServlet(standardResolverServlet, "/standard-resolver/*");
|
||||
handler.addServlet(standardResolverServlet, "/standard-resolver/*");
|
||||
|
||||
// TODO: add Servlet 3.0 test case without MultipartResolver
|
||||
// TODO: add Servlet 3.0 test case without MultipartResolver
|
||||
|
||||
server.setHandler(handler);
|
||||
server.start();
|
||||
}
|
||||
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
XmlAwareFormHttpMessageConverter converter = new XmlAwareFormHttpMessageConverter();
|
||||
converter.setPartConverters(Arrays.<HttpMessageConverter<?>>asList(
|
||||
new ResourceHttpMessageConverter(), new MappingJacksonHttpMessageConverter()));
|
||||
|
||||
|
||||
restTemplate = new RestTemplate(new HttpComponentsClientHttpRequestFactory());
|
||||
restTemplate.setMessageConverters(Arrays.<HttpMessageConverter<?>>asList(converter));
|
||||
}
|
||||
@@ -115,7 +117,7 @@ public class RequestPartIntegrationTests {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Test
|
||||
public void commonsMultipartResolver() throws Exception {
|
||||
testCreate(baseUrl + "/commons-resolver/test");
|
||||
@@ -147,7 +149,7 @@ public class RequestPartIntegrationTests {
|
||||
return new RequestPartTestController();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Configuration
|
||||
static class CommonsMultipartResolverTestConfig extends RequestPartTestConfig {
|
||||
|
||||
@@ -166,7 +168,6 @@ public class RequestPartIntegrationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Controller
|
||||
private static class RequestPartTestController {
|
||||
|
||||
@@ -178,10 +179,10 @@ public class RequestPartIntegrationTests {
|
||||
return new ResponseEntity<Object>(headers, HttpStatus.CREATED);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static class TestData {
|
||||
|
||||
|
||||
private String name;
|
||||
|
||||
public TestData() {
|
||||
@@ -200,5 +201,5 @@ public class RequestPartIntegrationTests {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user