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:
Rossen Stoyanchev
2012-07-24 16:00:05 -04:00
parent 026ee846c7
commit 529e62921d
47 changed files with 1837 additions and 1741 deletions

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}