Skip to content

Commit f9e6ea5

Browse files
committed
MvcResult returns asyncResult after asyncDispatch
Issue: SPR-16648
1 parent e6020ed commit f9e6ea5

File tree

5 files changed

+93
-49
lines changed

5 files changed

+93
-49
lines changed

spring-test/src/main/java/org/springframework/mock/web/MockAsyncContext.java

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2017 the original author or authors.
2+
* Copyright 2002-2018 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.
@@ -65,7 +65,14 @@ public MockAsyncContext(ServletRequest request, @Nullable ServletResponse respon
6565

6666
public void addDispatchHandler(Runnable handler) {
6767
Assert.notNull(handler, "Dispatch handler must not be null");
68-
this.dispatchHandlers.add(handler);
68+
synchronized (this) {
69+
if (this.dispatchedPath == null) {
70+
this.dispatchHandlers.add(handler);
71+
}
72+
else {
73+
handler.run();
74+
}
75+
}
6976
}
7077

7178
@Override
@@ -96,9 +103,9 @@ public void dispatch(String path) {
96103

97104
@Override
98105
public void dispatch(@Nullable ServletContext context, String path) {
99-
this.dispatchedPath = path;
100-
for (Runnable r : this.dispatchHandlers) {
101-
r.run();
106+
synchronized (this) {
107+
this.dispatchedPath = path;
108+
this.dispatchHandlers.forEach(Runnable::run);
102109
}
103110
}
104111

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

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,14 @@
1616

1717
package org.springframework.test.web.servlet;
1818

19+
import java.util.concurrent.CountDownLatch;
20+
import java.util.concurrent.TimeUnit;
1921
import java.util.concurrent.atomic.AtomicReference;
2022

2123
import org.springframework.lang.Nullable;
2224
import org.springframework.mock.web.MockHttpServletRequest;
2325
import org.springframework.mock.web.MockHttpServletResponse;
26+
import org.springframework.util.Assert;
2427
import org.springframework.web.servlet.FlashMap;
2528
import org.springframework.web.servlet.HandlerInterceptor;
2629
import org.springframework.web.servlet.ModelAndView;
@@ -56,6 +59,9 @@ class DefaultMvcResult implements MvcResult {
5659

5760
private final AtomicReference<Object> asyncResult = new AtomicReference<>(RESULT_NONE);
5861

62+
@Nullable
63+
private CountDownLatch asyncDispatchLatch;
64+
5965

6066
/**
6167
* Create a new instance with the given request and response.
@@ -135,27 +141,31 @@ public Object getAsyncResult(long timeToWait) {
135141
if (this.mockRequest.getAsyncContext() != null) {
136142
timeToWait = (timeToWait == -1 ? this.mockRequest.getAsyncContext().getTimeout() : timeToWait);
137143
}
138-
139-
if (timeToWait > 0) {
140-
long endTime = System.currentTimeMillis() + timeToWait;
141-
while (System.currentTimeMillis() < endTime && this.asyncResult.get() == RESULT_NONE) {
142-
try {
143-
Thread.sleep(100);
144-
}
145-
catch (InterruptedException ex) {
146-
Thread.currentThread().interrupt();
147-
throw new IllegalStateException("Interrupted while waiting for " +
148-
"async result to be set for handler [" + this.handler + "]", ex);
149-
}
150-
}
144+
if (!awaitAsyncDispatch(timeToWait)) {
145+
throw new IllegalStateException("Async result for handler [" + this.handler + "]" +
146+
" was not set during the specified timeToWait=" + timeToWait);
151147
}
152-
153148
Object result = this.asyncResult.get();
154-
if (result == RESULT_NONE) {
155-
throw new IllegalStateException("Async result for handler [" + this.handler + "] " +
156-
"was not set during the specified timeToWait=" + timeToWait);
149+
Assert.state(result != RESULT_NONE, "Async result for handler [" + this.handler + "] was not set");
150+
return this.asyncResult.get();
151+
}
152+
153+
/**
154+
* True if is there a latch was not set, or the latch count reached 0.
155+
*/
156+
private boolean awaitAsyncDispatch(long timeout) {
157+
Assert.state(this.asyncDispatchLatch != null,
158+
"The asynDispatch CountDownLatch was not set by the TestDispatcherServlet.\n");
159+
try {
160+
return this.asyncDispatchLatch.await(timeout, TimeUnit.MILLISECONDS);
161+
}
162+
catch (InterruptedException e) {
163+
return false;
157164
}
158-
return result;
165+
}
166+
167+
void setAsyncDispatchLatch(CountDownLatch asyncDispatchLatch) {
168+
this.asyncDispatchLatch = asyncDispatchLatch;
159169
}
160170

161171
}

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

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2014 the original author or authors.
2+
* Copyright 2002-2018 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,18 +18,19 @@
1818

1919
import java.io.IOException;
2020
import java.util.concurrent.Callable;
21+
import java.util.concurrent.CountDownLatch;
2122
import javax.servlet.ServletException;
2223
import javax.servlet.ServletRequest;
2324
import javax.servlet.http.HttpServletRequest;
2425
import javax.servlet.http.HttpServletResponse;
2526

2627
import org.springframework.lang.Nullable;
28+
import org.springframework.mock.web.MockAsyncContext;
2729
import org.springframework.web.context.WebApplicationContext;
2830
import org.springframework.web.context.request.NativeWebRequest;
2931
import org.springframework.web.context.request.async.CallableProcessingInterceptor;
3032
import org.springframework.web.context.request.async.DeferredResult;
3133
import org.springframework.web.context.request.async.DeferredResultProcessingInterceptor;
32-
import org.springframework.web.context.request.async.WebAsyncManager;
3334
import org.springframework.web.context.request.async.WebAsyncUtils;
3435
import org.springframework.web.servlet.DispatcherServlet;
3536
import org.springframework.web.servlet.HandlerExecutionChain;
@@ -63,23 +64,34 @@ protected void service(HttpServletRequest request, HttpServletResponse response)
6364
throws ServletException, IOException {
6465

6566
registerAsyncResultInterceptors(request);
67+
6668
super.service(request, response);
69+
70+
if (request.getAsyncContext() != null) {
71+
CountDownLatch dispatchLatch = new CountDownLatch(1);
72+
((MockAsyncContext) request.getAsyncContext()).addDispatchHandler(dispatchLatch::countDown);
73+
getMvcResult(request).setAsyncDispatchLatch(dispatchLatch);
74+
}
6775
}
6876

6977
private void registerAsyncResultInterceptors(final HttpServletRequest request) {
70-
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
71-
asyncManager.registerCallableInterceptor(KEY, new CallableProcessingInterceptor() {
72-
@Override
73-
public <T> void postProcess(NativeWebRequest r, Callable<T> task, Object value) throws Exception {
74-
getMvcResult(request).setAsyncResult(value);
75-
}
76-
});
77-
asyncManager.registerDeferredResultInterceptor(KEY, new DeferredResultProcessingInterceptor() {
78-
@Override
79-
public <T> void postProcess(NativeWebRequest r, DeferredResult<T> result, Object value) throws Exception {
80-
getMvcResult(request).setAsyncResult(value);
81-
}
82-
});
78+
79+
WebAsyncUtils.getAsyncManager(request).registerCallableInterceptor(KEY,
80+
new CallableProcessingInterceptor() {
81+
@Override
82+
public <T> void postProcess(NativeWebRequest r, Callable<T> task, Object value) {
83+
// We got the result, must also wait for the dispatch
84+
getMvcResult(request).setAsyncResult(value);
85+
}
86+
});
87+
88+
WebAsyncUtils.getAsyncManager(request).registerDeferredResultInterceptor(KEY,
89+
new DeferredResultProcessingInterceptor() {
90+
@Override
91+
public <T> void postProcess(NativeWebRequest r, DeferredResult<T> result, Object value) {
92+
getMvcResult(request).setAsyncResult(value);
93+
}
94+
});
8395
}
8496

8597
protected DefaultMvcResult getMvcResult(ServletRequest request) {

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2014 the original author or authors.
2+
* Copyright 2002-2018 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,6 +15,8 @@
1515
*/
1616
package org.springframework.test.web.servlet;
1717

18+
import java.util.concurrent.CountDownLatch;
19+
1820
import org.junit.Before;
1921
import org.junit.Test;
2022

@@ -38,13 +40,14 @@ public void setup() {
3840
}
3941

4042
@Test
41-
public void getAsyncResultSuccess() throws Exception {
43+
public void getAsyncResultSuccess() {
4244
this.mvcResult.setAsyncResult("Foo");
43-
assertEquals("Foo", this.mvcResult.getAsyncResult());
45+
this.mvcResult.setAsyncDispatchLatch(new CountDownLatch(0));
46+
this.mvcResult.getAsyncResult();
4447
}
4548

4649
@Test(expected = IllegalStateException.class)
47-
public void getAsyncResultFailure() throws Exception {
50+
public void getAsyncResultFailure() {
4851
this.mvcResult.getAsyncResult(0);
4952
}
5053

spring-web/src/test/java/org/springframework/mock/web/test/MockAsyncContext.java

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2017 the original author or authors.
2+
* Copyright 2002-2018 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.
@@ -30,6 +30,7 @@
3030
import javax.servlet.http.HttpServletResponse;
3131

3232
import org.springframework.beans.BeanUtils;
33+
import org.springframework.lang.Nullable;
3334
import org.springframework.util.Assert;
3435
import org.springframework.web.util.WebUtils;
3536

@@ -43,26 +44,35 @@ public class MockAsyncContext implements AsyncContext {
4344

4445
private final HttpServletRequest request;
4546

47+
@Nullable
4648
private final HttpServletResponse response;
4749

4850
private final List<AsyncListener> listeners = new ArrayList<>();
4951

52+
@Nullable
5053
private String dispatchedPath;
5154

5255
private long timeout = 10 * 1000L; // 10 seconds is Tomcat's default
5356

5457
private final List<Runnable> dispatchHandlers = new ArrayList<>();
5558

5659

57-
public MockAsyncContext(ServletRequest request, ServletResponse response) {
60+
public MockAsyncContext(ServletRequest request, @Nullable ServletResponse response) {
5861
this.request = (HttpServletRequest) request;
5962
this.response = (HttpServletResponse) response;
6063
}
6164

6265

6366
public void addDispatchHandler(Runnable handler) {
6467
Assert.notNull(handler, "Dispatch handler must not be null");
65-
this.dispatchHandlers.add(handler);
68+
synchronized (this) {
69+
if (this.dispatchedPath == null) {
70+
this.dispatchHandlers.add(handler);
71+
}
72+
else {
73+
handler.run();
74+
}
75+
}
6676
}
6777

6878
@Override
@@ -71,6 +81,7 @@ public ServletRequest getRequest() {
7181
}
7282

7383
@Override
84+
@Nullable
7485
public ServletResponse getResponse() {
7586
return this.response;
7687
}
@@ -91,13 +102,14 @@ public void dispatch(String path) {
91102
}
92103

93104
@Override
94-
public void dispatch(ServletContext context, String path) {
95-
this.dispatchedPath = path;
96-
for (Runnable r : this.dispatchHandlers) {
97-
r.run();
105+
public void dispatch(@Nullable ServletContext context, String path) {
106+
synchronized (this) {
107+
this.dispatchedPath = path;
108+
this.dispatchHandlers.forEach(Runnable::run);
98109
}
99110
}
100111

112+
@Nullable
101113
public String getDispatchedPath() {
102114
return this.dispatchedPath;
103115
}

0 commit comments

Comments
 (0)