• 使用Callable或DeferredResult实现springmvc的异步请求


    使用Callable实现springmvc的异步请求

    如果一个请求中的某些操作耗时很长,会一直占用线程。这样的请求多了,可能造成线程池被占满,新请求无法执行的情况。这时,可以考虑使用异步请求,即主线程只返回Callable类型,然后去处理新请求,耗时长的业务逻辑由其他线程执行。

    下面是一个示例demo,用线程睡眠来模拟耗时操作,springmvc配置以及视图解析器、拦截器等组件的注册略,详见https://www.cnblogs.com/dubhlinn/p/10808879.html博文,本文只展示controller组件,欢迎页面welcome.jsp略。

    package cn.monolog.annabelle.springmvc.controller;
    
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.Mapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.ResponseBody;
    
    import java.util.concurrent.Callable;
    
    /**
     * 处理器,用于测试springmvc异步请求
     * created on 2019-05-12
     */
    @Controller
    @RequestMapping(value = "/asyn")
    public class AsynController {
    
        /**
         * 使用Callable发送异步请求进入欢迎页面
         * @return
         */
        @RequestMapping(value = "/welcome")
        public Callable<String> welcome() {
            //打印主线程
            System.out.println("主线程" + Thread.currentThread().getName() + "开始:" + System.currentTimeMillis());
    
            //使用使用Callable获取页面路径
            Callable<String> path = new Callable() {
                @Override
                public String call() throws Exception {
                    //打印副线程
                    System.out.println("副线程" + Thread.currentThread().getName() + "开始:" + System.currentTimeMillis());
                    //线程休眠5秒钟
                    Thread.sleep(5000);
                    //打印副线程
                    System.out.println("副线程" + Thread.currentThread().getName() + "结束:" + System.currentTimeMillis());
                    //返回欢迎页面的url
                    return "welcome";
                }
            };
    
            //打印主线程
            System.out.println("主线程" + Thread.currentThread().getName() + "结束:" + System.currentTimeMillis());
    
            //返回页面
            return path;
        }
    
    }

    在浏览器访问/asyn/welcome,会延时5秒钟才进入欢迎页面,然后来看服务器日志,发现是这样的:

    1. 主线程开始、结束几乎是同一时刻;

    2. 副线程结束比开始晚了5秒多,是在执行线程休眠,即模拟"耗时多的业务逻辑";

    3. 拦截器的preHandle方法执行了两次。

    1和2都符合我们之前的描述和预期,但是为什么拦截器的preHandle方法会执行两次?来看一下springmvc异步请求的原理:

    1. 当处理器的方法返回Callable(以及其他异步形式的返回值,例如DeferredResult)时,springmvc会将

       Callable的实现方法交给TaskExecutor在一个隔离的线程中执行;

    2. 同时,DispatcherServlet(前端控制器)和所有的拦截器退出web容器的线程,但是response仍然保持打开状态;

    3. 当Callable的实现方法产生返回值时,springmvc会将请求重新派发给容器;

    4. DispatcherServlet(前端控制器)重新接收请求,并根据Callable的返回值进行视图渲染或者返回数据。

    因此,第一次请求之前执行了preHandle方法,当前端控制器接收到请求之后,发现返回值是Callable,

    拦截器就退出主线程了,也就没有了后面的postHandle和afterCompletion方法。

    使用DeferredResult实现springmvc的异步请求

    实际项目中的异步请求,可能在不同的接口中执行,甚至可能在不同的应用中执行,这时Callable就无法实现。

    springmvc提供了另一种异步返回类型:DeferredResult,其主要使用步骤是:

    1. 在主线程中新建DeferredResult实例,然后直接返回这个实例,主线程结束;

    2. 副线程在主线程新建的DeferredResult实例中设置值(调用setResult方法);

    3. 这时,主线程获取到副线程在DeferredResult实例中设置的值,重新接收请求、视图渲染或返回数据。

    下面是一个示例demo,我们用一个自定义队列来模拟DeferredResult的存取。

    自定义队列

    package cn.monolog.annabelle.springmvc.queue;
    
    import org.springframework.web.context.request.async.DeferredResult;
    
    import java.util.Queue;
    import java.util.concurrent.ConcurrentLinkedQueue;
    
    /**
     * 自定义队列,用于存储DeferredResult
     * created on 2019-05-12
     */
    public class DeferredResultQueue {
    
        //队列
        private static Queue<DeferredResult> deferredResultQueue = new ConcurrentLinkedQueue<>();
    
        /**
         * 添加DeferredResult进队列
         * @param deferredResult
         */
        public static void save(DeferredResult deferredResult) {
            deferredResultQueue.add(deferredResult);
        }
    
        /**
         * 从队列中取出第一个元素并删除
         * @return
         */
        public static DeferredResult get() {
            return deferredResultQueue.poll();
        }
    }

    controller组件

    package cn.monolog.annabelle.springmvc.controller;
    
    import cn.monolog.annabelle.springmvc.queue.DeferredResultQueue;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.Mapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.ResponseBody;
    import org.springframework.web.context.request.async.DeferredResult;
    
    import java.util.UUID;
    
    /**
     * 处理器,用于测试springmvc异步请求
     * created on 2019-05-12
     */
    @Controller
    @RequestMapping(value = "/asyn")
    public class AsynController {
    
        /**
         * 异步创建订单
         * 只返回DeferredResult,并不真正创建订单
         * @return
         */
        @RequestMapping("/createOrder")
        @ResponseBody
        public DeferredResult<String> createOrder() {
            //新建DeferredResult实例,并保存到队列中,参数的意义是最长等待5秒,否则返回值自动设置为timeout
            DeferredResult<String> deferredResult = new DeferredResult<>((long)5000, "timeout");
            DeferredResultQueue.save(deferredResult);
            //返回
            return deferredResult;
        }
    
        /**
         * 真正的创建订单
         * @return
         */
        @RequestMapping("/create")
        @ResponseBody
        public String createOrderActioin() {
            //随机生成订单号
            String orderNum = UUID.randomUUID().toString();
            //从队列中取出DeferredResult
            DeferredResult deferredResult = DeferredResultQueue.get();
            //向DeferredResult中存值
            deferredResult.setResult(orderNum);
            //返回
            return orderNum;
        }
    }

    直接在浏览器访问/asyn/createOrder,因为并没有线程为DeferredResult存值,因此等待5秒之后,返回timeout。

    如果在5秒之内,在浏览器的另一个标签访问/asyn/create,会发现第一个标签返回了第二个接口创建的订单编号。

  • 相关阅读:
    Python-内置数据结构listdictset
    Python-内置结构listsetdicttuple
    Python-内置数据结构
    Python-函数作用域和集合列表字典元祖
    Python-函数参数和文档
    Python-while循环+函数
    Python-分支循环
    Python基础
    五、Jmeter-数据库数据进行参数化
    mysql索引:四种类型,两种方法
  • 原文地址:https://www.cnblogs.com/dubhlinn/p/10853172.html
Copyright © 2020-2023  润新知