background

In actual services, the back-end interface needs to process for a certain amount of time, but the back-end interface cannot hang on to the user interface. Therefore, the back-end interface needs to be returned in advance and the user can obtain the corresponding result information after a certain period of time. Most backend users probably use a Websocket or SseEmitter backend (see the previous article -> Server push in SpringBoot). Or ask the front-end partner to poll for a separate result interface (maybe you need health insurance). DeferredResult is a good solution to this problem.

Code

The relevant code IS directly from the Internet CV down a copy, if there is a problem, please private I delete!

DeferredResultController:

@RestController
@RequestMapping(value = "/deferred-result")
public class DeferredResultController {

    @Autowired
    private DeferredResultService deferredResultService;

    private final String requestId = "haha";

    @GetMapping(value = "/getResult")
    public DeferredResult<DeferredResultResponse> getResult(@RequestParam(value = "timeout", required = false, defaultValue = "10000") Long timeout) {
        DeferredResult<DeferredResultResponse> deferredResult = new DeferredResult<>(timeout);
        deferredResultService.getResult(requestId, deferredResult);
        return deferredResult;
    }

    @GetMapping(value = "/result")
    public String settingResult(@requestParam (value = "desired", required = false, defaultValue = "success ") String desired) {
        DeferredResultResponse deferredResultResponse = new DeferredResultResponse();
        if (DeferredResultResponse.Msg.SUCCESS.getDesc().equals(desired)){
            deferredResultResponse.setCode(HttpStatus.OK.value());
            deferredResultResponse.setMsg(desired);
        }else{
            deferredResultResponse.setCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
            deferredResultResponse.setMsg(DeferredResultResponse.Msg.FAILED.getDesc());
        }
        deferredResultService.settingResult(requestId, deferredResultResponse);
        return "Done"; }}Copy the code

DeferredResultService:

public class DeferredResultService { private Map<String, Consumer<DeferredResultResponse>> taskMap; public DeferredResultService() { taskMap = new ConcurrentHashMap<>(); } public void getResult(String requestId, DeferredResult<DeferredResultResponse> DeferredResult) {// Request the timeout callback function deferredresult.onTimeout (() -> { taskMap.remove(requestId); DeferredResultResponse deferredResultResponse = new DeferredResultResponse(); deferredResultResponse.setCode(HttpStatus.REQUEST_TIMEOUT.value()); deferredResultResponse.setMsg(DeferredResultResponse.Msg.TIMEOUT.getDesc()); deferredResult.setResult(deferredResultResponse); }); Optional.ofNullable(taskMap) .filter(t -> ! t.containsKey(requestId)) .orElseThrow(() -> new IllegalArgumentException(String.format("requestId=%s is existing", requestId))); taskMap.putIfAbsent(requestId, deferredResult::setResult); } public void settingResult(String requestId, DeferredResultResponse deferredResultResponse) { if (taskMap.containsKey(requestId)) { Consumer<DeferredResultResponse> deferredResultResponseConsumer = taskMap.get(requestId); deferredResultResponseConsumer.accept(deferredResultResponse); taskMap.remove(requestId); }}}Copy the code

Code health

It can be seen that there are two request paths, one for creating data and the other for obtaining data. When obtaining the timeout set of data, the return value of the corresponding timeout set is returned.

  1. When we request the data interface, and when we don’t request the interface to put the data in, we return TIME_OUT.
  2. When we request the data interface, and when we do not request the interface to put the data in, SUCCESS is returned.

Run on

The put interface is nothing to understand, but a pure synchronous request interface. Focus on the corresponding interface that returns a value of DeferredResult, and see how it is executed internally. From the org. Springframework. Web. Servlet. DispatcherServlet# doDispatch begin execution.

  1. afterdoDispatchAnd then it goes to the correspondingorg.springframework.web.servlet.mvc.method.annotation.DeferredResultMethodReturnValueHandler#handleReturnValue, determine the current return value type.
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
      ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {

   if (returnValue == null) {
      mavContainer.setRequestHandled(true);
      return; } DeferredResult<? > result;if (returnValue instanceof DeferredResult) { // The data type in this Demoresult = (DeferredResult<? >) returnValue; }else if (returnValue instanceof ListenableFuture) { // Future enhancementsresult = adaptListenableFuture((ListenableFuture<? >) returnValue); }else if (returnValue instanceof CompletionStage) { // For phase processing in asynchronous executionresult = adaptCompletionStage((CompletionStage<? >) returnValue); }else {
      // Should not happen...
      throw new IllegalStateException("Unexpected return value type: " + returnValue);
   }
    WebAsyncUtils.getAsyncManager(webRequest).startDeferredResultProcessing(result, mavContainer); // Get the corresponding asynchronous manager and execute
}
Copy the code
  1. After wrapping the type of the return value, request processing proceeds:org.springframework.web.context.request.async.WebAsyncManager#startDeferredResultProcessing
public void startDeferredResultProcessing(
      finalDeferredResult<? > deferredResult, Object... processingContext) throws Exception {

   Assert.notNull(deferredResult, "DeferredResult must not be null");
   Assert.state(this.asyncWebRequest ! =null."AsyncWebRequest must not be null");

   Long timeout = deferredResult.getTimeoutValue();
   if(timeout ! =null) {
      this.asyncWebRequest.setTimeout(timeout);
   }
   // Add interceptor Settings now only the first and third (the first is the anonymous implementation of the DeferredResult class)
   List<DeferredResultProcessingInterceptor> interceptors = new ArrayList<>();
   interceptors.add(deferredResult.getInterceptor());
   interceptors.addAll(this.deferredResultInterceptors.values());
   interceptors.add(timeoutDeferredResultInterceptor);

   final DeferredResultInterceptorChain interceptorChain = new DeferredResultInterceptorChain(interceptors);
   // Add a timeout error to complete the callback processing to trigger
   this.asyncWebRequest.addTimeoutHandler(() -> {
      try {
         interceptorChain.triggerAfterTimeout(this.asyncWebRequest, deferredResult);
      }
      catch(Throwable ex) { setConcurrentResultAndDispatch(ex); }});this.asyncWebRequest.addErrorHandler(ex -> {
      if (!this.errorHandlingInProgress) {
         try {
            if(! interceptorChain.triggerAfterError(this.asyncWebRequest, deferredResult, ex)) {
               return;
            }
            deferredResult.setErrorResult(ex);
         }
         catch(Throwable interceptorEx) { setConcurrentResultAndDispatch(interceptorEx); }}});this.asyncWebRequest.addCompletionHandler(()
         -> interceptorChain.triggerAfterCompletion(this.asyncWebRequest, deferredResult));
   // Processing prior to request initialization - empty implementation
   interceptorChain.applyBeforeConcurrentHandling(this.asyncWebRequest, deferredResult);
   // Start asynchronous call processing
   startAsyncProcessing(processingContext);

   try {
      // 
      interceptorChain.applyPreProcess(this.asyncWebRequest, deferredResult);
      deferredResult.setResultHandler(result -> {
         result = interceptorChain.applyPostProcess(this.asyncWebRequest, deferredResult, result);
         setConcurrentResultAndDispatch(result);
      });
   }
   catch(Throwable ex) { setConcurrentResultAndDispatch(ex); }}Copy the code
  1. The request then goes down toorg.apache.catalina.core.AsyncContextImpl#setStarted. Where asynchronous event processing takes place.
public void setStarted(Context context, ServletRequest request, ServletResponse response, boolean originalRequestResponse) { synchronized (asyncContextLock) { this.request.getCoyoteRequest().action(ActionCode.ASYNC_START, this); // Async event handler - the hook function this.context = context; context.incrementInProgressAsyncCount(); this.servletRequest = request; this.servletResponse = response; this.hasOriginalRequestAndResponse = originalRequestResponse; this.event = new AsyncEvent(this, request, response); List<AsyncListenerWrapper> listenersCopy = new ArrayList<>(listeners); listeners.clear(); if (log.isDebugEnabled()) { log.debug(sm.getString("asyncContextImpl.fireOnStartAsync")); } for (AsyncListenerWrapper listener : listenersCopy) { try { listener.fireOnStartAsync(event); } catch (Throwable t) { ExceptionUtils.handleThrowable(t); log.warn(sm.getString("asyncContextImpl.onStartAsyncError", listener.getClass().getName()), t); }}}}Copy the code
  1. When the asynchronous event processing is complete, the correspondence is also requiredDispatchRespond.
private void setConcurrentResultAndDispatch(Object result) { synchronized (WebAsyncManager.this) { if (this.concurrentResult ! = RESULT_NONE) { return; } this.concurrentResult = result; this.errorHandlingInProgress = (result instanceof Throwable); } if (this.asyncWebRequest.isAsyncComplete()) { if (logger.isDebugEnabled()) { logger.debug("Async result set but request already complete: " + formatRequestUri()); } return; } if (logger.isDebugEnabled()) { boolean isError = result instanceof Throwable; logger.debug("Async " + (isError ? "error" : "result set") + ", dispatch to " + formatRequestUri()); } this.asyncWebRequest.dispatch(); // ASYNC_DISPATCH event register}Copy the code
  1. Determines whether the current request is a timeout logicorg.apache.coyote.AbstractProcessor#timeoutAsync:
@Override public void timeoutAsync(long now) { if (now < 0) { doTimeoutAsync(); } else { long asyncTimeout = getAsyncTimeout(); if (asyncTimeout > 0) { long asyncStart = asyncStateMachine.getLastAsyncStart(); if ((now - asyncStart) > asyncTimeout) { doTimeoutAsync(); } } else if (! asyncStateMachine.isAvailable()) { // Timeout the async process if the associated web application // is no longer running. doTimeoutAsync(); } } } private void doTimeoutAsync() { // Avoid multiple timeouts setAsyncTimeout(-1); asyncTimeoutGeneration = asyncStateMachine.getCurrentGeneration(); processSocketEvent(SocketEvent.TIMEOUT, true); // Send the timeout event}Copy the code
  1. HTTPTake a look at the event responseorg.apache.coyote.AbstractProcessor#actionClass for various HTTP events (too much code, I won’t post here).

Compare Callable

They both implement asynchronous data return: Callable submits tasks to TaskExecutor for execution, and then DispatcherServlet calls them back to the client. The results in DeferredResult are not necessarily put in by the current processing thread, and when the results are put in, the DispatcherServlet returns them to the client.

The difference is that the results are stored in different threads: the Callable execution process and the results can be understood as the same thread to complete. The result store of DeferredResult is not necessarily the current thread.

conclusion

This is a brief introduction to the use of DeferredResult and how it works. It gives you an idea of the types of return values supported by the framework (which you probably don’t use in real life).