前言:
  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/.

springmvc学习笔记--Interceptor机制和实践的更多相关文章

  1. SpringMVC学习笔记之二(SpringMVC高级参数绑定)

    一.高级参数绑定 1.1 绑定数组 需求:在商品列表页面选中多个商品,然后删除. 需求分析:功能要求商品列表页面中的每个商品前有一个checkbok,选中多个商品后点击删除按钮把商品id传递给Cont ...

  2. 史上最全的SpringMVC学习笔记

    SpringMVC学习笔记---- 一.SpringMVC基础入门,创建一个HelloWorld程序 1.首先,导入SpringMVC需要的jar包. 2.添加Web.xml配置文件中关于Spring ...

  3. springmvc学习笔记--REST API的异常处理

    前言: 最近使用springmvc写了不少rest api, 觉得真是一个好框架. 之前描述的几篇关于rest api的文章, 其实还是不够完善. 比如当遇到参数缺失, 类型不匹配的情况时, 直接抛出 ...

  4. springmvc学习笔记(简介及使用)

    springmvc学习笔记(简介及使用) 工作之余, 回顾了一下springmvc的相关内容, 这次也为后面复习什么的做个标记, 也希望能与大家交流学习, 通过回帖留言等方式表达自己的观点或学习心得. ...

  5. SpringMVC:学习笔记(3)——REST

    SpringMVC:学习笔记(3)——REST 了解REST风格 按照传统的开发方式,我们在实现CURD操作时,会写多个映射路径,比如对一本书的操作,我们会写多个URL,可能如下 web/delete ...

  6. springmvc学习笔记---面向移动端支持REST API

    前言: springmvc对注解的支持非常灵活和飘逸, 也得web编程少了以往很大一坨配置项. 另一方面移动互联网的到来, 使得REST API变得流行, 甚至成为主流. 因此我们来关注下spring ...

  7. SpringMVC:学习笔记(8)——文件上传

    SpringMVC--文件上传 说明: 文件上传的途径 文件上传主要有两种方式: 1.使用Apache Commons FileUpload元件. 2.利用Servlet3.0及其更高版本的内置支持. ...

  8. springmvc学习笔记(常用注解)

    springmvc学习笔记(常用注解) 1. @Controller @Controller注解用于表示一个类的实例是页面控制器(后面都将称为控制器). 使用@Controller注解定义的控制器有如 ...

  9. springmvc学习笔记(13)-springmvc注解开发之集合类型參数绑定

    springmvc学习笔记(13)-springmvc注解开发之集合类型參数绑定 标签: springmvc springmvc学习笔记13-springmvc注解开发之集合类型參数绑定 数组绑定 需 ...

随机推荐

  1. c++ 指针常量,常量指针

    当const遇到指针 一般来说,const修饰指针可以分为下面的集中情况. 描述 例子 含义 备注 const在*的左边 const int *b=&a; int const *b=& ...

  2. 无法从命令行或调试器启动服务,必须首先安装Windows服务(使用installutil.exe),然后用ServerExplorer、Windows服务器管理工具或NET START命令启动它

    无法从命令行或调试器启动服务,必须首先安装Windows服务(使用installutil.exe),然后用ServerExplorer.Windows服务器管理工具或NET START命令启动它 1. ...

  3. Codeforces Round #263 (Div. 1)

    B 树形dp 组合的思想. Z队长的思路. dp[i][1]表示以i为跟结点的子树向上贡献1个的方案,dp[i][0]表示以i为跟结点的子树向上贡献0个的方案. 如果当前为叶子节点,dp[i][0] ...

  4. Oracle数据库字段类型说明

    目前Oracle 数据库大概有26个字段类型,大体分为六类,分别是字符串类型.数字数据类型.日期时间数据类型.大型对象(LOB)数据类型.RAW和LONG RAW数据类型.ROWID和UROWID数据 ...

  5. Decorator

    1 意图:动态地给一个对象添加一些额外的职责.就增加功能来说,Decorator模式相比生成子类更灵活. 2 别名:包装器Wrapper 3 动机:将组件嵌入到另一个对象中,由这个对象添加边框.嵌入的 ...

  6. PHP中的变量详解

    php变量通过名只能我们就知道首先变量,是在程序执行期间,可以变化的量. 1.那变量是干嘛的呢,用变量就可以来保存我们值,这就是变量,那么我们接着来看,知道了变量是什么,以及它能干什么,我们再来看一下 ...

  7. Security » Authorization » 基于自定义策略的授权

    Custom Policy-Based Authorization¶ 基于自定义策略的授权 98 of 108 people found this helpful Underneath the cov ...

  8. Linux 系统启动过程

    linux启动时我们会看到许多启动信息. Linux系统的启动过程并不是大家想象中的那么复杂,其过程可以分为5个阶段: 内核的引导. 运行init. 系统初始化. 建立终端 . 用户登录系统. 内核引 ...

  9. OPENGL学习之路(0)--安装

    此次实验目的: 安装并且配置环境. 1 下载 https://www.opengl.org/ https://www.opengl.org/wiki/Getting_Started#Downloadi ...

  10. JS重要知识点

    这里列出了一些JS重要知识点(不全面,但自己感觉很重要).彻底理解并掌握这些知识点,对于每个想要深入学习JS的朋友应该都是必须的. 讲解还是以示例代码搭配注释的形式,这里做个小目录: JS代码预解析原 ...