Spring Boot拦截器(Interceptor)详解

Interceptor 介绍Interceptor 做用自定义 Interceptor运行程序并测试效果应用性能监控登陆检测参考资料html

Interceptor 介绍

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

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

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

Spring Interceptor是一个很是相似于Servlet Filter 的概念 。apache

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 个方法: 编程

  1. preHandler(HttpServletRequest request, HttpServletResponse response, Object handler) 方法在请求处理以前被调用。该方法在 Interceptor 类中最早执行,用来进行一些前置初始化操做或是对当前请求作预处理,也能够进行一些判断来决定请求是否要继续进行下去。该方法的返回至是 Boolean 类型,当它返回 false 时,表示请求结束,后续的 Interceptor 和 Controller 都不会再执行;当它返回为 true 时会继续调用下一个 Interceptor 的 preHandle 方法,若是已是最后一个 Interceptor 的时候就会调用当前请求的 Controller 方法。
  2. postHandler(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) 方法在当前请求处理完成以后,也就是 Controller 方法调用以后执行,可是它会在 DispatcherServlet 进行视图返回渲染以前被调用,因此咱们能够在这个方法中对 Controller 处理以后的 ModelAndView 对象进行操做。
  3. afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handle, Exception ex) 方法须要在当前对应的 Interceptor 类的 preHandle 方法返回值为 true 时才会执行。顾名思义,该方法将在整个请求结束以后,也就是在 DispatcherServlet 渲染了对应的视图以后执行。此方法主要用来进行资源清理。

接下来结合实际代码进行学习。安全

LogInterceptor 类:springboot

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 --- ");
    }
}
复制代码

配置拦截器 :markdown

@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>
    &nbsp;&nbsp; | &nbsp;&nbsp;
    <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>
    &nbsp;&nbsp; | &nbsp;&nbsp;
    <a th:href="@{/admin/oldLogin}">/admin/oldLogin (OLD URL)</a>
</div>

<h3>This is Login Page</h3>

<span style="color:blue">Testing OldLoginInterceptor &amp; 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 Time1576329730709

-------- OldLoginInterceptor.preHandle --- 
Request URLhttp://localhost:8080/admin/oldLogin
Sorry! This URL is no longer used, Redirect to /admin/login

-------- LogInterception.afterCompletion --- 
Request URLhttp://localhost:8080/admin/oldLogin
End Time1576329730709
Time Taken: 0

-------- LogInterception.preHandle --- 
Request URLhttp://localhost:8080/admin/login
Start Time1576329730716

-------- AdminInterceptor.preHandle --- 

-------- AdminInterceptor.postHandle --- 

-------- LogInterception.postHandle --- 
Request URLhttp://localhost:8080/admin/login

-------- AdminInterceptor.afterCompletion --- 

-------- LogInterception.afterCompletion --- 
Request URLhttp://localhost:8080/admin/login
End Time1576329730718
Time Taken: 2
复制代码

一样咱们用图解的形式分析:

应用

性能监控

如记录一下请求的处理时间,获得一些慢请求(如处理时间超过500毫秒),从而进行性能改进,通常的反向代理服务器如 apache 都具备这个功能,但此处咱们演示一下使用拦截器怎么实现。

实现分析:

一、在进入处理器以前记录开始时间,即在拦截器的 preHandle 记录开始时间;

二、在结束请求处理以后记录结束时间,即在拦截器的 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();//一、开始时间
        startTimeThreadLocal.set(beginTime);//线程绑定变量(该数据只有当前请求的线程可见)
        return true;//继续流程
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        long endTime = System.currentTimeMillis();//二、结束时间
        long beginTime = startTimeThreadLocal.get();//获得线程绑定的局部变量(开始时间)
        long consumeTime = endTime - beginTime;//三、消耗的时间
        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
复制代码

登陆检测

在访问某些资源时(如订单页面),须要用户登陆后才能查看,所以须要进行登陆检测。

流程:

一、访问须要登陆的资源时,由拦截器重定向到登陆页面;

二、若是访问的是登陆页面,拦截器不该该拦截;

三、用户登陆成功后,往 cookie/session 添加登陆成功的标识(如用户编号);

四、下次请求时,拦截器经过判断 cookie/session 中是否有该标识来决定继续流程仍是到登陆页面;

五、在此拦截器还应该容许游客访问的资源。

拦截器代码以下所示:

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

相关文章
相关标签/搜索