Skip to content

Commit 4b9aad8

Browse files
committed
Refactor async result handling in Spring MVC Test
This change removes the use of a CountDownLatch to wait for the asynchronously computed controller method return value. Instead we check in a loop every 200 milliseconds if the result has been set. If the result is not set within the specified amount of time to wait an IllegalStateException is raised. Additional changes: - Use AtomicReference to hold the async result - Remove @ignore annotations on AsyncTests methods - Remove checks for the presence of Servlet 3 Issue: SPR-11516
1 parent 5fe436c commit 4b9aad8

File tree

5 files changed

+96
-111
lines changed

5 files changed

+96
-111
lines changed

spring-test-mvc/src/main/java/org/springframework/test/web/servlet/DefaultMvcResult.java

Lines changed: 27 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2012 the original author or authors.
2+
* Copyright 2002-2014 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -15,13 +15,13 @@
1515
*/
1616
package org.springframework.test.web.servlet;
1717

18-
import java.util.concurrent.CountDownLatch;
19-
import java.util.concurrent.TimeUnit;
18+
import java.util.concurrent.atomic.AtomicReference;
2019

2120
import javax.servlet.http.HttpServletRequest;
2221

2322
import org.springframework.mock.web.MockHttpServletRequest;
2423
import org.springframework.mock.web.MockHttpServletResponse;
24+
import org.springframework.util.Assert;
2525
import org.springframework.web.servlet.FlashMap;
2626
import org.springframework.web.servlet.HandlerInterceptor;
2727
import org.springframework.web.servlet.ModelAndView;
@@ -36,6 +36,9 @@
3636
*/
3737
class DefaultMvcResult implements MvcResult {
3838

39+
private static final Object RESULT_NONE = new Object();
40+
41+
3942
private final MockHttpServletRequest mockRequest;
4043

4144
private final MockHttpServletResponse mockResponse;
@@ -48,9 +51,7 @@ class DefaultMvcResult implements MvcResult {
4851

4952
private Exception resolvedException;
5053

51-
private Object asyncResult;
52-
53-
private CountDownLatch asyncResultLatch;
54+
private final AtomicReference<Object> asyncResult = new AtomicReference<Object>(RESULT_NONE);
5455

5556

5657
/**
@@ -106,42 +107,39 @@ public FlashMap getFlashMap() {
106107
}
107108

108109
public void setAsyncResult(Object asyncResult) {
109-
this.asyncResult = asyncResult;
110+
this.asyncResult.set(asyncResult);
110111
}
111112

112113
public Object getAsyncResult() {
113114
return getAsyncResult(-1);
114115
}
115116

116-
public Object getAsyncResult(long timeout) {
117+
public Object getAsyncResult(long timeToWait) {
118+
117119
// MockHttpServletRequest type doesn't have async methods
118120
HttpServletRequest request = this.mockRequest;
119-
if ((timeout != 0) && request.isAsyncStarted()) {
120-
if (timeout == -1) {
121-
timeout = request.getAsyncContext().getTimeout();
122-
}
123-
if (!awaitAsyncResult(timeout)) {
124-
throw new IllegalStateException(
125-
"Gave up waiting on async result from handler [" + this.handler + "] to complete");
126-
}
121+
if (request.getAsyncContext() != null) {
122+
timeToWait = (timeToWait == -1 ? request.getAsyncContext().getTimeout() : timeToWait);
127123
}
128-
return this.asyncResult;
129-
}
130124

131-
private boolean awaitAsyncResult(long timeout) {
132-
if (this.asyncResultLatch != null) {
133-
try {
134-
return this.asyncResultLatch.await(timeout, TimeUnit.MILLISECONDS);
135-
}
136-
catch (InterruptedException e) {
137-
return false;
125+
if (timeToWait > 0) {
126+
long endTime = System.currentTimeMillis() + timeToWait;
127+
while (System.currentTimeMillis() < endTime && this.asyncResult.get() == RESULT_NONE) {
128+
try {
129+
Thread.sleep(200);
130+
}
131+
catch (InterruptedException ex) {
132+
throw new IllegalStateException("Interrupted while waiting for " +
133+
"async result to be set for handler [" + this.handler + "]", ex);
134+
}
138135
}
139136
}
140-
return true;
141-
}
142137

143-
public void setAsyncResultLatch(CountDownLatch asyncResultLatch) {
144-
this.asyncResultLatch = asyncResultLatch;
138+
Assert.state(this.asyncResult.get() != RESULT_NONE,
139+
"Async result for handler [" + this.handler + "] " +
140+
"was not set during the specified timeToWait=" + timeToWait);
141+
142+
return this.asyncResult.get();
145143
}
146144

147145
}

spring-test-mvc/src/main/java/org/springframework/test/web/servlet/MvcResult.java

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2012 the original author or authors.
2+
* Copyright 2002-2014 the original author or authors.
33
*
44
* Licensed under the Apache License; Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -76,24 +76,22 @@ public interface MvcResult {
7676
FlashMap getFlashMap();
7777

7878
/**
79-
* Get the result of asynchronous execution or {@code null} if concurrent
80-
* handling did not start. This method will hold and await the completion
81-
* of concurrent handling.
79+
* Get the result of async execution. This method will wait for the async result
80+
* to be set for up to the amount of time configured on the async request,
8281
*
83-
* @throws IllegalStateException if concurrent handling does not complete
84-
* within the allocated async timeout value.
82+
* @throws IllegalStateException if the async result was not set.
8583
*/
8684
Object getAsyncResult();
8785

8886
/**
89-
* Get the result of asynchronous execution or {@code null} if concurrent
90-
* handling did not start. This method will wait for up to the given timeout
91-
* for the completion of concurrent handling.
87+
* Get the result of async execution. This method will wait for the async result
88+
* to be set for up to the specified amount of time.
9289
*
93-
* @param timeout how long to wait for the async result to be set in
94-
* milliseconds; if -1, the wait will be as long as the async timeout set
95-
* on the Servlet request
90+
* @param timeToWait how long to wait for the async result to be set, in
91+
* milliseconds; if -1, then the async request timeout value is used,
92+
*
93+
* @throws IllegalStateException if the async result was not set.
9694
*/
97-
Object getAsyncResult(long timeout);
95+
Object getAsyncResult(long timeToWait);
9896

9997
}

spring-test-mvc/src/main/java/org/springframework/test/web/servlet/TestDispatcherServlet.java

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2012 the original author or authors.
2+
* Copyright 2002-2014 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -58,34 +58,25 @@ public TestDispatcherServlet(WebApplicationContext webApplicationContext) {
5858
}
5959

6060
@Override
61-
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
62-
63-
CountDownLatch latch = registerAsyncInterceptors(request);
64-
getMvcResult(request).setAsyncResultLatch(latch);
61+
protected void service(HttpServletRequest request, HttpServletResponse response)
62+
throws ServletException, IOException {
6563

64+
registerAsyncInterceptors(request);
6665
super.service(request, response);
6766
}
6867

69-
private CountDownLatch registerAsyncInterceptors(final HttpServletRequest servletRequest) {
70-
71-
final CountDownLatch asyncResultLatch = new CountDownLatch(1);
72-
68+
private void registerAsyncInterceptors(final HttpServletRequest servletRequest) {
7369
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(servletRequest);
74-
7570
asyncManager.registerCallableInterceptor(KEY, new CallableProcessingInterceptorAdapter() {
7671
public <T> void postProcess(NativeWebRequest request, Callable<T> task, Object value) throws Exception {
7772
getMvcResult(servletRequest).setAsyncResult(value);
78-
asyncResultLatch.countDown();
7973
}
8074
});
8175
asyncManager.registerDeferredResultInterceptor(KEY, new DeferredResultProcessingInterceptorAdapter() {
8276
public <T> void postProcess(NativeWebRequest request, DeferredResult<T> result, Object value) throws Exception {
8377
getMvcResult(servletRequest).setAsyncResult(value);
84-
asyncResultLatch.countDown();
8578
}
8679
});
87-
88-
return asyncResultLatch;
8980
}
9081

9182
protected DefaultMvcResult getMvcResult(ServletRequest request) {

spring-test-mvc/src/main/java/org/springframework/test/web/servlet/result/PrintingResultHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ protected void printAsyncResult(MvcResult result) throws Exception {
135135
if (servlet3Present) {
136136
HttpServletRequest request = result.getRequest();
137137
this.printer.printValue("Was async started", request.isAsyncStarted());
138-
this.printer.printValue("Async result", result.getAsyncResult(0));
138+
this.printer.printValue("Async result", (request.isAsyncStarted() ? result.getAsyncResult(0) : null));
139139
}
140140
}
141141

spring-test-mvc/src/test/java/org/springframework/test/web/servlet/DefaultMvcResultTests.java

Lines changed: 52 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2013 the original author or authors.
2+
* Copyright 2002-2014 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -15,16 +15,23 @@
1515
*/
1616
package org.springframework.test.web.servlet;
1717

18-
import java.util.concurrent.CountDownLatch;
19-
import java.util.concurrent.TimeUnit;
20-
21-
import javax.servlet.AsyncContext;
22-
2318
import org.junit.Before;
2419
import org.junit.Test;
2520
import org.springframework.mock.web.MockHttpServletRequest;
2621

27-
import static org.mockito.BDDMockito.*;
22+
import javax.servlet.AsyncContext;
23+
import javax.servlet.DispatcherType;
24+
import javax.servlet.ServletException;
25+
import javax.servlet.ServletRequest;
26+
import javax.servlet.ServletResponse;
27+
import javax.servlet.http.Part;
28+
29+
import java.io.IOException;
30+
import java.util.Collection;
31+
32+
import static org.junit.Assert.assertEquals;
33+
import static org.mockito.BDDMockito.given;
34+
import static org.mockito.Mockito.mock;
2835

2936
/**
3037
* Test fixture for {@link DefaultMvcResult}.
@@ -33,82 +40,73 @@
3340
*/
3441
public class DefaultMvcResultTests {
3542

36-
private static final long DEFAULT_TIMEOUT = 10000L;
37-
3843
private DefaultMvcResult mvcResult;
3944

40-
private CountDownLatch countDownLatch;
41-
42-
4345
@Before
4446
public void setup() {
4547
ExtendedMockHttpServletRequest request = new ExtendedMockHttpServletRequest();
46-
request.setAsyncStarted(true);
47-
48-
this.countDownLatch = mock(CountDownLatch.class);
49-
5048
this.mvcResult = new DefaultMvcResult(request, null);
51-
this.mvcResult.setAsyncResultLatch(this.countDownLatch);
52-
}
53-
54-
@Test
55-
public void getAsyncResultWithTimeout() throws Exception {
56-
long timeout = 1234L;
57-
given(this.countDownLatch.await(timeout, TimeUnit.MILLISECONDS)).willReturn(true);
58-
this.mvcResult.getAsyncResult(timeout);
59-
verify(this.countDownLatch).await(timeout, TimeUnit.MILLISECONDS);
60-
}
61-
62-
@Test
63-
public void getAsyncResultWithTimeoutNegativeOne() throws Exception {
64-
given(this.countDownLatch.await(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS)).willReturn(true);
65-
this.mvcResult.getAsyncResult(-1);
66-
verify(this.countDownLatch).await(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS);
6749
}
6850

6951
@Test
70-
public void getAsyncResultWithoutTimeout() throws Exception {
71-
given(this.countDownLatch.await(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS)).willReturn(true);
72-
this.mvcResult.getAsyncResult();
73-
verify(this.countDownLatch).await(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS);
52+
public void getAsyncResultSuccess() throws Exception {
53+
this.mvcResult.setAsyncResult("Foo");
54+
assertEquals("Foo", this.mvcResult.getAsyncResult());
7455
}
7556

76-
@Test
77-
public void getAsyncResultWithTimeoutZero() throws Exception {
57+
@Test(expected = IllegalStateException.class)
58+
public void getAsyncResultFailure() throws Exception {
7859
this.mvcResult.getAsyncResult(0);
79-
verifyZeroInteractions(this.countDownLatch);
80-
}
81-
82-
@Test(expected=IllegalStateException.class)
83-
public void getAsyncResultAndTimeOut() throws Exception {
84-
this.mvcResult.getAsyncResult(-1);
85-
verify(this.countDownLatch).await(DEFAULT_TIMEOUT, TimeUnit.MILLISECONDS);
8660
}
8761

8862

8963
private static class ExtendedMockHttpServletRequest extends MockHttpServletRequest {
9064

91-
private boolean asyncStarted;
9265
private AsyncContext asyncContext;
9366

9467
public ExtendedMockHttpServletRequest() {
95-
super();
9668
this.asyncContext = mock(AsyncContext.class);
97-
given(this.asyncContext.getTimeout()).willReturn(new Long(DEFAULT_TIMEOUT));
98-
}
99-
100-
public void setAsyncStarted(boolean asyncStarted) {
101-
this.asyncStarted = asyncStarted;
69+
given(this.asyncContext.getTimeout()).willReturn(0L);
10270
}
10371

10472
@Override
10573
public boolean isAsyncStarted() {
106-
return this.asyncStarted;
74+
return true;
10775
}
10876

10977
@Override
11078
public AsyncContext getAsyncContext() {
111-
return asyncContext;
79+
return this.asyncContext;
80+
}
81+
82+
@Override
83+
public Collection<Part> getParts() throws IOException, ServletException {
84+
return null;
85+
}
86+
87+
@Override
88+
public Part getPart(String name) throws IOException, ServletException {
89+
return null;
90+
}
91+
92+
@Override
93+
public AsyncContext startAsync() throws IllegalStateException {
94+
return this.asyncContext;
95+
}
96+
97+
@Override
98+
public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) {
99+
return this.asyncContext;
100+
}
101+
102+
@Override
103+
public boolean isAsyncSupported() {
104+
return true;
105+
}
106+
107+
@Override
108+
public DispatcherType getDispatcherType() {
109+
return null;
112110
}
113111
}
114112

0 commit comments

Comments
 (0)