拦截器(Interceptor)同Filter 过滤器一样,它俩都是面向切面编程——AOP 的具体实现(AOP切面编程只是一种编程思想而已)。
你可以使用 Interceptor 来执行某些任务,例如在 Controller 处理请求之前编写日志,添加或更新配置……
在 Spring中 ,当请求发送到 Controller 时,在被 Controller 处理之前,它必须经过 Interceptors (0或多个)。
Spring Interceptor是一个非常类似于 Servlet Filter 的概念 。
如果你需要自定义 Interceptor 的话必须实现 org.springframework.web.servlet.HandlerInterceptor
接口或继承 org.springframework.web.servlet.handler.HandlerInterceptorAdapter
类,并且需要重写下面下面 3 个方法:
preHandler(HttpServletRequest request, HttpServletResponse response, Object handler) postHandler(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handle, Exception ex)
接下来结合实际代码进行学习。
LogInterceptor 类:
public class LogInterceptor extends HandlerInterceptorAdapter { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { long startTime = System.currentTimeMillis(); System.out.println("/n-------- LogInterception.preHandle --- "); System.out.println("Request URL: " + request.getRequestURL()); System.out.println("Start Time: " + System.currentTimeMillis()); request.setAttribute("startTime", startTime); return true; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("/n-------- LogInterception.postHandle --- "); System.out.println("Request URL: " + request.getRequestURL()); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("/n-------- LogInterception.afterCompletion --- "); long startTime = (Long) request.getAttribute("startTime"); long endTime = System.currentTimeMillis(); System.out.println("Request URL: " + request.getRequestURL()); System.out.println("End Time: " + endTime); System.out.println("Time Taken: " + (endTime - startTime)); } } 复制代码
OldLoginInterceptor 类:
public class OldLoginInterceptor extends HandlerInterceptorAdapter { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { System.out.println("/n-------- OldLoginInterceptor.preHandle --- "); System.out.println("Request URL: " + request.getRequestURL()); System.out.println("Sorry! This URL is no longer used, Redirect to /admin/login"); response.sendRedirect(request.getContextPath()+ "/admin/login"); return false; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { System.out.println("/n-------- OldLoginInterceptor.postHandle --- "); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { System.out.println("/n-------- OldLoginInterceptor.afterCompletion --- "); } } 复制代码
配置拦截器 :
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LogInterceptor()); registry.addInterceptor(new OldLoginInterceptor()).addPathPatterns("/admin/oldLogin"); registry.addInterceptor(new AdminInterceptor()).addPathPatterns("/admin/*").excludePathPatterns("/admin/oldLogin"); } } 复制代码
LogInterceptor
拦截器用于拦截所有请求; OldLoginInterceptor
用来拦截链接 “ / admin / oldLogin” ,它将重定向到新的 “ / admin / login”。 ; AdminInterceptor
用来拦截链接 “/admin/*” ,除了链接 “ / admin / oldLogin” 。
自定义 Controller 验证拦截器
@Controller public class LoginController { @RequestMapping("/index") public String index(Model model){ return "index"; } @RequestMapping(value = "/admin/login") public String login(Model model){ return "login"; } } 复制代码
同时依赖 thymeleaf 模板 构建两个页面。
index.html
<!DOCTYPE HTML> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8" /> <title>Spring Boot Mvc Interceptor example</title> </head> <body> <div style="border: 1px solid #ccc;padding: 5px;margin-bottom:10px;"> <a th:href="@{/}">Home</a> | <a th:href="@{/admin/oldLogin}">/admin/oldLogin (OLD URL)</a> </div> <h3>Spring Boot Mvc Interceptor</h3> <span style="color:blue;">Testing LogInterceptor</span> <br/><br/> See Log in Console.. </body> </html> 复制代码
login.html
<!DOCTYPE HTML> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8" /> <title>Spring Boot Mvc Interceptor example</title> </head> <body> <div style="border: 1px solid #ccc;padding: 5px;margin-bottom:10px;"> <a th:href="@{/}">Home</a> | <a th:href="@{/admin/oldLogin}">/admin/oldLogin (OLD URL)</a> </div> <h3>This is Login Page</h3> <span style="color:blue">Testing OldLoginInterceptor & AdminInterceptor</span> <br/><br/> See more info in the Console. </body> </html> 复制代码
一切准备完毕,启动该项目。打开网址: http://localhost:8080/index
关于该请求在后台的执行过程,用图解的方式进行展示:
如果此时点击 /admin/oldLogin (OLD URL) 或者在网址栏输入:http://localhost:8080/admin/oldLogin
控制台打印结果:
-------- LogInterception.preHandle --- Request URL: http://localhost:8080/admin/oldLogin Start Time: 1576329730709 -------- OldLoginInterceptor.preHandle --- Request URL: http://localhost:8080/admin/oldLogin Sorry! This URL is no longer used, Redirect to /admin/login -------- LogInterception.afterCompletion --- Request URL: http://localhost:8080/admin/oldLogin End Time: 1576329730709 Time Taken: 0 -------- LogInterception.preHandle --- Request URL: http://localhost:8080/admin/login Start Time: 1576329730716 -------- AdminInterceptor.preHandle --- -------- AdminInterceptor.postHandle --- -------- LogInterception.postHandle --- Request URL: http://localhost:8080/admin/login -------- AdminInterceptor.afterCompletion --- -------- LogInterception.afterCompletion --- Request URL: http://localhost:8080/admin/login End Time: 1576329730718 Time Taken: 2 复制代码
同样我们用图解的形式分析:
性能监控
如记录一下请求的处理时间,得到一些慢请求(如处理时间超过500毫秒),从而进行性能改进,一般的反向代理服务器如 apache 都具有这个功能,但此处我们演示一下使用拦截器怎么实现。
实现分析:
1、在进入处理器之前记录开始时间,即在拦截器的 preHandle 记录开始时间;
2、在结束请求处理之后记录结束时间,即在拦截器的 afterCompletion 记录结束实现,并用结束时间-开始时间得到这次请求的处理时间。
问题:
我们的拦截器是单例,因此不管用户请求多少次都只有一个拦截器实现,即线程不安全,那我们应该怎么记录时间呢?
解决方案是使用 ThreadLocal,它是线程绑定的变量,提供线程局部变量(一个线程一个 ThreadLocal,A线程的ThreadLocal 只能看到A线程的 ThreadLocal,不能看到B线程的 ThreadLocal)。
代码实现:
public class StopWatchHandlerInterceptor extends HandlerInterceptorAdapter { private NamedThreadLocal<Long> startTimeThreadLocal = new NamedThreadLocal<>("StopWatch-StartTime"); private Logger logger = LoggerFactory.getLogger(StopWatchHandlerInterceptor.class); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { long beginTime = System.currentTimeMillis();//1、开始时间 startTimeThreadLocal.set(beginTime);//线程绑定变量(该数据只有当前请求的线程可见) return true;//继续流程 } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { long endTime = System.currentTimeMillis();//2、结束时间 long beginTime = startTimeThreadLocal.get();//得到线程绑定的局部变量(开始时间) long consumeTime = endTime - beginTime;//3、消耗的时间 if(consumeTime > 500) {//此处认为处理时间超过500毫秒的请求为慢请求 //TODO 记录到日志文件 logger.info(String.format("%s consume %d millis", request.getRequestURI(), consumeTime)); } //测试的时候由于请求时间未超过500,所以启用该代码 // logger.info(String.format("%s consume %d millis", request.getRequestURI(), consumeTime)); } } 复制代码
NamedThreadLocal:Spring提供的一个命名的ThreadLocal实现。
在测试时需要把 stopWatchHandlerInterceptor 放在拦截器链的第一个,这样得到的时间才是比较准确的。
拦截器配置类
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new StopWatchHandlerInterceptor()); registry.addInterceptor(new OldLoginInterceptor()).addPathPatterns("/admin/oldLogin"); } } 复制代码
和上述操作步骤一致,控制台打印结果为:
2019-12-14 21:51:43.881 INFO 4616 --- [nio-8080-exec-3] c.e.i.StopWatchHandlerInterceptor : /index consume 14 millis -------- OldLoginInterceptor.preHandle --- Request URL: http://localhost:8080/admin/oldLogin Sorry! This URL is no longer used, Redirect to /admin/login 2019-12-14 21:51:54.055 INFO 4616 --- [nio-8080-exec-5] c.e.i.StopWatchHandlerInterceptor : /admin/oldLogin consume 1 millis 2019-12-14 21:51:54.070 INFO 4616 --- [nio-8080-exec-6] c.e.i.StopWatchHandlerInterceptor : /admin/login consume 9 millis 复制代码
登录检测
在访问某些资源时(如订单页面),需要用户登录后才能查看,因此需要进行登录检测。
流程:
1、访问需要登录的资源时,由拦截器重定向到登录页面;
2、如果访问的是登录页面,拦截器不应该拦截;
3、用户登录成功后,往 cookie/session 添加登录成功的标识(如用户编号);
4、下次请求时,拦截器通过判断 cookie/session 中是否有该标识来决定继续流程还是到登录页面;
5、在此拦截器还应该允许游客访问的资源。
拦截器代码如下所示:
public class MyInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { boolean flag = true; String ip = request.getRemoteAddr(); long startTime = System.currentTimeMillis(); request.setAttribute("requestStartTime", startTime); if (handler instanceof ResourceHttpRequestHandler) { System.out.println("preHandle这是一个静态资源方法!"); } else if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); System.out.println("用户:" + ip + ",访问目标:" + method.getDeclaringClass().getName() + "." + method.getName()); } //如果用户未登录 User user = (User) request.getSession().getAttribute("user"); if (null == user) { //重定向到登录页面 response.sendRedirect("toLogin"); flag = false; } return flag; } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { if (handler instanceof ResourceHttpRequestHandler) { System.out.println("postHandle这是一个静态资源方法!"); } else if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); long startTime = (long) request.getAttribute("requestStartTime"); long endTime = System.currentTimeMillis(); long executeTime = endTime - startTime; int time = 1000; //打印方法执行时间 if (executeTime > time) { System.out.println("[" + method.getDeclaringClass().getName() + "." + method.getName() + "] 执行耗时 : " + executeTime + "ms"); } else { System.out.println("[" + method.getDeclaringClass().getSimpleName() + "." + method.getName() + "] 执行耗时 : " + executeTime + "ms"); } } } } 复制代码
https://snailclimb.gitee.io/springboot-guide/#/./docs/basis/springboot-interceptor
https://www.cnblogs.com/junzi2099/p/8260137.html#_label3_0