乐趣区

关于java:Spring-DeferredResult-异步请求

一、背景

最近在做我的项目的过程中,有一个领取的场景,前端须要依据领取的后果,跳转到不同的页面中。而咱们的领取告诉是支付方异步告诉回来的,因而在收回领取申请后
无奈立刻获取到领取后果,此时咱们就须要轮训交易后果,判断是否领取胜利。

二、剖析

要实现后端将领取后果告诉给前端,实现的形式有很多种。

  1. ajax 轮训
  2. 长轮训
  3. websocket
  4. sse
    ……

通过思考,最终决定应用 长轮训 来实现。而 Spring 的 DeferredResult 是一个异步申请,正好能够用来实现长轮训。而这个异步是基于 Servlet3 的异步来实现的,在 Spring 中 DeferredResult 后果会另起线程来解决,并不会占用容器 (Tomcat) 的线程,因而还能进步程序的吞吐量。

三、实现要求

前端申请 查问交易办法 (queryOrderPayResult), 后端将申请阻塞住 3s,如果在 3s 之内,领取告诉回调(payNotify) 过去了,那么之前查问交易
的办法立刻返回领取后果,否则返回超时了。

四、后端代码实现


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;

/**
 * 订单控制器
 *
 * @author huan.fu 2021/10/14 - 上午 9:34
 */
@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);
    }
    
    /**
     * 查问订单领取后果
     *
     * @param orderId 订单编号
     * @return DeferredResult
     */
    @GetMapping("queryOrderPayResult")
    public DeferredResult<String> queryOrderPayResult(@RequestParam("orderId") String orderId) {log.info("订单 orderId:[{}]发动了领取", orderId);
        ATOMIC_INTEGER.incrementAndGet();
        // 3s 超时
        DeferredResult<String> result = new DeferredResult<>(3000L);
        // 超时操作
        result.onTimeout(() -> {DEFERRED_RESULT.get(orderId).setResult("超时了");
            log.info("订单 orderId:[{}]发动领取, 获取后果超时了.", orderId);
        });
        
        // 实现操作
        result.onCompletion(() -> {log.info("订单 orderId:[{}]实现.", orderId);
            DEFERRED_RESULT.remove(orderId);
        });
        
        // 保留此 DeferredResult 的后果
        DEFERRED_RESULT.put(orderId, result);
        return result;
    }
    
    /**
     * 领取回调
     *
     * @param orderId 订单 id
     * @return 领取回调后果
     */
    @GetMapping("payNotify")
    public String payNotify(@RequestParam("orderId") String orderId) {log.info("订单 orderId:[{}]领取实现回调", orderId);
        
        // 默认后果产生了异样
        if ("123".equals(orderId)) {DEFERRED_RESULT.get(orderId).setErrorResult(new RuntimeException("订单产生了异样"));
            return "回调解决失败";
        }
        
        if (DEFERRED_RESULT.containsKey(orderId)) {Optional.ofNullable(DEFERRED_RESULT.get(orderId)).ifPresent(result -> result.setResult("实现领取"));
            // 设置之前 orderId toPay 申请的后果
            return "回调解决胜利";
        }
        return "回调解决失败";
    }
}

五、运行后果

1、超时操作


页面申请 http://localhost:8080/queryOrderPayResult?orderId=12345办法,在 3s 之内没有 DeferredResult#setResult 没有设置后果,间接返回超时了。

2、失常操作


页面申请 http://localhost:8080/queryOrderPayResult?orderId=12345办法之后,并立刻申请 http://localhost:8080/payNotify?orderId=12345 办法,失去了正确的后果。

六、DeferredResult 运行原理

  1. Controller 返回一个 DeferredResult 对象,并且把它保留在一个能够拜访的内存队列或列表中。
  2. Spring Mvc 开始异步解决。
  3. 同时,DispatcherServlet 和所有配置的过滤器退出申请解决线程,但 Response(响应)放弃关上状态。
  4. 应用程序从某个线程设置 DeferredResult,Spring MVC 将申请分派回 Servlet 容器。
  5. DispatcherServlet 再次被调用,并以异步产生的返回值复原解决。

六、注意事项

1、异样的解决

能够通过 @ExceptionHandler 来解决。

2、异步过程中的拦截器。

能够通过 DeferredResultProcessingInterceptor 或者 AsyncHandlerInterceptor 来实现。须要留神看拦截器办法上的正文,有些办法,如果调用了 setResult 等是不会再次执行的。

配置:

/**
 * 如果加了 @EnableWebMvc 注解的话, Spring 很多默认的配置就没有了,须要本人进行配置
 *
 * @author huan.fu 2021/10/14 - 上午 10:39
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
        // 默认超时工夫 60s
        configurer.setDefaultTimeout(60000);
        // 注册 deferred result 拦截器
        configurer.registerDeferredResultInterceptors(new CustomDeferredResultProcessingInterceptor());
    }
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new CustomAsyncHandlerInterceptor()).addPathPatterns("/**");
    }
}

七、残缺代码

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

八、参考链接

  1. https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-async-deferredresult
退出移动版