我正在开发一个 Spring Boot 应用程序,我想在其中使用 CompletableFuture 和 @Async 注释使我的控制器异步。为了测试实现,我特意在服务方法中添加了一个 Thread.sleep() 调用。然而,即使使用这些异步方法,控制器在执行之前仍然需要等待一段时间。我已经验证我的控制器和服务在不同的线程上运行,正如它们不同的线程 ID 所证明的那样。我不确定为什么会这样,如果有任何关于如何解决此问题的见解或建议,我将不胜感激。
代码如下:
//controller
@PostMapping
public CompletableFuture<User> createUser(@RequestBody User user) {
System.out.println(Thread.currentThread().getId()+"-----------------------------------"+System.currentTimeMillis()+"----------"+"inside controller");
return userService.createUser(user);
}
// 服务
@Override
@Async("asyncExecutor")
public CompletableFuture<User> createUser(User user) {
System.out.println(Thread.currentThread().getId()+"-----------------------------------"+System.currentTimeMillis()+"-------------inside services");
return CompletableFuture.supplyAsync(() ->{
try {
Thread.sleep(1000000000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getId());
return userRepository.save(user);
});
}
提供一些代码示例或有关您的实现的具体细节,以便更清楚地了解问题。
//输出
21-----------------------------------1683404458540----------inside controller
34-----------------------------------1683404458543-------------inside services
2023-05-07 01:51:28.812 WARN 36119 --- [nio-8080-exec-2] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.context.request.async.AsyncRequestTimeoutException]
Thread.sleep(1000000000);
对于(构建)测试来说似乎太多了......这指的是〜12(太阳)天!
这有效:
“应用程序”:
package com.example.demo;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.stereotype.Controller;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import lombok.extern.slf4j.Slf4j;
@EnableAsync
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Slf4j
@Service
static class MyAsyService {
@Value("${simulation.sleep.seconds:10}")
private Long simSeconds;
@Async
CompletableFuture<String> doSomething(String req) throws InterruptedException {
log.info("in service...");
TimeUnit.SECONDS.sleep(simSeconds);
log.info("...leaving service");
return CompletableFuture.completedFuture(req + "_" + UUID.randomUUID());
}
}
@Slf4j
@Controller
static class MyController {
@Autowired
MyAsyService service;
@GetMapping("/hello")
@ResponseBody
CompletableFuture<String> hello(@RequestParam(required = false, defaultValue = "anonymous") String req)
throws InterruptedException {
log.info("in controller...");
CompletableFuture<String> result = service.doSomething(req);
log.info("...controller done!");
return result;
}
}
}
在服务器日志中:
... o.s.web.servlet.DispatcherServlet : Completed initialization in 1 ms
... DemoApplication$MyController : in controller.... <= req#1
... DemoApplication$MyController : ....controller done!
... DemoApplication$MyAsyService : in service....
... DemoApplication$MyAsyService : ....leaving service
... DemoApplication$MyController : in controller.... <= req#2
... DemoApplication$MyController : ....controller done!
... DemoApplication$MyAsyService : in service....
... DemoApplication$MyAsyService : ....leaving service
... DemoApplication$MyController : in controller.... <= req#3
... DemoApplication$MyController : ....controller done!
... DemoApplication$MyAsyService : in service....
... DemoApplication$MyAsyService : ....leaving service
(自动,模拟 mvc)测试(最难的部分;):
package com.example.demo;
import static org.hamcrest.core.StringStartsWith.startsWith;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import java.util.UUID;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.RequestBuilder;
@WebMvcTest
class DemoApplicationTests {
@Autowired
MockMvc mockMvc;
@Value("${simulation.sleep.seconds}") // -> src/test/resources/application.properties
Long simSeconds;
@Test
void testController() throws Exception {
String uid = UUID.randomUUID().toString();
MvcResult mvcResult = mockMvc.perform(get("/hello?req={v}", uid)) //1.
.andExpect(request().asyncStarted()) // 2.
.andReturn(); // 3.
mockMvc.perform(asyncDispatch(mvcResult/*, (simSeconds + 1) * 1000*/)) // 4!*
.andExpectAll( // ...
status().isOk(),
content().string(startsWith(uid)));
}
// 4*: when we take/simulate more than ~10 seconds, we have to & can re-implement/enrich:
static RequestBuilder asyncDispatch(MvcResult mvcResult, long timeToWait) {
// copied/adopted from MockMvcRequestBuilders#asyncDispatch
mvcResult.getAsyncResult(timeToWait);
return servletContext -> {
MockHttpServletRequest request = mvcResult.getRequest();
request.setDispatcherType(DispatcherType.ASYNC);
request.setAsyncStarted(false);
return request;
};
}
}