zoukankan      html  css  js  c++  java
  • springmvc学习笔记--Interceptor机制和实践

    前言:
      Spring的AOP理念, 以及j2ee中责任链(过滤器链)的设计模式, 确实深入人心, 处处可以看到它的身影. 这次借项目空闲, 来总结一下SpringMVC的Interceptor机制, 并以用户登陆和日志记录作为案例, 以做实践.

    原理及类图:
      拦截器的使用, 其实非常的广泛, 尤其对通用普适的功能调用, 提取到拦截器层中实现.
      常见的拦截器有如下几种: 用户登陆/日志记录/性能评估/权限控制等等.
      拦截器Interceptor链, 横亘在控制器Controller(Action)前, 具体的接口定义如下所示:

    package org.springframework.web.servlet;
    
    public interface HandlerInterceptor {
        boolean preHandle(
                HttpServletRequest request, HttpServletResponse response,
                Object handler)
                throws Exception;
    
        void postHandle(
                HttpServletRequest request, HttpServletResponse response,
                Object handler, ModelAndView modelAndView)
                throws Exception;
    
        void afterCompletion(
                HttpServletRequest request, HttpServletResponse response,
                Object handler, Exception ex)
                throws Exception;
    }

      摘录了开涛老师的原话和图文解说:

    preHandle: 预处理回调方法, 在controller层之前调用.
        返回值: true 表示继续流程(如调用下一个拦截器或处理器).
            false 表示流程中断, 不会继续调用其他的拦截器或处理器.
    postHandle: 后处理回调方法, 在controller层之后调用(但在渲染视图之前), 
            我们可以通过modelAndView(模型和视图对象)对模型数据进行处理或对视图进行处理, 
            modelAndView也可能为null.
    afterCompletion: 整个请求处理完毕回调方法, 即在视图渲染完毕时回调, 
            类似于try-catch-finally中的finally.
            当然前提是该拦截器的preHandle返回true.

      正常流程和异常流程的图说明:

      注: 图摘自开涛老师的博客, <<第五章 处理器拦截器详解——跟着开涛学SpringMVC>>. 
      但有多个拦截器的时候, 其配置顺序也特别重要, preHandle是顺序执行, postHandle则是逆序执行, afterCompletion也是逆序执行.
      集成于springmvc时, 配置也非常的简洁, 如下样例即可:

        <mvc:interceptors>
            <!-- 使用bean定义一个Interceptor,直接定义在mvc:interceptors根下面的Interceptor将拦截所有的请求 -->
            <bean class="com.host.app.web.interceptor.AllInterceptor"/>
            <mvc:interceptor>
                <!-- 定义在mvc:interceptor下面的表示是对特定的请求才进行拦截的 -->
                <mvc:mapping path="/**"/>
                <bean class="xxx.xxx.XXXInterceptor"/>
            </mvc:interceptor>
            <mvc:interceptor>
                <mvc:mapping path="/**"/>
                <bean class="yyy.yyy.YYYInterceptor"/>
            </mvc:interceptor>
        </mvc:interceptors>

      注: 在最外层定义的Interceptor类, 对所有的url映射都进行拦截, 而mvc:interceptor标签申明的interceptor则通过mvc:mapping来自定义过滤规则.

    用户登陆:
    用户登陆验证, 是最常见的一种需求, 也是很多开发者第一次使用拦截器使用的对象. 因此我们就以此作为案例.
    比如我们编写如下代码:
    @Controller
    @RequestMapping("/")
    public class HelloController {
    
       @RequestMapping(value="/login", method={RequestMethod.POST, RequestMethod.GET})
       @ResponseBody
       public String login(@RequestParam("username") String username,
                      @RequestParam("password") String password,
                      HttpSession session) {
          session.setAttribute("user", "...");
          return "ok";
       }
    
       @RequestMapping(value="/echo", method={RequestMethod.POST, RequestMethod.GET})
       public ModelAndView echo(@RequestParam("message") String message,
                      HttpSession session, HttpServletResponse response) {
          ModelAndView mav = new ModelAndView();
    
          // *) 判断是否已经登陆
          Object obj = session.getAttribute("user");
          if ( obj == null ) {
             try {
                response.sendRedirect("/html/login.html");
             } catch (IOException e) {
                e.printStackTrace();
             }
          }
    
          mav.addObject("message", message);
          mav.setViewName("/echo");
          return mav;
    
       }
    
    }
        比如echo函数, 需要添加一段判断用户是否登陆的代码, 若没登陆, 需要重定向到登陆页面上去.
    当类似这样的接口很多, 这段登陆判断的代码, 就会被粘贴复制很多, 若登陆判断逻辑有变动, 难免形成蝴蝶效应.
    我们可以抽象到拦截器中去实现, 添加UserVerifyInterceptor类.
        @Component
        public class UserVerifyIntercptor extends HandlerInterceptorAdapter {
    
            private String[] allowUrls = new String[] {
                    // *) 用户登陆相关的接口
                    "/login",
            };
    
            @Override
            public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
                String uri = request.getRequestURI();
                for ( String allowUri : allowUrls ) {
                    if ( allowUri.equalsIgnoreCase(uri) ) {
                        return true;
                    }
                }
    
                // *) 判断是否已经登陆
                HttpSession session = request.getSession();
                Object obj = session.getAttribute("user");
                if ( obj == null ) {
                    response.sendRedirect("/html/login.html");
                    return false;
                }
    
                // *)
                return true;
    
            }
    
        }
        注: 有些url不需要登陆判断, 可以添加排除数组以实现白名单机制, 类似上边代码的allowUrls数组.
    然后在springmvc的dispatcher-servlet.xml中添加如下配置:
            <!-- 拦截器列表 -->
            <mvc:interceptors>
                <!-- 用户登陆的验证拦截器 -->
                <mvc:interceptor>
                    <mvc:mapping path="/**" />
                    <mvc:exclude-mapping path="/html/**" />
                    <bean class="com.springapp.mvc.interceptor.UserVerifyIntercptor" />
                </mvc:interceptor>
            </mvc:interceptors>
    
        注: 对于mvc:mapping和mvc:exclude-mapping, 很好地调控了拦截器作用对象的范围.
    同时, 这样之前的echo函数, 就可以简化为:
        @RequestMapping(value="/echo", method={RequestMethod.POST, RequestMethod.GET})
        public ModelAndView echo(@RequestParam("message") String message) {
            ModelAndView mav = new ModelAndView();
            mav.addObject("message", message);
            mav.setViewName("/echo");
            return mav;
        }
        这样就比之前的代码要简洁很多了.
    日志记录:
    其实, 这边我希望到达的一个目的是, 一个完整的rest api请求, 单独输出一条日志, 里面包含各类信息, 包括各个子过程的调用过程(耗时, 返回结果), 请求参数, 最终结果等. 这样的好处显而易见, 能够避免多个点的日志, 分散在多行, 当请求量多得时候, 难以寻找和聚合.
    这个实现机制, 大致和我之前写过的一篇文章类似: Thrift 个人实战--Thrift RPC服务框架日志的优化.
    大致的代码示例效果如下所示:
      @RequestMapping(value="/sample", method={RequestMethod.GET, RequestMethod.POST})
       @ResponseBody
       public String sample(@RequestParam("message") String message) {
          // *) 记录请求参数
          RestLoggerUtility.noticeLog("[params: {message:%s}]", message);
    
          // serviceA.call(),
          // 记录调用的子过程/子服务, 结果是什么, 总共耗时多少等等
          RestLoggerUtility.noticeLog("[serviceA.call, params: xxx, result: xxx, consume xs]");
    
          // serviceB.call(),
          // 记录调用的子过程/子服务, 结果是什么, 总共耗时多少等等
          RestLoggerUtility.noticeLog("[serviceB.call, params: xxx, result: xxx, consume xs]");
    
          // *) 记录最终的响应结果
          RestLoggerUtility.noticeLog("[response: ok]");
          return "ok";
       }
    
        其最终的日志输出如下所示:
    [params: {message:10}][serviceA.call, params: xxx, result: xxx, consume xs][serviceB.call, params: xxx, result: xxx, consume xs][response: ok]
        我们可以借助, 线程私有变量ThreadLocal来组装日志, 然后在Action的外层做拦截, 并做日志的准备和输出.
    1). 添加借助ThreadLocal实现的日志聚合工具类
    对RestLoggerUtility类的设计如下:
      public class RestLoggerUtility {
    
            private static final Logger restLogger = LoggerFactory.getLogger("rest");
    
            public static final ThreadLocal<StringBuilder> threadLocals = new ThreadLocal<StringBuilder>();
    
            public static void beforeInvoke() {
                StringBuilder sb = threadLocals.get();
                if (sb == null) {
                    sb = new StringBuilder();
                    threadLocals.set(sb);
                }
                sb.delete(0, sb.length());
            }
    
            public static void returnInvoke() {
                StringBuilder sb = threadLocals.get();
                if (sb != null && sb.length() > 0) {
                    restLogger.info(sb.toString());
                }
            }
    
            public static void throwableInvoke(String fmt, Object... args) {
                StringBuilder sb = threadLocals.get();
                if (sb != null) {
                    restLogger.info(sb.toString() + " " + String.format(fmt, args));
                }
            }
    
            public static void noticeLog(String fmt, Object... args) {
                StringBuilder sb = threadLocals.get();
                if (sb != null) {
                    // *) 对长度进行限定
                    if ( sb.length() < 1024 ) {
                        sb.append(String.format(fmt, args));
                    }
                }
            }
    
        }
    
        2). 实现日志拦截器
    然后, 我们定义拦截器类RestLoggerInterceptor, 其具体的类代码如下:
      public class RestLoggerInterceptor extends HandlerInterceptorAdapter {
    
            private static final Logger restLogger = LoggerFactory.getLogger("rest");
    
            @Override
            public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
                // *) 日志准备
                RestLoggerUtility.beforeInvoke();
                return super.preHandle(request, response, handler);
            }
    
            @Override
            public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
                super.postHandle(request, response, handler, modelAndView);
            }
    
            @Override
            public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
                super.afterCompletion(request, response, handler, ex);
                // *) 进行日志的刷新
                RestLoggerUtility.returnInvoke();
            }
        }
        根据springmvc拦截器的原理, 我们需要把日志初始化工作, 放在preHandle中实现. 把日志整体输入, 放在afterCompletion函数中实现.
    3). 添加拦截器配置
    再次添加拦截器配置, 并把它置于首位.
        <!-- 拦截器列表 -->
            <mvc:interceptors>
                <!-- 日志拦截器, 更好地记录整个请求过程 -->
                <mvc:interceptor>
                    <mvc:mapping path="/**"/>
                    <mvc:exclude-mapping path="/html/**" />
                    <bean class="com.springapp.mvc.interceptor.RestLoggerInterceptor" />
                </mvc:interceptor>
    
                <mvc:interceptor>
                    <mvc:mapping path="/**" />
                    <mvc:exclude-mapping path="/html/**" />
                    <bean class="xxx.xxx.XXXIntercptor" />
                </mvc:interceptor>
            </mvc:interceptors>
        4). 完善异常的处理
    对异常的拦截, 需要再补充, 定义一个ControlAdvice, 在处理异常的代码中, 添加异常日志记录的pointcut.
        @ControllerAdvice
        public class RestApiControlAdvice {
    
            private static final Logger restLogger = LoggerFactory.getLogger("rest");
    
            @ExceptionHandler(value=Exception.class)
            @ResponseBody
            public String handle(Exception e) {
                restLogger.warn("exception", e);
                RestLoggerUtility.throwableInvoke("[exception: msg:%s]", e.getMessage());
                return "error";
            }
    
        }
        这样, 我们想要实现的基本目标就能达到了.

    示例代码:
      样例代码的下载:http://pan.baidu.com/s/1jH1ggZ0.
      代码类组织如下:
      
    总结:
      好久想写这篇文章了,算是对springmvc拦截器机制的一份整理和自身理解. 希望能对读者有益,对自己而言,权当学习笔记.

    公众号&游戏站点:
      个人微信公众号: 木目的H5游戏世界

      个人游戏作品集站点(尚在建设中...): www.mmxfgame.com,  也可直接ip访问http://120.26.221.54/.
  • 相关阅读:
    .net core 一次坑爹的类库打包过程
    elasticsearch的store属性 vs _source字段
    VMware虚拟机中CentOS设置固定IP
    Elasticsearch 中文分词(elasticsearch-analysis-ik) 安装
    docker之docker-machine用法
    docker之数据卷管理
    docker之Dockerfile实践
    docker之Dockerfile指令介绍
    docker之手动构建新的镜像
    docker之container
  • 原文地址:https://www.cnblogs.com/mumuxinfei/p/5072675.html
Copyright © 2011-2022 走看看