转载

Spring Boot拦截器(Interceptor)详解

Interceptor 介绍

拦截器(Interceptor)同Filter 过滤器一样,它俩都是面向切面编程——AOP 的具体实现(AOP切面编程只是一种编程思想而已)。

你可以使用 Interceptor 来执行某些任务,例如在 Controller 处理请求之前编写日志,添加或更新配置……

Spring中 ,当请求发送到 Controller 时,在被 Controller 处理之前,它必须经过 Interceptors (0或多个)。

Spring Interceptor是一个非常类似于 Servlet Filter 的概念 。

Interceptor 作用

  1. 日志记录:记录请求信息的日志,以便进行信息监控、信息统计、计算 PV(Page View)等;
  2. 权限检查:如登录检测,进入处理器检测是否登录;
  3. 性能监控:通过拦截器在进入处理器之前记录开始时间,在处理完后记录结束时间,从而得到该请求的处理时间。(反向代理,如 Apache 也可以自动记录)
  4. 通用行为:读取 Cookie 得到用户信息并将用户对象放入请求,从而方便后续流程使用,还有如提取 Locale、Theme 信息等,只要是多个处理器都需要的即可使用拦截器实现。

自定义 Interceptor

如果你需要自定义 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

Spring Boot拦截器(Interceptor)详解

关于该请求在后台的执行过程,用图解的方式进行展示:

Spring Boot拦截器(Interceptor)详解

如果此时点击 /admin/oldLogin (OLD URL) 或者在网址栏输入:http://localhost:8080/admin/oldLogin

Spring Boot拦截器(Interceptor)详解

控制台打印结果:

-------- 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
复制代码

同样我们用图解的形式分析:

Spring Boot拦截器(Interceptor)详解

应用

性能监控

如记录一下请求的处理时间,得到一些慢请求(如处理时间超过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

原文  https://juejin.im/post/5df4f5536fb9a0166243623c
正文到此结束
Loading...