Spring DeferredResult asynchronous request

Keywords: Spring Spring Boot

1, Background

Recently, during the project, there was a payment scenario. The front end needs to jump to different pages according to the payment results. Our payment notice is sent back by the payer asynchronously, so after sending the payment request
If the payment result cannot be obtained immediately, we need to rotate the transaction result to judge whether the payment is successful.

2, Analysis

There are many ways to realize that the back end notifies the front end of the payment results.

  1. ajax rotation
  2. Long rotation training
  3. websocket
  4. sse
    ...

After consideration, it was finally decided to use long rotation training. Spring's DeferredResult is an asynchronous request, which can be used to implement long rotation training. This asynchrony is implemented based on the asynchrony of Servlet3. In spring, DeferredResult results will be processed by another thread and will not occupy the thread of the container (Tomcat), so it can also improve the throughput of the program.

3, Implementation requirements

The front end requests to query the transaction method (queryordpayresult), and the back end blocks the request for 3s. If the paynotify callback comes within 3s, the transaction will be queried before
The method of returns the payment result immediately, otherwise the return times out.

4, Backend code implementation

package com.huan.study.controller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;

import javax.annotation.PostConstruct;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Order controller
 *
 * @author huan.fu 2021/10/14 - 9:34 am
 */
@RestController
public class OrderController {
    
    private static final Logger log = LoggerFactory.getLogger(OrderController.class);
    
    private static volatile ConcurrentHashMap<String, DeferredResult<String>> DEFERRED_RESULT = new ConcurrentHashMap<>(20000);
    private static volatile AtomicInteger ATOMIC_INTEGER = new AtomicInteger(0);
    
    @PostConstruct
    public void printRequestCount() {
        Executors.newSingleThreadScheduledExecutor()
                .scheduleAtFixedRate(() -> {
                    log.error("" + ATOMIC_INTEGER.get());
                }, 10, 1, TimeUnit.SECONDS);
    }
    
    /**
     * Query order payment results
     *
     * @param orderId Order No
     * @return DeferredResult
     */
    @GetMapping("queryOrderPayResult")
    public DeferredResult<String> queryOrderPayResult(@RequestParam("orderId") String orderId) {
        log.info("order orderId:[{}]Initiated payment", orderId);
        ATOMIC_INTEGER.incrementAndGet();
        // 3s timeout
        DeferredResult<String> result = new DeferredResult<>(3000L);
        // Timeout operation
        result.onTimeout(() -> {
            DEFERRED_RESULT.get(orderId).setResult("Timeout");
            log.info("order orderId:[{}]Initiate payment,Getting results timed out.", orderId);
        });
        
        // operation completed
        result.onCompletion(() -> {
            log.info("order orderId:[{}]complete.", orderId);
            DEFERRED_RESULT.remove(orderId);
        });
        
        // Save the results of this DeferredResult
        DEFERRED_RESULT.put(orderId, result);
        return result;
    }
    
    /**
     * Payment callback
     *
     * @param orderId Order id
     * @return Payment callback result
     */
    @GetMapping("payNotify")
    public String payNotify(@RequestParam("orderId") String orderId) {
        log.info("order orderId:[{}]Payment completion callback", orderId);
        
        // An exception occurred in the default result
        if ("123".equals(orderId)) {
            DEFERRED_RESULT.get(orderId).setErrorResult(new RuntimeException("An exception occurred in the order"));
            return "Callback processing failed";
        }
        
        if (DEFERRED_RESULT.containsKey(orderId)) {
            Optional.ofNullable(DEFERRED_RESULT.get(orderId)).ifPresent(result -> result.setResult("Complete payment"));
            // Set the result of the previous orderId toPay request
            return "Callback processing succeeded";
        }
        return "Callback processing failed";
    }
}

5, Operation results

1. Timeout operation


Page request http://localhost:8080/queryOrderPayResult?orderId=12345 Method, there is no DeferredResult#setResult within 3s, there is no result set, and the timeout is returned directly.

2. Normal operation


Page request http://localhost:8080/queryOrderPayResult?orderId=12345 Method and immediately request http://localhost:8080/payNotify?orderId=12345 The correct results are obtained.

6, DeferredResult operation principle

  1. The Controller returns a DeferredResult object and saves it in an accessible memory queue or list.
  2. Spring Mvc starts asynchronous processing.
  3. At the same time, the dispatcher servlet and all configured filters exit the request processing thread, but the Response remains open.
  4. The application sets DeferredResult from a thread, and Spring MVC dispatches the request back to the Servlet container.
  5. The DispatcherServlet is called again and resumes processing with the asynchronously generated return value.

6, Precautions

1. Exception handling

It can be handled through @ ExceptionHandler.

2. Interceptor in asynchronous process.

It can be implemented through DeferredResultProcessingInterceptor or AsyncHandlerInterceptor. Note the comments on the interceptor method. Some methods will not be executed again if setResult is called.

to configure:

/**
 * If the @ EnableWebMvc annotation is added, many default configurations of Spring will not be available. You need to configure them yourself
 *
 * @author huan.fu 2021/10/14 - 10:39 am
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
        // The default timeout is 60s
        configurer.setDefaultTimeout(60000);
        // Register deferred result interceptor
        configurer.registerDeferredResultInterceptors(new CustomDeferredResultProcessingInterceptor());
    }
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new CustomAsyncHandlerInterceptor()).addPathPatterns("/**");
    }
}

7, Complete code

https://gitee.com/huan1993/spring-cloud-parent/tree/master/springboot/spring-deferred-result

8, Reference link

  1. https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-async-deferredresult

Posted by gdure on Thu, 14 Oct 2021 20:58:56 -0700