拦截器(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