Skip to content

Commit 74de35d

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 e50cff4 commit 74de35d

File tree

7 files changed

+53
-145
lines changed

7 files changed

+53
-145
lines changed

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

Lines changed: 22 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,11 @@
1616
package org.springframework.test.web.servlet;
1717

1818
import java.util.concurrent.CountDownLatch;
19-
import java.util.concurrent.TimeUnit;
20-
21-
import javax.servlet.http.HttpServletRequest;
19+
import java.util.concurrent.atomic.AtomicReference;
2220

2321
import org.springframework.mock.web.MockHttpServletRequest;
2422
import org.springframework.mock.web.MockHttpServletResponse;
23+
import org.springframework.util.Assert;
2524
import org.springframework.web.servlet.FlashMap;
2625
import org.springframework.web.servlet.HandlerInterceptor;
2726
import org.springframework.web.servlet.ModelAndView;
@@ -51,7 +50,7 @@ class DefaultMvcResult implements MvcResult {
5150

5251
private Exception resolvedException;
5352

54-
private Object asyncResult = RESULT_NONE;
53+
private final AtomicReference<Object> asyncResult = new AtomicReference<Object>(RESULT_NONE);
5554

5655
private CountDownLatch asyncResultLatch;
5756

@@ -116,7 +115,7 @@ public FlashMap getFlashMap() {
116115
}
117116

118117
public void setAsyncResult(Object asyncResult) {
119-
this.asyncResult = asyncResult;
118+
this.asyncResult.set(asyncResult);
120119
}
121120

122121
@Override
@@ -125,35 +124,30 @@ public Object getAsyncResult() {
125124
}
126125

127126
@Override
128-
public Object getAsyncResult(long timeout) {
129-
if (this.asyncResult == RESULT_NONE) {
130-
if ((timeout != 0) && this.mockRequest.isAsyncStarted()) {
131-
if (timeout == -1) {
132-
timeout = this.mockRequest.getAsyncContext().getTimeout();
127+
public Object getAsyncResult(long timeToWait) {
128+
129+
if (this.mockRequest.getAsyncContext() != null) {
130+
timeToWait = (timeToWait == -1 ? this.mockRequest.getAsyncContext().getTimeout() : timeToWait);
131+
}
132+
133+
if (timeToWait > 0) {
134+
long endTime = System.currentTimeMillis() + timeToWait;
135+
while (System.currentTimeMillis() < endTime && this.asyncResult.get() == RESULT_NONE) {
136+
try {
137+
Thread.sleep(200);
133138
}
134-
if (!awaitAsyncResult(timeout) && this.asyncResult == RESULT_NONE) {
135-
throw new IllegalStateException(
136-
"Gave up waiting on async result from handler [" + this.handler + "] to complete");
139+
catch (InterruptedException ex) {
140+
throw new IllegalStateException("Interrupted while waiting for " +
141+
"async result to be set for handler [" + this.handler + "]", ex);
137142
}
138143
}
139144
}
140-
return (this.asyncResult == RESULT_NONE ? null : this.asyncResult);
141-
}
142145

143-
private boolean awaitAsyncResult(long timeout) {
144-
if (this.asyncResultLatch != null) {
145-
try {
146-
return this.asyncResultLatch.await(timeout, TimeUnit.MILLISECONDS);
147-
}
148-
catch (InterruptedException e) {
149-
return false;
150-
}
151-
}
152-
return true;
153-
}
146+
Assert.state(this.asyncResult.get() != RESULT_NONE,
147+
"Async result for handler [" + this.handler + "] " +
148+
"was not set during the specified timeToWait=" + timeToWait);
154149

155-
public void setAsyncResultLatch(CountDownLatch asyncResultLatch) {
156-
this.asyncResultLatch = asyncResultLatch;
150+
return this.asyncResult.get();
157151
}
158152

159153
}

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

Lines changed: 13 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,24 @@ 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,
81+
* i.e. {@link org.springframework.mock.web.MockAsyncContext#getTimeout()}.
8282
*
83-
* @throws IllegalStateException if concurrent handling does not complete
84-
* within the allocated async timeout value.
83+
* @throws IllegalStateException if the async result was not set.
8584
*/
8685
Object getAsyncResult();
8786

8887
/**
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.
88+
* Get the result of async execution. This method will wait for the async result
89+
* to be set for up to the specified amount of time.
9290
*
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
91+
* @param timeToWait how long to wait for the async result to be set, in
92+
* milliseconds; if -1, then the async request timeout value is used,
93+
* i.e.{@link org.springframework.mock.web.MockAsyncContext#getTimeout()}.
94+
*
95+
* @throws IllegalStateException if the async result was not set.
9696
*/
97-
Object getAsyncResult(long timeout);
97+
Object getAsyncResult(long timeToWait);
9898

9999
}

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

Lines changed: 3 additions & 20 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.
@@ -18,14 +18,12 @@
1818

1919
import java.io.IOException;
2020
import java.util.concurrent.Callable;
21-
import java.util.concurrent.CountDownLatch;
2221

2322
import javax.servlet.ServletException;
2423
import javax.servlet.ServletRequest;
2524
import javax.servlet.http.HttpServletRequest;
2625
import javax.servlet.http.HttpServletResponse;
2726

28-
import org.springframework.mock.web.MockAsyncContext;
2927
import org.springframework.web.context.WebApplicationContext;
3028
import org.springframework.web.context.request.NativeWebRequest;
3129
import org.springframework.web.context.request.async.*;
@@ -57,15 +55,11 @@ public TestDispatcherServlet(WebApplicationContext webApplicationContext) {
5755

5856

5957
@Override
60-
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
58+
protected void service(HttpServletRequest request, HttpServletResponse response)
59+
throws ServletException, IOException {
6160

6261
registerAsyncResultInterceptors(request);
63-
6462
super.service(request, response);
65-
66-
if (request.isAsyncStarted()) {
67-
addAsyncResultLatch(request);
68-
}
6963
}
7064

7165
private void registerAsyncResultInterceptors(final HttpServletRequest request) {
@@ -84,17 +78,6 @@ public <T> void postProcess(NativeWebRequest r, DeferredResult<T> result, Object
8478
});
8579
}
8680

87-
private void addAsyncResultLatch(HttpServletRequest request) {
88-
final CountDownLatch latch = new CountDownLatch(1);
89-
((MockAsyncContext) request.getAsyncContext()).addDispatchHandler(new Runnable() {
90-
@Override
91-
public void run() {
92-
latch.countDown();
93-
}
94-
});
95-
getMvcResult(request).setAsyncResultLatch(latch);
96-
}
97-
9881
protected DefaultMvcResult getMvcResult(ServletRequest request) {
9982
return (DefaultMvcResult) request.getAttribute(MockMvc.MVC_RESULT_ATTRIBUTE);
10083
}

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

Lines changed: 6 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.
@@ -19,15 +19,13 @@
1919
import java.util.Enumeration;
2020
import java.util.Map;
2121

22-
import javax.servlet.ServletRequest;
2322
import javax.servlet.http.HttpServletRequest;
2423

2524
import org.springframework.http.HttpHeaders;
2625
import org.springframework.mock.web.MockHttpServletRequest;
2726
import org.springframework.mock.web.MockHttpServletResponse;
2827
import org.springframework.test.web.servlet.MvcResult;
2928
import org.springframework.test.web.servlet.ResultHandler;
30-
import org.springframework.util.ClassUtils;
3129
import org.springframework.util.LinkedMultiValueMap;
3230
import org.springframework.util.MultiValueMap;
3331
import org.springframework.validation.BindingResult;
@@ -48,8 +46,6 @@
4846
*/
4947
public class PrintingResultHandler implements ResultHandler {
5048

51-
private static final boolean servlet3Present = ClassUtils.hasMethod(ServletRequest.class, "startAsync");
52-
5349
private final ResultValuePrinter printer;
5450

5551

@@ -80,10 +76,8 @@ public final void handle(MvcResult result) throws Exception {
8076
this.printer.printHeading("Handler");
8177
printHandler(result.getHandler(), result.getInterceptors());
8278

83-
if (servlet3Present) {
84-
this.printer.printHeading("Async");
85-
printAsyncResult(result);
86-
}
79+
this.printer.printHeading("Async");
80+
printAsyncResult(result);
8781

8882
this.printer.printHeading("Resolved Exception");
8983
printResolvedException(result.getResolvedException());
@@ -133,11 +127,9 @@ protected final MultiValueMap<String, String> getParamsMultiValueMap(MockHttpSer
133127
}
134128

135129
protected void printAsyncResult(MvcResult result) throws Exception {
136-
if (servlet3Present) {
137-
HttpServletRequest request = result.getRequest();
138-
this.printer.printValue("Was async started", request.isAsyncStarted());
139-
this.printer.printValue("Async result", result.getAsyncResult(0));
140-
}
130+
HttpServletRequest request = result.getRequest();
131+
this.printer.printValue("Was async started", request.isAsyncStarted());
132+
this.printer.printValue("Async result", (request.isAsyncStarted() ? result.getAsyncResult(0) : null));
141133
}
142134

143135
/** Print the handler */
Lines changed: 8 additions & 64 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.
@@ -24,6 +24,7 @@
2424
import org.junit.Test;
2525
import org.springframework.mock.web.MockHttpServletRequest;
2626

27+
import static org.junit.Assert.assertEquals;
2728
import static org.mockito.BDDMockito.*;
2829

2930
/**
@@ -37,80 +38,23 @@ public class DefaultMvcResultTests {
3738

3839
private DefaultMvcResult mvcResult;
3940

40-
private CountDownLatch countDownLatch;
41-
4241

4342
@Before
4443
public void setup() {
45-
ExtendedMockHttpServletRequest request = new ExtendedMockHttpServletRequest();
44+
MockHttpServletRequest request = new MockHttpServletRequest();
4645
request.setAsyncStarted(true);
47-
48-
this.countDownLatch = mock(CountDownLatch.class);
49-
5046
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);
6047
}
6148

6249
@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);
50+
public void getAsyncResultSuccess() throws Exception {
51+
this.mvcResult.setAsyncResult("Foo");
52+
assertEquals("Foo", this.mvcResult.getAsyncResult(10 * 1000));
6753
}
6854

69-
@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);
74-
}
75-
76-
@Test
77-
public void getAsyncResultWithTimeoutZero() throws Exception {
55+
@Test(expected = IllegalStateException.class)
56+
public void getAsyncResultFailure() throws Exception {
7857
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);
86-
}
87-
88-
89-
private static class ExtendedMockHttpServletRequest extends MockHttpServletRequest {
90-
91-
private boolean asyncStarted;
92-
private AsyncContext asyncContext;
93-
94-
public ExtendedMockHttpServletRequest() {
95-
super();
96-
this.asyncContext = mock(AsyncContext.class);
97-
given(this.asyncContext.getTimeout()).willReturn(new Long(DEFAULT_TIMEOUT));
98-
}
99-
100-
@Override
101-
public void setAsyncStarted(boolean asyncStarted) {
102-
this.asyncStarted = asyncStarted;
103-
}
104-
105-
@Override
106-
public boolean isAsyncStarted() {
107-
return this.asyncStarted;
108-
}
109-
110-
@Override
111-
public AsyncContext getAsyncContext() {
112-
return asyncContext;
113-
}
11458
}
11559

11660
}

spring-test/src/test/java/org/springframework/test/web/servlet/StubMvcResult.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ public Object getAsyncResult() {
133133
}
134134

135135
@Override
136-
public Object getAsyncResult(long timeout) {
136+
public Object getAsyncResult(long timeToWait) {
137137
return null;
138138
}
139139

spring-test/src/test/java/org/springframework/test/web/servlet/samples/standalone/AsyncTests.java

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,23 +59,19 @@ public void setup() {
5959
}
6060

6161
@Test
62-
@Ignore
6362
public void testCallable() throws Exception {
6463
MvcResult mvcResult = this.mockMvc.perform(get("/1").param("callable", "true"))
65-
.andDo(print())
6664
.andExpect(request().asyncStarted())
6765
.andExpect(request().asyncResult(new Person("Joe")))
6866
.andReturn();
6967

7068
this.mockMvc.perform(asyncDispatch(mvcResult))
71-
.andDo(print())
7269
.andExpect(status().isOk())
7370
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
7471
.andExpect(content().string("{\"name\":\"Joe\",\"someDouble\":0.0,\"someBoolean\":false}"));
7572
}
7673

7774
@Test
78-
@Ignore
7975
public void testDeferredResult() throws Exception {
8076
MvcResult mvcResult = this.mockMvc.perform(get("/1").param("deferredResult", "true"))
8177
.andExpect(request().asyncStarted())
@@ -90,7 +86,6 @@ public void testDeferredResult() throws Exception {
9086
}
9187

9288
@Test
93-
@Ignore
9489
public void testDeferredResultWithSetValue() throws Exception {
9590
MvcResult mvcResult = this.mockMvc.perform(get("/1").param("deferredResultWithSetValue", "true"))
9691
.andExpect(request().asyncStarted())

0 commit comments

Comments
 (0)