理解Callable 和 Spring DeferredResult(翻譯)


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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM