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.
- ajax rotation
- Long rotation training
- websocket
- 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
- The Controller returns a DeferredResult object and saves it in an accessible memory queue or list.
- Spring Mvc starts asynchronous processing.
- At the same time, the dispatcher servlet and all configured filters exit the request processing thread, but the Response remains open.
- The application sets DeferredResult from a thread, and Spring MVC dispatches the request back to the Servlet container.
- 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