前言:
  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})@ResponseBodypublic 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类.
    @Componentpublic class UserVerifyIntercptor extends HandlerInterceptorAdapter {private String[] allowUrls = new String[] {// *) 用户登陆相关的接口"/login",};@Overridepublic 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})@ResponseBodypublic 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");@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// *) 日志准备RestLoggerUtility.beforeInvoke();return super.preHandle(request, response, handler);}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {super.postHandle(request, response, handler, modelAndView);}@Overridepublic 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.
    @ControllerAdvicepublic class RestApiControlAdvice {private static final Logger restLogger = LoggerFactory.getLogger("rest");@ExceptionHandler(value=Exception.class)@ResponseBodypublic 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学习笔记 以下是我整理的SpringMVC学习笔记: 导入jar包 一:springmvc工作流程. ①.     servlet容器初始化一个request请求 ②.     Di ...

  2. JavaEE 企业级分布式高级架构师(四)SpringMVC学习笔记(4)

    SpringMVC学习笔记 高级应用篇 ControllerAdvice @ControllerAdvice @ModelAttribute 作用于方法 作用于方法参数 @InitBinder @Ex ...

  3. springmvc学习笔记二:重定向,拦截器,参数绑定

    springmvc学习笔记二:重定向,拦截器,参数绑定 Controller方法返回值 返回ModelAndView controller方法中定义ModelAndView对象并返回,对象中可添加mo ...

  4. SpringMVC学习笔记_01

    SpringMVC学习笔记_01 1.JAVAEE体系结构 JAVAEE体系结构图如下所示: 2.什么是springmvc? 什么是mvc? Model1 Model2 SpringMVC是什么? S ...

  5. SpringMVC:学习笔记(11)——依赖注入与@Autowired

    SpringMVC:学习笔记(11)--依赖注入与@Autowired 使用@Autowired 从Spring2.5开始,它引入了一种全新的依赖注入方式,即通过@Autowired注解.这个注解允许 ...

  6. SpringMVC:学习笔记(10)——整合Ckeditor且实现图片上传

    SpringMVC:学习笔记(10)--整合Ckeditor且实现图片上传 配置CKEDITOR 精简文件 解压之后可以看到ckeditor/lang下面有很多语言的js,如果不需要那么多种语言的,可 ...

  7. springmvc学习笔记(10)-springmvc注解开发之商品改动功能

    springmvc学习笔记(10)-springmvc注解开发之商品改动功能 springmvc学习笔记(10)-springmvc注解开发之商品改动功能 标签: springmvc springmv ...

  8. springmvc学习笔记(19)-RESTful支持

    springmvc学习笔记(19)-RESTful支持 标签: springmvc springmvc学习笔记19-RESTful支持 概念 REST的样例 controller REST方法的前端控 ...

  9. SpringMVC:学习笔记(5)——数据绑定及表单标签

    SpringMVC--数据绑定及表单标签 理解数据绑定 为什么要使用数据绑定 基于HTTP特性,所有的用户输入的请求参数类型都是String,比如下面表单: 按照我们以往所学,如果要获取请求的所有参数 ...

最新文章

  1. php开发之登录注册教程,PHP开发登录注册完整代码之注册PHP页面
  2. RedHat Linux下利用sersync进行实时同步数据
  3. python画图代码星星-Python打印“菱形”星号代码方法
  4. 如何利用反射实现EL表达式
  5. 高度平衡的二叉搜索树基础概念与经典题目(Leetcode题解-Python语言)
  6. 【Java学习】JDBC可以再深一点理解
  7. 华为交换机CPU SNMP OID [简单整理]
  8. solr通过连接数据库删除document 总结
  9. *关于C++堆和栈的理解
  10. 系统架构设计师与系统分析师历年实体分析与解答下载_架构设计的本质
  11. Adobe LiveCycle Designer 报表设计器
  12. Zipf law 定律
  13. 【电脑小白】提高ppt矢量图导出分辨率
  14. SPSS在农业生产中的应用
  15. 大数据与云计算有什么关系?
  16. 逍遥书生服务器啥时候维护完毕,《逍遥西游2》12月8日服务器维护公告
  17. 简单笔记(rsrp/mbps/session/dialog/dbm)
  18. redis的多路复用原理
  19. 各系统安装NetFrameWork3.5 安装
  20. 互联网日报 | 5月2日 星期日 | 五一档总票房破5亿;中国联通在香港正式推出5G服务;欧盟首次对苹果发起反垄断诉讼

热门文章

  1. java学习笔记(十二)----集合
  2. 解析 http 请求 header 错误_详解http报文(2)-web容器是如何解析http报文的
  3. 开发环境 测试环境 定义_「PHP7数组详解」:第1章 环境搭建安装(一)
  4. Win64 驱动内核编程-11.回调监控进线程句柄操作
  5. POJ1719行列匹配
  6. hdu4022 map+multiset
  7. 【五线谱】Sibelius 7.5.1 打谱软件安装 ( 软件下载 | 软件安装 )
  8. 【Android 逆向】函数拦截 ( GOT 表数据结构分析 | 函数根据 GOT 表进行跳转的流程 )
  9. 【组合数学】组合恒等式 ( 递推 组合恒等式 | 变下项求和 组合恒等式 简单和 | 变下项求和 组合恒等式 交错和 )
  10. c++入门之运算符重载