zoukankan      html  css  js  c++  java
  • SpringMVC之八:基于SpringMVC拦截器和注解实现controller中访问权限控制,及异步模式

    一、SpringMVC定义interceptor方式

    在SpringMVC 中定义一个Interceptor是比较非常简单,主要有两种方式:
    第一种:实现HandlerInterceptor 接口,或者是继承实现了HandlerInterceptor 接口的类,例如HandlerInterceptorAdapter; 
    第二种:实现Spring的WebRequestInterceptor接口,或者是继承实现了WebRequestInterceptor的类。

    1.1、HandlerInterceptorAdapter

    1.1.1、 HandlerInterceptor接口

    SpringMVC的拦截器HandlerInterceptor对应提供了三个preHandle,postHandle,afterCompletion方法:

    1.  boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handle)方法:该方法将在请求处理之前进行调用,只有该方法返回true,才会继续执行后续的Interceptor和Controller,当返回值为true 时就会继续调用下一个Interceptor的preHandle 方法,如果已经是最后一个Interceptor的时候就会是调用当前请求的Controller方法; 
    2. void postHandle (HttpServletRequest request, HttpServletResponse response, Object handle, ModelAndView modelAndView)方法:该方法将在请求处理之后,DispatcherServlet进行视图返回渲染之前进行调用,可以在这个方法中对Controller 处理之后的ModelAndView 对象进行操作。 
    3. void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handle, Exception ex)方法:该方法也是需要当前对应的Interceptor的preHandle方法的返回值为true时才会执行,该方法将在整个请求结束之后,也就是在DispatcherServlet 渲染了对应的视图之后执行。用于进行资源清理。

    1.1.2、HandlerInterceptorAdapter抽象类

      HandlerInterceptorAdapter它实现了AsyncHandlerInterceptor接口,为每个方法提供了空实现。这样下来HandlerInterceptorAdapter比HandlerInterceptor多了一个实现方法afterConcurrentHandlingStarted(),它来自HandlerInterceptorAdapter的直接实现类AsyncHandlerInterceptor,AsyncHandlerInterceptor接口直接继承了HandlerInterceptor,并新添了afterConcurrentHandlingStarted()方法用于处理异步请求。

    afterConcurrentHandlingStarted()执行时机:???

    1.2、WebRequestInterceptor

    1.2.1、 WebRequestInterceptor接口

    WebRequestInterceptor接口同HandlerInterceptor接口一样定义了三个方法,preHandle 、postHandle 以及afterCompletion。两个接口的方法名都相同,调用次序也相同。即preHandle是在请求处理之前调用;postHandle实在请求处理之后,视图渲染之前调用;afterCompletion是在视图渲染之后调用。接下来我们看看他们的不同之处。

    1.方法参数不同。WebRequest是Spring定义的接口,它是对HttpServletRequest的封装。对WebRequest 进行的操作都将同步到HttpServletRequest 中。WebRequest 的set/getAttribute(name, value, scope)比HttpServletRequest 的set/getAttribute多了一个scope参数。它有三个取值:

    • SCOPE_REQUEST:它的值是0,表示request请求作用范围。
    • SCOPE_SESSION :它的值是1,表示session请求作用范围。
    • SCOPE_GLOBAL_SESSION :它的值是2 ,表示全局会话作用范围,即ServletContext上下文作用范围。

    2.preHandle 方法。WebRequestInterceptor的该方法返回值为void,不是boolean。所以该方法不能用于请求阻断,一般用于资源准备。

    3.postHandle 方法。preHandle 中准备的数据都可以通过参数WebRequest访问。ModelMap 是Controller 处理之后返回的Model 对象,可以通过改变它的属性来改变Model 对象模型,达到改变视图渲染效果的目的。

    4.afterCompletion方法。Exception 参数表示的是当前请求的异常对象,如果Controller 抛出的异常已经被处理过,则Exception对象为null 。

    1.2.1、 WebRequestInterceptorAdapter抽象类

    在 Spring 框架之中,还提供了一个和WebRequestInterceptor接口长的很像的抽象类,那就是:WebRequestInterceptorAdapter,其实现了AsyncHandlerInterceptor接口,并在内部调用了WebRequestInterceptor接口。
    afterConcurrentHandlingStarted()执行时机:???

    1.3、HandlerInterceptorAdapter和WebRequestInterceptor相同点:

    两个接口都可用于Contrller层请求拦截,接口中定义的方法作用也是一样的。

    1.4、HandlerInterceptorAdapter和WebRequestInterceptor不同点:

    1. WebRequestInterceptor的入参WebRequest是包装了HttpServletRequest 和HttpServletResponse的,通过WebRequest获取Request中的信息更简便。
    2. WebRequestInterceptor的preHandle是没有返回值的,说明该方法中的逻辑并不影响后续的方法执行,所以这个接口实现就是为了获取Request中的信息,或者预设一些参数供后续流程使用。
    3. HandlerInterceptor的功能更强大也更基础,可以在preHandle方法中就直接拒绝请求进入controller方法。

    二、自定义拦截器配置方法

    1. 在sping的xml配置中可以用<mvc:interceptors>和<mvc:interceptor>来配置拦截器类(实现HandlerInterceptorAdapter)
    2. 在javaConfig中配置通过WebMvcConfiguration的实现类配置拦截器类(实现HandlerInterceptorAdapter)

    2.1、javaconfig中配置SpringMVC示例

    1、新建一个springboot项目auth-demo2

    2、权限校验相关的注解

    package com.dxz.authdemo2.web.auth;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Permission {
        /** 检查项枚举 */
        PermissionEnum[] permissionTypes() default {};
    
        /** 检查项关系 */
        RelationEnum relation() default RelationEnum.OR;
    }
    
    package com.dxz.authdemo2.web.auth;
    
    import java.io.PrintWriter;
    import java.lang.annotation.Annotation;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import javax.servlet.http.HttpSession;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    import org.springframework.web.method.HandlerMethod;
    import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
    
    /**
     * 权限检查拦截器 
     */
    @Component
    public class PermissionCheckInterceptor extends HandlerInterceptorAdapter {  
        /** 权限检查服务 */
        @Autowired
        private PermissionCheckProcessor permissionCheckProcessor;  
        @Override  
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {  
            //Class<?> clazz = handler.getClass();  
            Class<?> clazz = ((HandlerMethod)handler).getBeanType();
            System.out.println("PermissionCheckInterceptor.preHandle()" + clazz);
            for(Annotation a : clazz.getAnnotations()){
                System.out.println(a);
            }
            if (clazz.isAnnotationPresent(Permission.class)) { 
                Permission permission = (Permission) clazz.getAnnotation(Permission.class);  
                return permissionCheckProcessor.process(permission, request, response);  
            }  
            return true;  
        }  
        
        public boolean preHandle2(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 
            System.out.println("SecurityInterceptor:"+request.getContextPath()+","+request.getRequestURI()+","+request.getMethod());
            HttpSession session = request.getSession();
            if (session.getAttribute("uid") == null) {
                System.out.println("AuthorizationException:未登录!"+request.getMethod());
                if("POST".equalsIgnoreCase(request.getMethod())){
                    response.setContentType("text/html; charset=utf-8");  
                    PrintWriter out = response.getWriter();   
                    out.write("未登录!");
                    out.flush();
                    out.close();
                }else{
                    response.sendRedirect(request.getContextPath()+"/login"); 
                }
                return false;
            } else {
                return true;
            } 
        }  
    }  
    
    package com.dxz.authdemo2.web.auth;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    
    import org.springframework.stereotype.Component;
    @Component
    public class PermissionCheckProcessor {
        public boolean process(Permission permission, HttpServletRequest request, HttpServletResponse response) {
            PermissionEnum[] permissionTypes = permission.permissionTypes();
            try {
                String uid = request.getParameter("uid");
                if ("duanxz".equals(uid)) {
                    System.out.println("认证成功");
                    return true;
                } else {
                    System.out.println("认证失败");
                    return false;    
                }
            } catch (Exception e) {
                return false;
            }
        }
    }
    
    package com.dxz.authdemo2.web.auth;
    
    public enum PermissionEnum {
        DEVELOPER_VALID, DEVELOPER_FREEZE;
    }
    
    package com.dxz.authdemo2.web.auth;
    
    public enum RelationEnum {
        OR, AND;
    }

    3、SpringMVC拦截器配置

    package com.dxz.authdemo2.web.auth;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
    
    @Configuration
    public class WebMvcConfiguration extends WebMvcConfigurerAdapter {
    
        @Autowired
        PermissionCheckInterceptor permissionCheckInterceptor;
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            // addPathPatterns 用于添加拦截规则
            // excludePathPatterns 用户排除拦截
            // 映射为 user 的控制器下的所有映射
            registry.addInterceptor(permissionCheckInterceptor).addPathPatterns("/admin/*").excludePathPatterns("/index", "/");
            super.addInterceptors(registry);
        }
    }

    4、测试controller

    package com.dxz.authdemo2.web;
    
    import javax.servlet.http.HttpServletRequest;
    
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.ModelMap;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.servlet.ModelAndView;
    
    import com.dxz.authdemo2.web.auth.Permission;
    import com.dxz.authdemo2.web.auth.PermissionEnum;
    
    @Controller  
    @RequestMapping("/admin")  
    @Permission(permissionTypes = { PermissionEnum.DEVELOPER_VALID })  
    public class AppDetailController {  
        @RequestMapping(value="/appDetail", method = RequestMethod.GET)  
        public String doGet(ModelMap modelMap, HttpServletRequest httpServletRequest) {  
            //1. 业务操作,此处省略  
            System.out.println("appDetail.htm 处理中...");
            return "appDetail";
        }
    }  
      
    package com.dxz.authdemo2.web;
    
    import javax.servlet.http.HttpServletRequest;
    
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.ModelMap;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    
    import com.dxz.authdemo2.web.auth.Permission;
    import com.dxz.authdemo2.web.auth.PermissionEnum;
    
    @Controller  
    @RequestMapping("index")  
    public class IndexController {  
        @RequestMapping(method = RequestMethod.GET)  
        public void doGet(ModelMap modelMap, HttpServletRequest httpServletRequest) {  
            System.out.println("index");
        }  
    }  

    cotroller中的jsp文件appDetail.jsp

    <html>
    <h1>appDetail</h1>
    </html>

    启动类:

    package com.dxz.authdemo2;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.context.annotation.Bean;
    import org.springframework.web.servlet.ViewResolver;
    import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
    import org.springframework.web.servlet.config.annotation.EnableWebMvc;
    import org.springframework.web.servlet.view.InternalResourceViewResolver;
    
    @EnableWebMvc
    @EnableAutoConfiguration
    @SpringBootApplication
    public class AuthDemo2Application {
    
        public static void main(String[] args) {
            SpringApplication.run(AuthDemo2Application.class, args);
        }
    
        // 配置JSP视图解析器
        @Bean
        public ViewResolver viewResolver() {
            InternalResourceViewResolver resolver = new InternalResourceViewResolver();
            resolver.setPrefix("/WEB-INF/views/");
            resolver.setSuffix(".jsp");
            return resolver;
        }
    }

    结果:

    访问:http://localhost:8080/admin/appDetail?uid=duanxz2

    访问:http://localhost:8080/admin/appDetail?uid=duanxz

    2.2、xml中配置SpringMVC示例

    首先在springmvc.xml中加入自己定义的拦截器我的实现逻辑PermissionCheckInterceptor,如下:

    <!--配置拦截器, 多个拦截器,顺序执行 -->  
    <mvc:interceptors>    
        <mvc:interceptor>    
            <!-- 匹配的是url路径, 如果不配置或/**,将拦截所有的Controller -->  
            <mvc:mapping path="/" />  
            <mvc:mapping path="/user/**" />  
            <mvc:mapping path="/test/**" />  
            <bean class="com.dxz.authdemo2.web.auth.PermissionCheckInterceptor"></bean>    
        </mvc:interceptor>  
        <!-- 当设置多个拦截器时,先按顺序调用preHandle方法,然后逆序调用每个拦截器的postHandle和afterCompletion方法 -->  
    </mvc:interceptors> 

    三、体验Spring MVC的异步模式(Callable、WebAsyncTask、DeferredResult)

    Spring MVC的同步模式

    要知道什么是异步模式,就先要知道什么是同步模式。

    浏览器发起请求,Web服务器开一个线程处理(请求处理线程),处理完把处理结果返回浏览器。这就是同步模式。绝大多数Web服务器都如此般处理。这里面有几个关键的点:简单示例图如下

    此处需要明晰一个概念:比如tomcat,它既是一个web服务器,同时它也是个servlet后端容器(调java后端服务),所以要区分清楚这两个概念。请求处理线程是有限的,宝贵的资源~(注意它和处理线程的区别)

    1. 请求发起者发起一个request,然后会一直等待一个response,这期间它是阻塞的
    2. 请求处理线程会在Call了之后等待Return,自身处于阻塞状态(这个很关键)
    3. 然后都等待return,知道处理线程全部完事后返回了,然后把response反给调用者就算全部结束了
    问题在哪里?

    Tomcat等应用服务器的连接线程池实际上是有限制的;每一个连接请求都会耗掉线程池的一个连接数;如果某些耗时很长的操作,如对大量数据的查询操作、调用外部系统提供的服务以及一些IO密集型操作等,会占用连接很长时间,这个时候这个连接就无法被释放而被其它请求重用。如果连接占用过多,服务器就很可能无法及时响应每个请求;极端情况下如果将线程池中的所有连接耗尽,服务器将长时间无法向外提供服务!

    Spring MVC异步模式Demo Show

    Spring MVC3.2之后支持异步请求,能够在controller中返回一个Callable或者DeferredResult。由于Spring MVC的良好封装,异步功能使用起来出奇的简单。

    Callable案例:
    import java.util.concurrent.Callable;
    import java.util.concurrent.TimeUnit;
    
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.ResponseBody;
    
    @Controller
    @RequestMapping("/async/controller")
    public class AsyncHelloController {
    
        @ResponseBody
        @GetMapping("/hello")
        public Callable<String> helloGet() throws Exception {
            System.out.println(Thread.currentThread().getName() + " 主线程start");
    
            Callable<String> callable = () -> {
                System.out.println(Thread.currentThread().getName() + " 子线程start");
                TimeUnit.SECONDS.sleep(5); // 模拟处理业务逻辑,花费了5秒钟
                System.out.println(Thread.currentThread().getName() + " 子线程end");
    
                // 这里稍微小细节一下:最终返回的不是Callable对象,而是它里面的内容
                return "hello world";
            };
    
            System.out.println(Thread.currentThread().getName() + " 主线程end");
            return callable;
        }
    }

    输出:

    http-apr-8080-exec-3 主线程start
    http-apr-8080-exec-3 主线程end
    MvcAsync1 子线程start
    MvcAsync1 子线程end

    先明细两个概念:

    1. 请求处理线程:处理线程 属于 web 服务器线程,负责 处理用户请求,采用 线程池 管理。
    2. 异步线程:异步线程 属于 用户自定义的线程,也可采用 线程池管理。

    前端页面等待5秒出现结果,如下:

    注意:异步模式对前端来说,是无感知的,这是后端的一种技术。所以这个和我们自己开启一个线程处理,立马返回给前端是有非常大的不同的,需要注意~

    由此我们可以看出,主线程早早就结束了(需要注意,此时还并没有把response返回的,此处一定要注意),真正干事的是子线程(交给TaskExecutor去处理的,后续分析过程中可以看到),它的大致的一个处理流程图可以如下:

    这里能够很直接的看出:我们很大程度上提高了我们请求处理线程的利用率,从而肯定就提高了我们系统的吞吐量。

    异步模式处理步骤概述如下:
    1. 当Controller返回值是Callable的时候
    2. Spring就会将Callable交给TaskExecutor去处理(一个隔离的线程池)
    3. 与此同时将DispatcherServlet里的拦截器、Filter等等都马上退出主线程,但是response仍然保持打开的状态
    4. Callable线程处理完成后,Spring MVC将请求重新派发给容器**(注意这里的重新派发,和后面讲的拦截器密切相关)**
    5. 根据Callabel返回结果,继续处理(比如参数绑定、视图解析等等就和之前一样了)~~~

    Spring官方解释如下截图:

    WebAsyncTask案例:

    官方有这么一句话,截图给你:

    如果我们需要超时处理的回调或者错误处理的回调,我们可以使用WebAsyncTask代替Callable

    实际使用中,我并不建议直接使用Callable ,而是使用Spring提供的WebAsyncTask 代替,它包装了Callable,功能更强大些

    import java.util.concurrent.Callable;
    import java.util.concurrent.TimeUnit;
    
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.ResponseBody;
    import org.springframework.web.context.request.async.WebAsyncTask;
    
    @Controller
    @RequestMapping("/async/controller")
    public class AsyncHelloController {
    
        @ResponseBody
        @GetMapping("/hello")
        public WebAsyncTask<String> helloGet() throws Exception {
            System.out.println(Thread.currentThread().getName() + " 主线程start");
    
            Callable<String> callable = () -> {
                System.out.println(Thread.currentThread().getName() + " 子线程start");
                TimeUnit.SECONDS.sleep(5); // 模拟处理业务逻辑,话费了5秒钟
                System.out.println(Thread.currentThread().getName() + " 子线程end");
    
                return "hello world";
            };
    
            // 采用WebAsyncTask 返回 这样可以处理超时和错误 同时也可以指定使用的Excutor名称
            WebAsyncTask<String> webAsyncTask = new WebAsyncTask<>(3000, callable);
            // 注意:onCompletion表示完成,不管你是否超时、是否抛出异常,这个函数都会执行的
            webAsyncTask.onCompletion(() -> System.out.println("程序[正常执行]完成的回调"));
    
            // 这两个返回的内容,最终都会放进response里面去===========
            webAsyncTask.onTimeout(() -> "程序[超时]的回调");
            // 备注:这个是Spring5新增的
            // webAsyncTask.onError(() -> "程序[出现异常]的回调");
    
            System.out.println(Thread.currentThread().getName() + " 主线程end");
            return webAsyncTask;
        }
    }

    如上,由于我们设置了超时时间为3000ms,而业务处理是5s,所以会执行onTimeout这个回调函数。因此页面是会显示“程序[超时]的回调”这几个字。其执行的过程同Callback。

    下面我们简单看看WebAsyncTask的源码,非常简单,就是个包装:

    public class WebAsyncTask<V> implements BeanFactoryAware {
        
        // 正常执行的函数(通过WebAsyncTask的构造函数可以传进来)
        private final Callable<V> callable;
        // 处理超时时间(ms),可通过构造函数指定,也可以不指定(不会有超时处理)
        private Long timeout;
        // 执行任务的执行器。可以构造函数设置进来,手动指定。
        private AsyncTaskExecutor executor;
        // 若设置了,会根据此名称去IoC容器里找这个Bean (和上面二选一)  
        // 若传了executorName,请务必调用set方法设置beanFactory
        private String executorName;
        private BeanFactory beanFactory;
    
        // 超时的回调
        private Callable<V> timeoutCallback;
        // 发生错误的回调
        private Callable<V> errorCallback;
        // 完成的回调(不管超时还是错误都会执行)
        private Runnable completionCallback;
    
        ...
        
        // 这是获取执行器的逻辑
        @Nullable
        public AsyncTaskExecutor getExecutor() {
            if (this.executor != null) {
                return this.executor;
            } else if (this.executorName != null) {
                Assert.state(this.beanFactory != null, "BeanFactory is required to look up an executor bean by name");
                return this.beanFactory.getBean(this.executorName, AsyncTaskExecutor.class);
            } else {
                return null;
            }
        }
    
    
        public void onTimeout(Callable<V> callback) {
            this.timeoutCallback = callback;
        }
        public void onError(Callable<V> callback) {
            this.errorCallback = callback;
        }
        public void onCompletion(Runnable callback) {
            this.completionCallback = callback;
        }
    
        // 最终执行超时回调、错误回调、完成回调都是通过这个拦截器实现的
        CallableProcessingInterceptor getInterceptor() {
            return new CallableProcessingInterceptor() {
                @Override
                public <T> Object handleTimeout(NativeWebRequest request, Callable<T> task) throws Exception {
                    return (timeoutCallback != null ? timeoutCallback.call() : CallableProcessingInterceptor.RESULT_NONE);
                }
                @Override
                public <T> Object handleError(NativeWebRequest request, Callable<T> task, Throwable t) throws Exception {
                    return (errorCallback != null ? errorCallback.call() : CallableProcessingInterceptor.RESULT_NONE);
                }
                @Override
                public <T> void afterCompletion(NativeWebRequest request, Callable<T> task) throws Exception {
                    if (completionCallback != null) {
                        completionCallback.run();
                    }
                }
            };
        }
    
    }

    WebAsyncTask 的异步编程 API。相比于 @Async 注解,WebAsyncTask 提供更加健全的 超时处理 和 异常处理 支持。但是@Async也有更优秀的地方,就是他不仅仅能用于controller中~~~~(任意地方)

    DeferredResult案例:

    DeferredResult使用方式与Callable类似,但在返回结果上不一样,它返回的时候实际结果可能没有生成,实际的结果可能会在另外的线程里面设置到DeferredResult中去。

    这个特性非常非常的重要,对后面实现复杂的功能(比如服务端推技术、订单过期时间处理、长轮询、模拟MQ的功能等等高级应用

    官方给的Demo如下:

    自己写个非常粗糙的Demo:

    import java.util.ArrayList;
    import java.util.List;
    
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.ResponseBody;
    import org.springframework.web.context.request.async.DeferredResult;
    
    @Controller
    @RequestMapping("/async/controller")
    public class AsyncHelloController {
    
        private List<DeferredResult<String>> deferredResultList = new ArrayList<>();
    
        @ResponseBody
        @GetMapping("/hello")
        public DeferredResult<String> helloGet() throws Exception {
            DeferredResult<String> deferredResult = new DeferredResult<>();
    
            //先存起来,等待触发
            deferredResultList.add(deferredResult);
            return deferredResult;
        }
    
        @ResponseBody
        @GetMapping("/setHelloToAll")
        public void helloSet() throws Exception {
            // 让所有hold住的请求给与响应
            deferredResultList.forEach(d -> d.setResult("say hello to all"));
        }
    }

    我们第一个请求/hello,会先deferredResult存起来,然后前端页面是一直等待(转圈状态)的。知道我发第二个请求:setHelloToAll,所有的相关页面才会有响应~~

    执行过程

    官方:

    1. controller 返回一个DeferredResult,我们把它保存到内存里或者List里面(供后续访问)
    2. Spring MVC调用request.startAsync(),开启异步处理
    3. 与此同时将DispatcherServlet里的拦截器、Filter等等都马上退出主线程,但是response仍然保持打开的状态
    4. 应用通过另外一个线程(可能是MQ消息、定时任务等)给DeferredResult set值。然后Spring MVC会把这个请求再次派发给servlet容器
    5. DispatcherServlet再次被调用,然后处理后续的标准流程

    简单看看源码:

    public class DeferredResult<T> {
    
        private static final Object RESULT_NONE = new Object()
    
        
        // 超时时间(ms) 可以不配置
        @Nullable
        private final Long timeout;
        // 相当于超时的话的,传给回调函数的值
        private final Object timeoutResult;
    
        // 这三种回调也都是支持的
        private Runnable timeoutCallback;
        private Consumer<Throwable> errorCallback;
        private Runnable completionCallback;
    
    
        // 这个比较强大,就是能把我们结果再交给这个自定义的函数处理了 他是个@FunctionalInterface
        private DeferredResultHandler resultHandler;
    
        private volatile Object result = RESULT_NONE;
        private volatile boolean expired = false;
    
    
        // 判断这个DeferredResult是否已经被set过了(被set过的对象,就可以移除了嘛)
        // 如果expired表示已经过期了你还没set,也是返回false的
        // Spring4.0之后提供的
        public final boolean isSetOrExpired() {
            return (this.result != RESULT_NONE || this.expired);
        }
    
        // 没有isSetOrExpired 强大,建议使用上面那个
        public boolean hasResult() {
            return (this.result != RESULT_NONE);
        }
    
        // 还可以获得set进去的结果
        @Nullable
        public Object getResult() {
            Object resultToCheck = this.result;
            return (resultToCheck != RESULT_NONE ? resultToCheck : null);
        }
    
    
        public void onTimeout(Runnable callback) {
            this.timeoutCallback = callback;
        }
        public void onError(Consumer<Throwable> callback) {
            this.errorCallback = callback;
        }
        public void onCompletion(Runnable callback) {
            this.completionCallback = callback;
        }
    
        
        // 如果你的result还需要处理,可以这是一个resultHandler,会对你设置进去的结果进行处理
        public final void setResultHandler(DeferredResultHandler resultHandler) {
            Assert.notNull(resultHandler, "DeferredResultHandler is required");
            // Immediate expiration check outside of the result lock
            if (this.expired) {
                return;
            }
            Object resultToHandle;
            synchronized (this) {
                // Got the lock in the meantime: double-check expiration status
                if (this.expired) {
                    return;
                }
                resultToHandle = this.result;
                if (resultToHandle == RESULT_NONE) {
                    // No result yet: store handler for processing once it comes in
                    this.resultHandler = resultHandler;
                    return;
                }
            }
            try {
                resultHandler.handleResult(resultToHandle);
            } catch (Throwable ex) {
                logger.debug("Failed to handle existing result", ex);
            }
        }
    
        // 我们发现,这里调用是private方法setResultInternal,我们设置进来的结果result,会经过它的处理
        // 而它的处理逻辑也很简单,如果我们提供了resultHandler,它会把这个值进一步的交给我们的resultHandler处理
        // 若我们没有提供此resultHandler,那就保存下这个result即可
        public boolean setResult(T result) {
            return setResultInternal(result);
        }
    
        private boolean setResultInternal(Object result) {
            // Immediate expiration check outside of the result lock
            if (isSetOrExpired()) {
                return false;
            }
            DeferredResultHandler resultHandlerToUse;
            synchronized (this) {
                // Got the lock in the meantime: double-check expiration status
                if (isSetOrExpired()) {
                    return false;
                }
                // At this point, we got a new result to process
                this.result = result;
                resultHandlerToUse = this.resultHandler;
                if (resultHandlerToUse == null) {
                    this.resultHandler = null;
                }
            }
            resultHandlerToUse.handleResult(result);
            return true;
        }
    
        // 发生错误了,也可以设置一个值。这个result会被记下来,当作result
        // 注意这个和setResult的唯一区别,这里入参是Object类型,而setResult只能set规定的指定类型
        // 定义成Obj是有原因的:因为我们一般会把Exception等异常对象放进来。。。
        public boolean setErrorResult(Object result) {
            return setResultInternal(result);
        }
    
        // 拦截器 注意最终finally里面,都可能会调用我们的自己的处理器resultHandler(若存在的话)
        // afterCompletion不会调用resultHandler~~~~~~~~~~~~~
        final DeferredResultProcessingInterceptor getInterceptor() {
            return new DeferredResultProcessingInterceptor() {
                @Override
                public <S> boolean handleTimeout(NativeWebRequest request, DeferredResult<S> deferredResult) {
                    boolean continueProcessing = true;
                    try {
                        if (timeoutCallback != null) {
                            timeoutCallback.run();
                        }
                    } finally {
                        if (timeoutResult != RESULT_NONE) {
                            continueProcessing = false;
                            try {
                                setResultInternal(timeoutResult);
                            } catch (Throwable ex) {
                                logger.debug("Failed to handle timeout result", ex);
                            }
                        }
                    }
                    return continueProcessing;
                }
                @Override
                public <S> boolean handleError(NativeWebRequest request, DeferredResult<S> deferredResult, Throwable t) {
                    try {
                        if (errorCallback != null) {
                            errorCallback.accept(t);
                        }
                    } finally {
                        try {
                            setResultInternal(t);
                        } catch (Throwable ex) {
                            logger.debug("Failed to handle error result", ex);
                        }
                    }
                    return false;
                }
                @Override
                public <S> void afterCompletion(NativeWebRequest request, DeferredResult<S> deferredResult) {
                    expired = true;
                    if (completionCallback != null) {
                        completionCallback.run();
                    }
                }
            };
        }
    
        // 内部函数式接口 DeferredResultHandler
        @FunctionalInterface
        public interface DeferredResultHandler {
            void handleResult(Object result);
        }
    
    }

    DeferredResult的超时处理,采用委托机制,也就是在实例DeferredResult时给予一个超时时长(毫秒),同时在onTimeout中委托(传入)一个新的处理线程(我们可以认为是超时线程);当超时时间到来,DeferredResult启动超时线程,超时线程处理业务,封装返回数据,给DeferredResult赋值(正确返回的或错误返回的)

    Spring MVC异步模式中使用Filter和HandlerInterceptor

    看到上面的异步访问,不免我们会新生怀疑,若是普通的拦截器HandlerInterceptor,还生效吗?若生效,效果是怎么样的,现在我们直接看一下吧:(备注:我以上面Callable的Demo为示例)

    Filter
    // 注意,这里必须开启异步支持asyncSupported = true,否则报错:Async support must be enabled on a servlet and for all filters involved in async request processing
    @WebFilter(urlPatterns = "/*", asyncSupported = true)
    public class HelloFilter extends OncePerRequestFilter {
    
        @Override
        protected void initFilterBean() throws ServletException {
            System.out.println("Filter初始化...");
        }
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            System.out.println(Thread.currentThread().getName() + "--->" + request.getRequestURI());
            filterChain.doFilter(request, response);
        }
    
    }

    输出:

    http-apr-8080-exec-3--->/demowar_war/async/controller/hello
    http-apr-8080-exec-3 主线程start
    http-apr-8080-exec-3 主线程end
    MvcAsync1 子子子线程start
    MvcAsync1 子子子线程end

    由此可以看出,异步上下文,Filter还是只会被执行一次拦截的,符合我们的预期,所以没什么毛病。

    HandlerInterceptor
    public class HelloInterceptor implements HandlerInterceptor {
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            System.out.println(Thread.currentThread().getName() + "---preHandle-->" + request.getRequestURI());
            return true;
        }
    
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
            System.out.println(Thread.currentThread().getName() + "---postHandle-->" + request.getRequestURI());
        }
    
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            System.out.println(Thread.currentThread().getName() + "---postHandle-->" + request.getRequestURI());
        }
    }
    
    // 注册拦截器
    @Configuration
    @EnableWebMvc
    public class AppConfig implements WebMvcConfigurer {
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            // /**拦截所有请求
            registry.addInterceptor(new HelloInterceptor()).addPathPatterns("/**");
        }
    }

    输出:

    http-apr-8080-exec-3--->/demowar_war/async/controller/hello
    http-apr-8080-exec-3---preHandle-->/demowar_war/async/controller/hello
    
    http-apr-8080-exec-3 主线程start
    http-apr-8080-exec-3 主线程end
    MvcAsync1 子子子线程start
    MvcAsync1 子子子线程end
    
    // 注意  子子子线程处理结束后,再一次触发了preHandle=====
    // 此处还要一个细节:这里面的线程既不是子线程,也不是上面的线程  而是新开了一个线程~~~
    http-apr-8080-exec-5---preHandle-->/demowar_war/async/controller/hello
    http-apr-8080-exec-5---postHandle-->/demowar_war/async/controller/hello
    http-apr-8080-exec-5---afterCompletion-->/demowar_war/async/controller/hello

    从上面可以看出,如果我们就是普通的Spring MVC的拦截器,preHandler会执行两次,这也符合我们上面分析的处理步骤。所以我们在书写preHandler的时候,一定要特别的注意,要让preHandler即使执行多次,也不要受到影响(幂等)

    异步拦截器 AsyncHandlerInterceptor、CallableProcessingInterceptor、DeferredResultProcessingInterceptor

    Spring MVC给提供了异步拦截器,能让我们更深入的参与进去异步request的生命周期里面去。其中最为常用的为:AsyncHandlerInterceptor

    public class AsyncHelloInterceptor implements AsyncHandlerInterceptor {
    
        // 这是Spring3.2提供的方法,专门拦截异步请求的方式
        @Override
        public void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            System.out.println(Thread.currentThread().getName() + "---afterConcurrentHandlingStarted-->" + request.getRequestURI());
        }
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            System.out.println(Thread.currentThread().getName() + "---preHandle-->" + request.getRequestURI());
            return true;
        }
    
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
            System.out.println(Thread.currentThread().getName() + "---postHandle-->" + request.getRequestURI());
        }
    
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            System.out.println(Thread.currentThread().getName() + "---afterCompletion-->" + request.getRequestURI());
        }
    }

    输出:

    http-apr-8080-exec-3---preHandle-->/demowar_war/async/controller/hello
    http-apr-8080-exec-3 主线程start
    http-apr-8080-exec-3 主线程end
    
    // 这里发现,它在主线程结束后,子线程开始之前执行的(线程号还是同一个哦~)
    http-apr-8080-exec-3---afterConcurrentHandlingStarted-->/demowar_war/async/controller/hello
    
    MvcAsync1 子子子线程start
    MvcAsync1 子子子线程end
    http-apr-8080-exec-6---preHandle-->/demowar_war/async/controller/hello
    http-apr-8080-exec-6---postHandle-->/demowar_war/async/controller/hello
    http-apr-8080-exec-6---afterCompletion-->/demowar_war/async/controller/hello

    AsyncHandlerInterceptor提供了一个afterConcurrentHandlingStarted()方法, 这个方法会在Controller方法异步执行时开始执行, 而Interceptor的postHandle方法则是需要等到Controller的异步执行完才能执行

    (比如我们用DeferredResult的话,afterConcurrentHandlingStarted是在return的之后执行,而postHandle()是执行.setResult()之后执行)

    需要说明的是:如果我们不是异步请求,afterConcurrentHandlingStarted是不会执行的。所以我们可以把它当做加强版的HandlerInterceptor来用。平时我们若要使用拦截器,建议使用它。(Spring5,JDK8以后,很多的xxxAdapter都没啥用了,直接implements接口就成~)

    同样可以注册CallableProcessingInterceptor或者一个DeferredResultProcessingInterceptor用于更深度的集成异步request的生命周期

        @Override
        public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
            // 注册异步的拦截器、默认的超时时间、任务处理器TaskExecutor等等
            //configurer.registerCallableInterceptors();
            //configurer.registerDeferredResultInterceptors();
            //configurer.setDefaultTimeout();
            //configurer.setTaskExecutor();
        }

    只是一般来说,我们并不需要注册这种精细的拦截器,绝大多数情况下,使用AsyncHandlerInterceptor是够了的。 (Spring MVC的很多默认设置,请参考WebMvcConfigurationSupport

    区别使用

    我觉得最主要的区别是:DeferredResult需要自己用线程来处理结果setResult,而Callable的话不需要我们来维护一个结果处理线程。 总体来说,Callable的话更为简单,同样的也是因为简单,灵活性不够; 相对地,DeferredResult更为复杂一些,但是又极大的灵活性,所以能实现非常多个性化的、复杂的功能,可以设计高级应用。

    有些较常见的场景, Callable也并不能解决,比如说:我们访问A接口,A接口调用三方的服务,服务回调(注意此处指的回调,不是返回值)B接口,这种情况就没办法使用Callable了,这个时候可以使用DeferredResult

    使用原则:基本上在可以用Callable的时候,直接用Callable;而遇到Callable没法解决的场景的时候,可以尝试使用DeferredResult

    这里所指的Callable包括WebAsyncTask

    总结

    在Reactive编程模型越来越流行的今天,多一点对异步编程模型(Spring MVC异步模式)的了解,可以更容易去接触Spring5带来的新特性—响应式编程。 同时,异步编程是我们高效利用系统资源,提高系统吞吐量,编写高性能应用的必备技能。希望此篇文章能帮助到大家,运用到工作中~

    然后,关于DeferredResult的高级使用场景,见下一篇博文:高级应用和源码分析篇

    四、spring boot 加入拦截器后swagger不能访问问题

    网上找的资料中大部分只说添加这个

    // 注册拦截器
    @Configuration
    @EnableWebMvc
    public class AppConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(localInterceptor())
    .addPathPatterns("/**")
    .excludePathPatterns("/user/login")
    .excludePathPatterns("/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**");
    }

    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("swagger-ui.html")
    .addResourceLocations("classpath:/META-INF/resources/");
    registry.addResourceHandler("/webjars/**")
    .addResourceLocations("classpath:/META-INF/resources/webjars/");
    }
    //...

    }

     参考:https://cloud.tencent.com/developer/article/1497804

      

  • 相关阅读:
    第七课 GDB调试 (下)
    设计模式之原型模式
    设计模式之再读重构
    设计模式之代理模式
    设计模式之建造者模式
    设计模式之模板方法模式
    设计模式之抽象工厂模式
    设计模式之工厂模式
    设计模式之单例模式
    设计模式之6大原则
  • 原文地址:https://www.cnblogs.com/duanxz/p/5229364.html
Copyright © 2011-2022 走看看