springmvc学习笔记--Interceptor机制和实践
前言:
Spring的AOP理念, 以及j2ee中责任链(过滤器链)的设计模式, 确实深入人心, 处处可以看到它的身影. 这次借项目空闲, 来总结一下SpringMVC的Interceptor机制, 并以用户登陆和日志记录作为案例, 以做实践.
原理及类图:
拦截器的使用, 其实非常的广泛, 尤其对通用普适的功能调用, 提取到拦截器层中实现.
常见的拦截器有如下几种: 用户登陆/日志记录/性能评估/权限控制等等.
拦截器Interceptor链, 横亘在控制器Controller(Action)前, 具体的接口定义如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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; } |
摘录了开涛老师的原话和图文解说:
1 2 3 4 5 6 7 8 9 | preHandle: 预处理回调方法, 在controller层之前调用. 返回值: true 表示继续流程(如调用下一个拦截器或处理器). false 表示流程中断, 不会继续调用其他的拦截器或处理器. postHandle: 后处理回调方法, 在controller层之后调用(但在渲染视图之前), 我们可以通过modelAndView(模型和视图对象)对模型数据进行处理或对视图进行处理, modelAndView也可能为 null . afterCompletion: 整个请求处理完毕回调方法, 即在视图渲染完毕时回调, 类似于 try - catch - finally 中的 finally . 当然前提是该拦截器的preHandle返回 true . |
正常流程和异常流程的图说明:
注: 图摘自开涛老师的博客, <<第五章 处理器拦截器详解——跟着开涛学SpringMVC>>.
但有多个拦截器的时候, 其配置顺序也特别重要, preHandle是顺序执行, postHandle则是逆序执行, afterCompletion也是逆序执行.
集成于springmvc时, 配置也非常的简洁, 如下样例即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 | <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来自定义过滤规则.
用户登陆:
用户登陆验证, 是最常见的一种需求, 也是很多开发者第一次使用拦截器使用的对象. 因此我们就以此作为案例.
比如我们编写如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | @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类.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | @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中添加如下配置:
1 2 3 4 5 6 7 8 9 | <!-- 拦截器列表 --> <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函数, 就可以简化为:
1 2 3 4 5 6 7 | @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服务框架日志的优化.
大致的代码示例效果如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | @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" ; } |
其最终的日志输出如下所示:
1 | [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类的设计如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | 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, 其具体的类代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 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). 添加拦截器配置
再次添加拦截器配置, 并把它置于首位.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <!-- 拦截器列表 --> <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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | @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/.
posted on 2015-12-25 15:45 mumuxinfei 阅读(3746) 评论(1) 编辑 收藏 举报
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构