1-介紹
Servlet 3中的異步支援為在另一個線程中處理HTTP請求提供了可能性。當有一個長時間運作的任務時,這是特别有趣的,因為當另一個線程處理這個請求時,容器線程被釋放,并且可以繼續為其他請求服務。
這個主題已經解釋了很多次,Spring架構提供的關于這個功能的類似乎有一點混亂——在一個Controller中傳回Callable 和 DeferredResult。
在這篇文章中,我将實施這兩個例子,以顯示其差異。
這裡所顯示的所有示例都包括執行一個控制器,該控制器将執行一個長期運作的任務,然後将結果傳回給客戶機。長時間運作的任務由taskservice處理:
@Service
public class TaskServiceImpl implements TaskService {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
public String execute() {
try {
Thread.sleep(5000);
logger.info("Slow task executed");
return "Task finished";
} catch (InterruptedException e) {
throw new RuntimeException();
}
}
}
這個web應用是用Spring Boot建立的,我們将執行下面的類來運作我們的例子:
@SpringBootApplication
public class MainApp {
public static void main(String[] args) {
SpringApplication.run(MainApp.class, args);
}
}
2-阻塞的Controller
在這個例子中,一個請求到達控制器。servlet線程不會被釋放,直到長時間運作的方法被執行,我們退出
@requestmapping
注釋的方法。
@RestController
public class BlockingController {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final TaskService taskService;
@Autowired
public BlockingController(TaskService taskService) {
this.taskService = taskService;
}
@RequestMapping(value = "/block", method = RequestMethod.GET, produces = "text/html")
public String executeSlowTask() {
logger.info("Request received");
String result = taskService.execute();
logger.info("Servlet thread released");
return result;
}
}
如果我們運作這個例子http://localhost:8080/block,在日志裡我們會發現servlet request不會被釋放,直到長時間的任務執行完(5秒後)。
2015-07-12 12:41:11.849 [nio-8080-exec-6] x.s.web.controller.BlockingController : Request received
2015-07-12 12:41:16.851 [nio-8080-exec-6] x.spring.web.service.TaskServiceImpl : Slow task executed
2015-07-12 12:41:16.851 [nio-8080-exec-6] x.s.web.controller.BlockingController : Servlet thread released
3-傳回Callable
在這個例子中,不是直接傳回的結果,我們将傳回一個Callable:
@RestController
public class AsyncCallableController {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final TaskService taskService;
@Autowired
public AsyncCallableController(TaskService taskService) {
this.taskService = taskService;
}
@RequestMapping(value = "/callable", method = RequestMethod.GET, produces = "text/html")
public Callable<String> executeSlowTask() {
logger.info("Request received");
Callable<String> callable = taskService::execute;
logger.info("Servlet thread released");
return callable;
}
}
傳回Callable意味着Spring MVC将調用在不同的線程中執行定義的任務。Spring将使用TaskExecutor來管理線程。在等待完成的長期任務之前,servlet線程将被釋放。
2015-07-12 13:07:07.012 [nio-8080-exec-5] x.s.w.c.AsyncCallableController : Request received
2015-07-12 13:07:07.013 [nio-8080-exec-5] x.s.w.c.AsyncCallableController : Servlet thread released
2015-07-12 13:07:12.014 [ MvcAsync2] x.spring.web.service.TaskServiceImpl : Slow task executed
你可以看到我們在長時間運作的任務執行完畢之前就已經從servlet傳回了。這并不意味着用戶端收到了一個響應。與用戶端的通信仍然是開放的等待結果,但接收到的請求的線程已被釋放,并可以服務于另一個客戶的請求。
4-傳回DeferredResult
首先,我們需要建立一個deferredresult對象。此對象将由控制器傳回。我們将完成和Callable相同的事,當我們在另一個線程處理長時間運作的任務的時候釋放servlet線程。
@RestController
public class AsyncDeferredController {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final TaskService taskService;
@Autowired
public AsyncDeferredController(TaskService taskService) {
this.taskService = taskService;
}
@RequestMapping(value = "/deferred", method = RequestMethod.GET, produces = "text/html")
public DeferredResult<String> executeSlowTask() {
logger.info("Request received");
DeferredResult<String> deferredResult = new DeferredResult<>();
CompletableFuture.supplyAsync(taskService::execute)
.whenCompleteAsync((result, throwable) -> deferredResult.setResult(result));
logger.info("Servlet thread released");
return deferredResult;
}
}
是以,傳回DeferredResult和傳回Callable有什麼差別?不同的是這一次線程是由我們管理。建立一個線程并将結果set到DeferredResult是由我們自己來做的。
用completablefuture建立一個異步任務。這将建立一個新的線程,在那裡我們的長時間運作的任務将被執行。也就是在這個線程中,我們将set結果到DeferredResult并傳回。
是在哪個線程池中我們取回這個新的線程?預設情況下,在completablefuture的supplyasync方法将在forkjoin池運作任務。如果你想使用一個不同的線程池,你可以通過傳一個executor到supplyasync方法:
public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)
如果我們運作這個例子,我們将得到如下結果:
2015-07-12 13:28:08.433 [io-8080-exec-10] x.s.w.c.AsyncDeferredController : Request received
2015-07-12 13:28:08.475 [io-8080-exec-10] x.s.w.c.AsyncDeferredController : Servlet thread released
2015-07-12 13:28:13.469 [onPool-worker-1] x.spring.web.service.TaskServiceImpl : Slow task executed
5-結論
站在一定高度來看這問題,Callable和Deferredresult做的是同樣的事情——釋放容器線程,在另一個線程上異步運作長時間的任務。不同的是誰管理執行任務的線程。
文中涉及的代碼spring-rest
翻譯自Xavier Padró's Blog