过滤器监听器拦截器AOP

过滤器、监听器、拦截器、AOP的实现





一、过滤器 Filter

​ 在传统的Servlet容器中,可以使用过滤器和监听器,在Java框架中还可以使用拦截器。

​ 过滤器,这里指的是Servlet过滤器,它是在Java Servlet中定义的,能够对Servlet容器中的请求和响应对象进行检查和修改,只起到过滤作用,不会生成Request和Response对象。

​ 过滤器特点:(1)过滤器是基于回调函数实现;(2)过滤器是Servlet规范规定的,只能用于Web程序中;(3)过滤器只在Servlet启动前后起作用,作用范围较窄。



1.1 Java web中实现Filter

Web三大组件:Servlet、Filter、 Listener

什么是过滤器:当浏览器向服务器发送请求的时候,过滤器可以将请求拦截下来,完成一些特殊的功能(我们可以将这种操作理解成方法层面的增强)。

过滤器通常可以做什么操作:编码过滤、权限校验、日志记录等。

1.1.1、 创建一个java类实现Filter接口,重写接口中的方法

@WebFilter("/demo1") // 当前过滤器拦截/demo1的请求
//@WebFilter(value = "/user/demo2",dispatcherTypes = DispatcherType.FORWARD) // 当前过滤器拦截/demo1的请求
public class FilterDemo1 implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    //真正执行过滤业务的方法
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("FilterDemo1 is running...");
        filterChain.doFilter(request,response);//过滤器要放行 只有放行之后,过滤器过滤的资源才会执行
        System.out.println("write log to database...");
    }

    @Override
    public void destroy() {

    }
}

  • 使用过滤器需要注意的事项:
  1. 过滤器必须实现Filter接口。
  2. 过滤器拦截的请求执行完毕之后,必须要放行,否则我们的请求就不会被执行

filterChain.doFilter(request,response); //过滤器放行

  • 可以看到里面只有三个方法,它们的作用分别如下所示:
  1. init(),该方法在Servlet容器启动初始化过滤器时被调用,即它在Filter整个的生命周期中只会被调用一次。此方法必须调用成功,否则Filter无法初始化,那么后续将无法提供服务。
  2. doFilter(),Servlet容器中每一次请求都会调用该方法,FilterChain 用于调用下一个过滤器。
  3. destroy(),当Servlet容器销毁该Filter实例时被调用,通常在该方法中书写销毁或者关闭资源的逻辑。同样在Filter整个的生命周期中只会被调用一次。

1.1.2、我们可以使用@WebFilter来配置过滤器要拦截的资源,当然我们也可以通过xml的方式配置过滤器。

<!--配置过滤器2 -->
<filter>
    <filter-name>demo2</filter-name>
    <filter-class>com.qf.filter.FilterDemo2</filter-class>
</filter>

<filter-mapping>
    <filter-name>demo2</filter-name>
    <url-pattern>/user/demo1</url-pattern>
</filter-mapping>

<!--配置过滤器1 -->
<filter>
    <filter-name>demo1</filter-name>
    <filter-class>com.qf.filter.FilterDemo1</filter-class>
</filter>

<filter-mapping>
    <filter-name>demo1</filter-name>
    <url-pattern>/user/demo1</url-pattern>
</filter-mapping>

​ 在存在多个过滤器时,过滤器链中,在默认的情况下,过滤器会按照名称的自然顺序进行执行。我们在web.xml里面定义过滤器的配置,谁定义在上面谁就先执行。

  • Filter过滤器的生命周期
    • 当加载我们的web应用的时候,首先会初始化过滤器的实例对象,然后执行1次init方法。
    • 当我们发送请求的时候,会将doFilter方法执行。
    • 当我们重新部署项目或者停止web服务器的时候,会执行destroy方法。如果重新部署项目,也会初始化过滤器实例对象,还会执行init方法。




1.2 SpringBoot中实现Filter三种方式

  • 无路径无顺序方式实现过滤器

    无路径无顺序,即默认过滤所有的路径,且多个过滤器之间不存在执行顺序。这种方式最简单,只需自定义类并实现 Filter 接口,并在自定义类上使用 @Component 注解将其交由Spring管理。

  • 有路径无顺序方式实现过滤器

    即指定过滤的路径,但是多个过滤器之间不存在执行顺序。在自定义类上使用 @WebFilter 注解,通过 @WebFilter 注解来设置过滤器的匹配路径,同时还需要在启动类中添加@ServletComponentScan 注解,@ServletComponentScan 注解用于扫描添加 @WebFilter 、@WebServlet 、@WebListener 注解的Bean,并在使用的时候自动注入。

  • 有路径有顺序方式实现过滤器

    即指定过滤的路径,同时多个过滤器之间存在执行顺序。这种方式配置比较复杂,只能通过配置类来实现,且只针对实现过滤器的接口,它不需要通过注解来注入Spring容器,只通过配置类实现。

    //配置类示例:
    @Configuration
    public class IndexFilterConfig {
        @Bean
        public FilterRegistrationBean filterA(){
            FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
            FilterA filterA = new FilterA();
            filterRegistrationBean.setFilter(filterA);
            filterRegistrationBean.addUrlPatterns("*");
            filterRegistrationBean.setName("filterA");
            filterRegistrationBean.setOrder(1);
            return filterRegistrationBean;
        }
    
        @Bean
        public FilterRegistrationBean filterB(){
            FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
            FilterB filterB = new FilterB();
            filterRegistrationBean.setFilter(filterB);
            filterRegistrationBean.addUrlPatterns("*");
            filterRegistrationBean.setName("filterB");
            filterRegistrationBean.setOrder(2);
            return filterRegistrationBean;
        }
    }
    

在spring中是实现Filter同样是需要实现Filter接口并且重写三个方法:

  1. init 方法:在容器中创建当前过滤器的时候自动调用
  2. destory 方法:在容器中销毁当前过滤器的时候自动调用
  3. doFilter 方法:过滤的具体操作
1.2.1、实现Filter接口

1.2.1.1 过滤器B:
ps: 使用 @WebFilter 注解来配置过滤器时,在项目的启动类上需要加上 @ServletComponentScan 注解,从而使过滤器能够注册成功!

package com.example.spring_test_handler.filter;

import lombok.extern.slf4j.Slf4j;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author: AD_Liu
 * @Description: 过滤器实现测试类
 * @ClassName: MyFilterTest
 * @author AD
 */
@Slf4j
//@Component
@WebFilter(filterName = "MyFilterB", urlPatterns = {"/testFilter/*"})
public class MyFilterB implements Filter {

    /**开始时间*/
    private static  long startTime;
    /**结束时间*/
    private static  long endTime;
    /**
    * 声明一个本地线程对象(为了数据的安全,保存到该对象中的数据只能当前线程访问
    * */
    private static  ThreadLocal<Long> threadLocal = new ThreadLocal<>();

    /**
     * 过滤器的英文名称为 Filter, 是 Servlet 技术中最实用的技术。
     * 如同它的名字一样,过滤器是处于客户端和服务器资源文件之间的一道过滤网,帮助我们过滤掉一些不符合要求的请求,通常用作 Session 校验,判断用户权限,如果不符合设定条件,则会被拦截到特殊的地址或者基于特殊的响应。
     *
     * 首先需要实现 Filter接口然后重写它的三个方法
     * init 方法:在容器中创建当前过滤器的时候自动调用
     * destory 方法:在容器中销毁当前过滤器的时候自动调用
     * doFilter 方法:过滤的具体操作
     * */

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        String filterName = filterConfig.getFilterName();
        log.info("【Filter:{}】==========Filter.init():初始化过滤器,在项目启动的时候会初始化对应的Filter过滤器到容器中!",filterName);
        Filter.super.init(filterConfig);
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        startTime = System.currentTimeMillis();
        threadLocal.set(startTime);

        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse responseWrapper = (HttpServletResponse) servletResponse;
        String requestURI = request.getRequestURI();
        log.info("【Filter:MyFilterB】========== 请求地址 requestURI ="+requestURI);
        if (requestURI.contains("/add")
            || requestURI.contains("/delete")
            || requestURI.contains("/update")
        ){
            responseWrapper.sendRedirect("/index.html");
            endTime = System.currentTimeMillis();
            log.info("【Filter:MyFilterB】==========response.sendRedirect:执行doFilter1 处理总耗时: "+ (endTime - threadLocal.get()) );
        }else {
            //Filter过滤器放行该请求
            filterChain.doFilter(servletRequest,servletResponse);
        }
        try {
            //线程睡眠3秒
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        endTime = System.currentTimeMillis();
        log.info("【Filter:MyFilterB】========== 执行doFilter1 处理总耗时: "+ (endTime - threadLocal.get()) );
    }

    @Override
    public void destroy() {
        log.info("【Filter:MyFilterB】========== Filter.destroy():销毁过滤器,当Filter被移除或服务器正常关闭时,会销毁对应的 Filter 过滤器!");
        Filter.super.destroy();
    }
}

1.2.1.2 过滤器A:

package com.example.spring_test_handler.filterConfig;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import javax.servlet.*;
import java.io.IOException;

/**
 * @author: AD_Liu
 * @Description: 测试拦截器2
 * @ClassName: TestFilter2
 */
@Slf4j
//@Component
public class MyFilterA implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        String filterName = filterConfig.getFilterName();
        log.info("【Filter:{}】==========执行初始化!", filterName);
        Filter.super.init(filterConfig);
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        long startTime = System.currentTimeMillis();
        filterChain.doFilter(servletRequest,servletResponse);
        long endTime = System.currentTimeMillis();
        log.info("【Filter: MyFilterA】========== 执行耗时: {} ms", endTime - startTime);
    }

    @Override
    public void destroy() {
        log.info("【Filter: MyFilterA】========== 执行销毁!");
        Filter.super.destroy();
    }
}
  • 配置类中注册过滤器Bean

​ 通过实现 WebMvcConfigurer接口,使用 @Bean 注解实现Filter过滤器的注册, 这一步可有可无。因为我们在spring中可以通过 @Component注解实现了Bean对象的注入。

配置类如下:

package com.example.spring_test_handler.config;

import com.example.spring_test_handler.filterConfig.TestFilter2;
import com.example.spring_test_handler.interceptorConfig.MyHandlerInterceptorTest;
import com.example.spring_test_handler.listenerConfig.MyListenerTest1_sessionListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.servlet.Filter;
import javax.servlet.FilterRegistration;

/**
 * @author: AD_Liu
 * @Description: 初始化监听器Listener 配置类
 * @ClassName: WebListenerConfig
 */

@Configuration
public class WebAllHandlerConfig implements WebMvcConfigurer {

/** ================注入过滤器Filter================ */

    /**
    *@Description:注册过滤器,也可通过@Component注解+@Autowired注解注入,也可以通过 new 在通过@Bean进行注入到系统中去
    *@Param []
    *@return org.springframework.boot.web.servlet.FilterRegistrationBean
    */
    @Bean
    public FilterRegistrationBean filterRegistrationBean(){
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        filterRegistrationBean.setFilter(new MyFilterA());
        filterRegistrationBean.addUrlPatterns("/testFilter/*");
        filterRegistrationBean.setName("MyFilterA");
        filterRegistrationBean.setOrder(1);
        return filterRegistrationBean;
    }

}





二、监听器 Listener

​ 监听器,它也是Servlet层面的内容,可用于监听Web应用中某些对象或者信息的创建、修改和销毁等动作发生,并作出相应的响应处理。

  • 监听器大概分为以下几种:

1、ServletContextListener:用来监听 ServletContext (Web应用)对象的生命周期,接口主要有两个方法,一个在当Servlet容器启动web应用时调用(contextInitialized),另一个是在Servlet容器终止web应用时调用(contextDestroyed)。

2、HttpSessionListener:用来监听 Web 应用中的 Session (Session会话)对象生命周期。接口主要有两个方法,一个在Session会话创建时调用(sessionCreated),另一个在Session会话销毁时调用(sessionDestroyed)

3、ServletRequestListener:用来监听 ServletRequest 对象的生命周期。主要有两个方法,一个在ServletRequest对象初始化时调用(requestInitialized),另一个在ServletRequest对象销毁时调用(requestDestroyed)

  • 监听器的使用

我们通过 HttpSessionListener来统计当前在线人数、ip等信息,为了避免并发问题我们使用原子int来计数。
ServletContext,是一个全局的储存信息的空间,它的生命周期与Servlet容器也就是服务器保持一致,服务器关闭才销毁。
request,一个用户可有多个;
session,一个用户一个;
而servletContext,所有用户共用一个。
所以,为了节省空间,提高效率,ServletContext中,要放必须的、重要的、所有用户需要共享的线程又是安全的一些信息。因此我们这里用ServletContext来存储在线人数sessionCount最为合适。





2.1 JavaWeb中实现Listener

​ 简单演示介绍一个监听器:ServletContextListener。这个监听器负责监听ServletContext容器的创建和销毁。

2.1.1、创建 ServletListener监听器类,通过实现 ServletContenxListener接口实现接口的两个抽象方法(创建、销毁)。

@WebListener
public class MyListener implements ServletContextListener {

    //监听Servlet上下文对象创建的方法
    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) {
        System.out.println("contextInitialized is running");
    }

    //监听Servlet上下文对象销毁的方法
    @Override
    public void contextDestroyed(ServletContextEvent servletContextEvent) {
        System.out.println("contextDestroyed is running");
    }
}

2.1.2、配置监听器,可以通过@WebListener来进行配置。也可以通过XML配置文件来配置

<!--配置监听器-->
<listener>
    <listener-class>com.qf.listener.MyListener</listener-class>
</listener>




2.2 Spring中实现Listener

2.2.1、创建 ServletRequestListener 监听器
package com.example.spring_test_handler.listener;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebFilter;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpServletRequest;

/**
 * @author: AD_Liu
 * @Description: ServletRequestListener Rquest监听器测试
 * @ClassName: MyServletRequestListener
 */
@Slf4j
//@Component
@WebListener()
public class MyServletRequestListener implements ServletRequestListener {

    /**
    *@Description:ServletRequestListener监听器创建时执行
    *@Param [sre]
    *@return void
    */
    @Override
    public void requestInitialized(ServletRequestEvent sre) {
        HttpServletRequest request = (HttpServletRequest) sre.getServletRequest();
        String requestURI = request.getRequestURI();
        log.info("【ServletRequestListener】========== Request监听器Listener被创建:被调用路径{}",requestURI);
    }

    /**
    *@Description:ServletRequestListener监听器销毁时执行
    *@Param [sre]
    *@return void
    */
    @Override
    public void requestDestroyed(ServletRequestEvent sre) {
        log.info("【ServletRequestListener】========== Request监听器Listener被销毁:MyServletRequestListener_test2 requestDestroyedP");
    }
}

2.2.2、创建监 HttpSessionListener 监听器的实现类
package com.example.spring_test_handler.listener;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import java.util.concurrent.atomic.AtomicInteger;


/**
 * 监听器通常用于监听 Web 应用程序中对象的创建、销毁等动作的发送,同时对监听的情况作出相应的处理,最常用于统计网站的在线人数、访问量等。
 * 监听器大概分为以下几种:
 *      1.ServletContextListener:用来监听 ServletContext 属性的操作,比如新增、修改、删除等。
 *      2.HttpSessionListener:用来监听 Web 应用中的 Session 对象,通常用于统计在线情况。
 *      3.ServletRequestListener:用来监听 Request 对象的属性操作。
 *
 * 监听器的使用:
 *      我们通过 HttpSessionListener来统计当前在线人数、ip等信息,为了避免并发问题我们使用原子int来计数。
 * ServletContext,是一个全局的储存信息的空间,它的生命周期与Servlet容器也就是服务器保持一致,服务器关闭才销毁。
 * request,一个用户可有多个;
 * session,一个用户一个;
 * 而servletContext,所有用户共用一个。
 * 所以,为了节省空间,提高效率,ServletContext中,要放必须的、重要的、所有用户需要共享的线程又是安全的一些信息。因此我们这里用ServletContext来存储在线人数sessionCount最为合适。
 * */

/**
 * @author: AD_Liu
 * @Description: 监听器Listener的实现及功能___该测试类通过‘一个session一个用户的原则’利HttpSessionListener监听器的触发机制来实现统计在线人数!
 * @ClassName: MySessionListener
 */
@Slf4j
@Component
public class MySessionListener implements HttpSessionListener /*ServletContextListener*/{
    public  static AtomicInteger userCount = new AtomicInteger(0);
    @Override
    public synchronized void sessionCreated(HttpSessionEvent se) {
        //以原子方式将当前值递增 1
        userCount.getAndIncrement();
        se.getSession().getServletContext().setAttribute("sessionCount",userCount.get());
        log.info("【HttpSessionListener】========== 在线人数人数增加为:{}",userCount.get());
        //此处可以在ServletContext域对象中为访问量计数,然后传入过滤器的销毁方法
        //在销毁方法中调用数据库入库,因为过滤器生命周期与容器一致
    }


    @Override
    public void sessionDestroyed(HttpSessionEvent se) {
        //原子递减当前值 1
        userCount.getAndDecrement();
        se.getSession().getServletContext().setAttribute("sessionCount",userCount.get());
        log.info("【HttpSessionListener】========== 在线人数人数减少为:{}",userCount.get());
    }
}

  • 初始化监听器
    ​ 同样在初始化监听器的方式上,我们可以在监听器实现类上通过@Component注解来实现监听器Bean 的注入,也可以通过 WebMvcConfigurer配置类 来实现监听器Bean的注入。
package com.example.spring_test_handler.config;

import com.example.spring_test_handler.filterConfig.TestFilter2;
import com.example.spring_test_handler.interceptorConfig.MyHandlerInterceptorTest;
import com.example.spring_test_handler.listenerConfig.MyListenerTest1_sessionListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.servlet.Filter;
import javax.servlet.FilterRegistration;

/**
 * @author: AD_Liu
 * @Description: 初始化监听器Listener 配置类
 * @ClassName: WebListenerConfig
 */

@Configuration
public class WebAllHandlerConfig implements WebMvcConfigurer {

    /** ================注入监听器 Listener================ */
    /**
    *@Description:注册监听器————也可通过@Component注解+@Autowired注解注入,也可以通过 new 在通过@Bean进行注入到系统中去
    *@Param []
    *@return org.springframework.boot.web.servlet.ServletListenerRegistrationBean
    */
    @Bean
    public ServletListenerRegistrationBean ListenerRegistrationBean(){
        ServletListenerRegistrationBean servletListener = new ServletListenerRegistrationBean();
        servletListener.setListener(new MySessionListener());
        servletListener.setOrder(1);
        return servletListener;
    }

}
2.2.3、监听器测试类
package com.example.spring_test_handler.listener.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

/**
 * @ClassName : ListenerTestController
 * @Description : 监听器测试控制层Controller
 * @Author : ich liebe Dich
 */

@RestController
@RequestMapping("/testListener")
public class ListenerTestController {

    @GetMapping("/listener")
    public String listenerTest(HttpServletRequest request){
        //创建Session,触发 HttpSessionListener
        HttpSession session = request.getSession();
        System.out.println("Listener test Controller Execution");
        return "listener test success";
    }
}




三、拦截器 Interceptor

​ 主要用于拦截用户请求并做相应的处理。拦截器和过滤器、监听器不同,它不依赖于Servlet容器,依赖于Spring框架,是AOP的一种体现,底层基于Java的动态代理实现。

​ 过滤器可以拦截请求,对请求进行相关处理,但是过滤器对请求的处理很有限,比如我们如果要在过滤器中进行数据验证,只能验证数据是否为空(不能获取到目标方法的各种信息),如果要实现数据的合法性验证就无法实现了。

​ 拦截器是基本上所有项目都会使用到技术,所谓的拦截器就是用户和控制器之间的一个屏障,这个屏障可以对用户的请求进行拦截,拦截之后可以进行数据的验证,保证到了控制器的数据都是合法的、安全的,当然还有其他很多应用场景,比如性能检测(可以统计出一个请求耗费的时间)等。

在这里插入图片描述

  • 拦截器的使用

我们需要实现 HandlerInterceptor 类,并且重写三个方法:

1、boolean preHandle (HttpServletRequest request,HttpServletResponse response,Object handler):在 Controoler 处理请求之前被调用,返回值是 boolean类型,如果是true就进行下一步操作;若返回false,则证明不符合拦截条件,在失败的时候不会包含任何响应,此时需要调用对应的response返回对应响应。

2、public void postHandle(HttpServletRequest reques, HttpServletResponse response, Object handler,ModelAndView modleAndView)throws Exceptio:在 Controoler
处理请求执行完成后、生成视图前执行,可以通过ModelAndView对视图进行处理,当然ModelAndView也可以设置为 null。

3、public void afterCompletion(HttpServletRequest request , HttpServletResponse response,Object handler ,Exception ex):在 DispatcherServlet 完全处理请求后被调用,通常用于记录消耗时间,也可以对一些资源进行处理。





3.1 拦截器实现

3.1.1、创建拦截器实现类
package com.example.spring_test_handler.interceptorConfig;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * Java中的拦截器是动态拦截 action 调用的对象,然后提供了可以在 action 执行前后增加一些操作,也可以在 action 执行前停止操作,功能与过滤器类似,但是标准和实现方式不同。
 *  1.登录认证:在一些应用中,可能会通过拦截器来验证用户的登录状态,如果没有登录或者登录失败,就会给用户一个友好的提示或者返回登录页面,当然大型项目中都不采用这种方式,都是调单点登录系统接口来验证用户。
 *  2.记录系统日志:我们在常见应用中,通常要记录用户的请求信息,比如请求ip,方法执行时间等,通过这些记录可以监控系统的状况,以便于对系统进行信息监控、信息统计、计算 PV、性能调优等。
 *  3.通用处理:在应用程序中可能存在所有方法都要返回的信息,这是可以利用拦截器来实现,省去每个方法冗余重复的代码实现。
 *
 *拦截器的使用:我们需要实现 HandlerInterceptor 类,并且重写三个方法:
 *  1.preHandle:在 Controoler 处理请求之前被调用,返回值是 boolean类型,如果是true就进行下一步操作;若返回false,则证明不符合拦截条件,在失败的时候不会包含任何响应,此时需要调用对应的response返回对应响应。
 *  2.postHandler:在 Controoler 处理请求执行完成后、生成视图前执行,可以通过ModelAndView对视图进行处理,当然ModelAndView也可以设置为 null。
 *  3.afterCompletion:在 DispatcherServlet 完全处理请求后被调用,通常用于记录消耗时间,也可以对一些资源进行处理。
 * */

/**
 * @author: AD_Liu
 * @Description: 拦截器测试
 * @ClassName: MyHandlerInterceptorTest
 */
@Slf4j
@Component
public class MyHandlerInterceptorTest implements HandlerInterceptor {

    private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
    private static Long startTime;
    private static Long endTime;

    /**
    *@Description:在Controller处理请求之前执行
    *@Param [request, response, handler]
    *@return boolean
    */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        startTime = System.currentTimeMillis();
        threadLocal.set(startTime);
        //获取目标方法对象及名称
        String methodName =null;
        if (handler != null && handler instanceof HandlerMethod){
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            methodName = handlerMethod.getMethod().getName();
        }
        log.info("【HandlerInterceptor:preHandle】==》 调用了路径:{} \t 目标方法:{}",request.getRequestURI(),methodName);
        return true;
    }

    /**
    *@Description:Controller执行完目标方法,响应数据之前
    *@Param [request, response, handler, modelAndView]
    *@return void
    */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        //获取目标方法中的 ModelAndView 试图对象
        if (modelAndView != null){
            modelAndView.addObject("msg","HandlerInterceptor拦截器追加数据!");
        }
        String methodName =null;
        if (handler != null && handler instanceof HandlerMethod){
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            methodName = handlerMethod.getMethod().getName();
        }
        log.info("【HandlerInterceptor:postHandle】==》 Controller执行完毕对应方法:{},即将响应视图层!",methodName);
    }

    /**
    *@Description:渲染(响应)结束之后触发(请求完全执行完毕)
    *@Param [request, response, handler, ex]
    *@return void
    */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        endTime = System.currentTimeMillis();
        log.info("【HandlerInterceptor:afterCompletion】==> 请求执行完毕!总耗时:{}ms",endTime-threadLocal.get());
    }
}
3.1.2、拦截器初始化配置
package com.example.spring_test_handler.config;

import com.example.spring_test_handler.filterConfig.TestFilter2;
import com.example.spring_test_handler.interceptorConfig.MyHandlerInterceptorTest;
import com.example.spring_test_handler.listenerConfig.MyListenerTest1_sessionListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import javax.servlet.Filter;
import javax.servlet.FilterRegistration;

/**
 * @author: AD_Liu
 * @Description: 初始化监听器Listener 配置类
 * @ClassName: WebListenerConfig
 */

@Configuration
public class WebAllHandlerConfig implements WebMvcConfigurer {


    /** 注入拦截器HandlerInterceptor */
    @Autowired
    private MyHandlerInterceptorTest handlerInterceptorTest;

     /** ================注入拦截器HandlerInterceptor================ */
    @Autowired
    private MyHandlerInterceptorTest handlerInterceptorTest;
    /**
    *@Description:配置拦截器,拦截器属于springframework,需要通过配置类进行添加!!!!
     *  1.通过@Component注解无效,还需要通过InterceptorRegistry注册: addInterceptor(xx)添加拦截器Interceptor
     *  2.通过addPathPatterns(xxx)添加拦截路径
    *@Param [registry]
    *@return void
    */
    @Override //重写该方法,通过 InterceptorRegistry 拦截器注册实例进行 addInterceptor(xx)添加拦截器Interceptor
    public void addInterceptors(InterceptorRegistry registry) {
        //配置拦截路径
        registry.addInterceptor(/*new MyHandlerInterceptorTest()*/ handlerInterceptorTest).addPathPatterns("/testInterceptor/*","/interceptor/*");
    }
}

3.2 HandlerMethod类型介绍

preHandle 方法中的handler参数:HandlerMethod 类型介绍

1、public Object getBean():取得目标控制器对象

2、public Class<?> getBeanType() :取得目标控制器的Class类类型

3、public Method getMethod():取得目标控制器中要访问的方法,就是目方法

​ 3.1、 method.getAnnotation(xxxx.class) :获取注解信息

4、public MethodParameter[] getMethodParamenters():取得目标方法的参数列表信息,返回值是“MethodParameter”类型的数组,在该类型中要关注的方法如下:

​ 4.1、public String getParameterName():取得的是参数的名称

​ 4.2、public Class<?> getParameterType() :取得参数的类型的Class类对象





3.3 sign、token、ts认证

​ 服务端与前端对接的API接口,如果被第三方抓包并进行恶意篡改参数,可能会导致数据泄露和篡改数据,下面主要围绕token,签名,时间戳,三个部分来保证API接口的安全性。

3.3.1 签名Sign

​ 为了防止API接口中的数据被篡改,很多时候我们需要对API接口做签名。

​ 接口请求方将请求参数+ 时间戳+ 密钥拼接成一个字符串,然后通过md5等hash算法,生成一个前面sign。然后在请求参数或者请求头中,增加sign参数,传递给API接口。

​ API接口的网关服务,获取到该sign值,然后用相同的请求参数 + 时间戳 + 密钥拼接成一个字符串,用相同的m5算法生成另外一个sign,对比两个sign值是否相等。

​ 如果两个sign相等,则认为是有效请求,API接口的网关服务会将给请求转发给相应的业务系统。如果两个sign不相等,则API接口的网关服务会直接返回签名错误。

​ 为了安全性考虑,防止同一次请求被反复利用,增加了密钥没破解的可能性,我们必须要对每次请求都设置一个合理的过期时间,比如:15分钟。这样一次请求,在15分钟之内是有效的,超过15分钟,API接口的网关服务会返回超过有效期的异常提示。

3.3.2 请求身份合法性Token认证参数

​ 用户登录成功后,会获取一个ticket值,接下去任何接口的访问都需要这个参数。我们把它放置在redis内,有效期为10分钟,在ticket即将超时,无感知续命。延长使用时间,如果用户在一段时间内没进行任何操作,就需要重新登录系统。

​ 请求携带参数TokenSign,只有拥有合法的身份Token和正确的签名Sign才能放行。这样就解决了身份验证和参数篡改问题,即使请求参数被劫持,由于获取不到SecretKey仅作本地加密使用,不参与网络传输),无法伪造合法的请求。

3.3.3 timestamp(ts)参数

​ 引入一个时间戳参数,保证接口仅在一分钟内有效,需要和客户端时间保持一致。

​ 在请求中增加 timestamp 参数要来表示请求时间戳,服务方端接收该请求后,根据当前时间生成一个接收时间戳,然后根据两个时间戳的差值进行请求判定,如果差值大于指定的阈值,则认为请求无效,否则请求通过。关于阈值的选定,可以根据接口的响应速度进行适当的调整,一般默认为 60 秒。

​ 缺点:通过上面的判定逻辑可以发现,在小于阈值的时间段内是可以进行重复请求的,该方案不能保证请求仅一次有效。

3.3.4 接口安全拦截器的实现
package com.example.spring_test_handler.interceptorConfig;
import com.alibaba.fastjson.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.StringUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.concurrent.TimeUnit;

/**
 * @author: AD_Liu
 * @Description: 每次请求都带有这三个参数,我们都需要进行验证,只有在三个参数都满足我们的要求,才允许数据返回或被操作。
 * 1.请求身份合法性:Token认证参数
 * 2.防止篡改:Sign / Signature 签名参数
 * 3.重放攻击:ts / Timestamp参数时间戳参数
 *
 * @ClassName: LoginInterceptor
 */

//@Component
public class LoginInterceptor implements HandlerInterceptor {

    //注入Redis,实现缓存操作
    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 封装签名 Signature 时用到的密钥key
     * */
    private static final String secretKeyOfWxh = "dffkdgdfgdsss";

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        // 如果不是映射到方法直接通过
        if (handler instanceof HandlerMethod){
            return true;
        }
        //方法注解级拦截器
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        //判断接口是否需要验证,验证接口为标注了指定类型的自定义注解的
        Annotation annotation = method.getAnnotation(Annotation.class);
        if (annotation ==null){
            System.out.println("LoginInterceptor 拦截器执行了==> 无需鉴权直接通过");
            //还可以做细致化操作:通过获取注解中的参数数据进行判断该接口是否需要进行鉴权处理!
            return true;
        }

        JSONObject jsonObject = new JSONObject();
        /*
         * 1.请求身份合法性:Token认证参数:在用户登录创建一个票根传给前端,后端将该票根存入 redis 中,下次需要验证的时候通过redis获取验证
         *  String ticket = UUID.randomUUID().toString();
         *  icket = ticket.replace("-","");
         *  redisTemplate.opsForValue().set(ticket,personEntity.getLoginName(),10L, TimeUnit.MINUTES);
         * */
        String ticket = request.getParameter("token/ticket");
        String sign = request.getParameter("sign/signature");
        String ts = request.getParameter("ts");
        if (StringUtils.isEmpty(ticket) || StringUtils.isEmpty(sign) || StringUtils.isEmpty(ts)){
            jsonObject.put("message","args is isEmpty");
            response.getWriter().write(jsonObject.toJSONString());
            return false;
        }

        //如果redis存在ticket就认为是合法的请求
        if (redisTemplate.hasKey(ticket)){
            String values = (String) redisTemplate.opsForValue().get(ticket);
            //判断ticket是否即将过期,进行续命操作
            if (redisTemplate.opsForValue().getOperations().getExpire(ticket) != -2 && redisTemplate.opsForValue().getOperations().getExpire(ticket) < 20){
                redisTemplate.opsForValue().set(ticket,values,10L, TimeUnit.MINUTES);
            }
            //判断是否重复访问,存在重放攻击的时间窗口期
            if (getTimestamp() - Long.valueOf(ts) > 600){
                jsonObject.put("message","Overtime to connect to server");
                PrintWriter printWriter = response.getWriter();
                printWriter.write(jsonObject.toJSONString());
                return false;
            }
            //验证签名
            if (checkSign(request,sign)){
                jsonObject.put("message","sign is invalid");
                PrintWriter printWriter = response.getWriter();
                printWriter.write(jsonObject.toJSONString());
                return false;
            }
            return true;
        }else {
            jsonObject.put("message","ticket is invalid,Relogin.");
            PrintWriter printWriter = response.getWriter();
            printWriter.write(jsonObject.toJSONString());
        }
        return false;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }

    /**
     *@Description:引入一个时间戳参数,保证接口仅在一分钟内有效,需要和客户端时间保持一致。
     *@Param []
     *@return long
     */
    public static long getTimestamp()
    {
        long timestampLong = System.currentTimeMillis();
        long timestampsStr = timestampLong / 1000;
        return timestampsStr;
    }

    /**
     *@Description:验证权限的方法,在HandlerInterceptor拦截器中的proHandle()方法中进行鉴权、验签操作!
     *@Param [request, response]
     *@return java.lang.Boolean
     */
    private Boolean checkSign(HttpServletRequest request,String sign){
        System.out.println("HandlerInterceptorTest2:variableOauth ==> 进行验签操作");
        Boolean flag = false;

        Enumeration<String> parameterNames = request.getParameterNames();
        HashMap<String, String> parameterMap = new HashMap<>();
        String old_Signature = null;

        while (parameterNames.hasMoreElements()) {
            String pName  = parameterNames.nextElement();
            if ("signature".equals(pName)){
                old_Signature = request.getParameter(pName);
                continue;
            }
            String pValue = request.getParameter(pName);
            parameterMap.put(pName,pValue);
        }
        if (old_Signature != null && old_Signature.equals( getSignature(parameterMap ,secretKeyOfWxh))){
            flag = true;
        }
        return flag;
    }


    /**
     *@Description:TODO---通过该方法,生成相关参数的[signature]相关数据信息---设计到加密解密技术相关操作!!
     *@Param [parameterMap, secretKeyOfWxh]
     *@return java.lang.String
     */
    private String getSignature(HashMap<String, String> parameterMap,String secretKeyOfWxh){
        return "根据参数、密钥等信息生成签名信息数据";
    }
}




四、切面编程AOP

​ AOP(Aspect Oriented Programming)称为面向切面编程,AOP是一种编程思想,并不是某种具体实现。一般在提到AOP实现时,会有过滤器和代理模式这两种,过滤器基于函数回调实现,而代理模式基于java反射机制。代理模式分为静态代理和动态代理,动态代理就是拦截器的简单实现。

​ 为了解决重复的代码,提出了一种开发思想:面向切面的编程,叫做AOP编程(AspectOrientedPropramming),就是将那些辅助的操作(打开连接、关闭连接、权限验证…)放到一个专门的类中实现,这个类可以横切需要操作数据的类的方法。简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。

​ 使用"横切"技术,AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事物。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。

在这里插入图片描述

4.1 相关技术术语

  • AOP的相关概念

    • 横切关注点:对哪些方法进行拦截,拦截后怎么处理,这些关注点称之为横切关注点
    • Aspect(切面):通常是一个类,里面可以定义切入点和通知
    • JointPoint(连接点):程序执行过程中明确的点,一般是方法的调用。被拦截到的点,因为Spring只支持方法类型的连接点,所以在Spring中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器
    • Advice(通知):AOP在特定的切入点上执行的增强处理,有before(前置),after(后置),afterReturning(最终),afterThrowing(异常),around(环绕)
    • Pointcut(切入点):就是带有通知的连接点,在程序中主要体现为书写切入点表达式
    • weave(织入):将切面应用到目标对象并导致代理对象创建的过程
    • introduction(引入):在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段
    • AOP代理(AOP Proxy):AOP框架创建的对象,代理就是目标对象的加强。Spring中的AOP代理可以使JDK动态代理,也可以是CGLIB代理,前者基于接口,后者基于子类
    • 目标对象(Target Object): 包含连接点的对象。也被称作被通知或被代理对象。POJO

  • Advice通知类型介绍

    • @Aspect,使用在类上,用于将普通Java类定义为切面类;
    • @Pointcut,用于定义一个切入点,可以是一个规则表达式,也可以是注解;
    • @Before,用于在切入点开始处切入内容;
    • @After,用于在切入点结尾处切入内容;
    • @AfterReturning,用于在切入点返回内容之后处理逻辑;
    • @Around,用于在切入点前后切入内容,并控制何时执行切入点的内容。可以理解是 @Before 和 @After注解的组合使用;
    • @AfterThrowing,用于当切入内容部分抛出异常之后的处理逻辑。(
    • @Order AOP切面的执行顺序。注意 @Before 数值越小越先执行,@After 和@AfterReturning 则是数值越大越先执行。
  • AOP使用场景

    Authentication 权限
    Caching 缓存
    Context passing 内容传递
    Error handling 错误处理
    Lazy loading 懒加载
    Debugging  调试
    logging, tracing, profiling and monitoring 记录跟踪 优化 校准
    Performance optimization 性能优化
    Persistence  持久化
    Resource pooling 资源池
    Synchronization 同步
    Transactions 事务

Spring中实现AOP操作

​ spring的aop本质就是动态代理设计,spring默认使用jdk动态代理,可以手工配置cglib的动态代理。





4.2 AOP实现类测试

package com.example.spring_test_handler.springAOPConfig;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

/**
 * @author: AD_Liu
 * @Description:
 * @ClassName: AOPTest1
 * AOP(Aspect Oriented
 *
 * Programming)称为面向切面编程,在程序开发中主要用来解决一些系统层面上的问题,比如日志,事务,权限等待,Struts2的拦截器设计就是基于AOP的思想,是个比较经典的例子。
 *
 * (1)横切关注点:对哪些方法进行拦截,拦截后怎么处理,这些关注点称之为横切关注点
 * (2)Aspect(切面):通常是一个类,里面可以定义切入点和通知
 * (3)JointPoint(连接点):程序执行过程中明确的点,一般是方法的调用。被拦截到的点,因为Spring只支持方法类型的连接点,所以在Spring中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器
 * (4)Advice(通知):AOP在特定的切入点上执行的增强处理,有before(前置),after(后置),afterReturning(最终),afterThrowing(异常),around(环绕)
 * (5)Pointcut(切入点):就是带有通知的连接点,在程序中主要体现为书写切入点表达式
 * (6)weave(织入):将切面应用到目标对象并导致代理对象创建的过程
 * (7)introduction(引入):在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段
 * (8)AOP代理(AOP Proxy):AOP框架创建的对象,代理就是目标对象的加强。Spring中的AOP代理可以使JDK动态代理,也可以是CGLIB代理,前者基于接口,后者基于子类
 * (9)目标对象(Target Object): 包含连接点的对象。也被称作被通知或被代理对象。POJO
 *
 * (1)Before:在目标方法被调用之前做增强处理,@Before只需要指定切入点表达式即可
 * (2)AfterReturning:在目标方法正常完成后做增强,@AfterReturning除了指定切入点表达式后,还可以指定一个返回值形参名returning,代表目标方法的返回值
 * (3)AfterThrowing:主要用来处理程序中未处理的异常,@AfterThrowing除了指定切入点表达式后,还可以指定一个throwing的返回值形参名,可以通过该形参名来访问目标方法中所抛出的异常对象
 * (4)After:在目标方法完成之后做增强,无论目标方法时候成功完成。@After可以指定一个切入点表达式
 * (5)Around:环绕通知,在目标方法完成前后做增强处理,环绕通知是最重要的通知类型,像事务,日志等都是环绕通知,注意编程中核心是一个ProceedingJoinPoint
 */
@Aspect
@Slf4j
@Component
public class AopTest1 {

    @Pointcut("execution(* com.example.spring_test_handler.*.*(..))")
//    @Pointcut("execution(* com.cj.controller..*.*(..))")   //切所有controller
    public void pointcut() {
    }

    @Before("pointcut()")
    public void beforeControllerMethod(JoinPoint joinPoint) {
        log.info("前置通知!!!!方法执行前执行");
    }

    @Around("pointcut()")
    public Object handleControllerMethod(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        String uuIdTrace = UUID.randomUUID().toString().replace("-", "");
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        //IP地址
        String ipAddr = getRemoteHost(request);
//        String url = request.getRequestURL().toString();
        String requestMethod = request.getMethod();
        String requestURI = request.getRequestURI();
        String reqParam = preHandle(proceedingJoinPoint, request);//获取请求参数
        log.info(uuIdTrace + " 环绕通知,执行方法前,请求源IP:【{}】,请求方法:【{}】,请求URL:【{}】,请求参数:【{}】", ipAddr, requestMethod, requestURI, reqParam);
        long start = System.currentTimeMillis();

        Object result = proceedingJoinPoint.proceed();//执行方法,获取响应信息

        String respParam = postHandle(result);
        log.info(uuIdTrace + " 环绕通知,执行方法后,请求源IP:【{}】,请求URL:【{}】,返回参数:【{}】,执行耗时 : {}", ipAddr, requestURI, respParam, (System.currentTimeMillis() - start) + "ms");
        return result;
    }

    @After("pointcut()")
    public void afterControllerMethod(JoinPoint joinPoint) {
        log.info("后置通知:方法执行完后执行");
    }

    @AfterReturning(returning = "result", pointcut = "pointcut()")
    public void doAfterReturnint(Object result) {
        log.info("后置通知,方法执行完后执行,响应信息为:{}", JSONObject.toJSONString(result));
    }

    /**
     * 入参数据
     *
     * @param joinPoint
     * @param request
     * @return
     */
    private String preHandle(ProceedingJoinPoint joinPoint, HttpServletRequest request) {
        try {
            Map<String, Object> fieldsName = getFieldsName(joinPoint);
            return JSON.toJSONString(fieldsName);
        } catch (Exception e) {
            log.warn("切面获取请求参数出现异常", e);
            return null;
        }
    }

    private Map<String, Object> getFieldsName(JoinPoint joinPoint) throws Exception {
        String classType = joinPoint.getTarget().getClass().getName();
        String methodName = joinPoint.getSignature().getName();
        // 参数值
        Object[] args = joinPoint.getArgs();
        Class<?>[] classes = new Class[args.length];
        for (int k = 0; k < args.length; k++) {
            // 对于接受参数中含有MultipartFile,ServletRequest,ServletResponse类型的特殊处理,我这里是直接返回了null。(如果不对这三种类型判断,会报异常)
            if (args[k] instanceof MultipartFile || args[k] instanceof ServletRequest || args[k] instanceof ServletResponse) {
                return null;
            }
            if (!args[k].getClass().isPrimitive()) {
                // 当方法参数是基础类型,但是获取到的是封装类型的就需要转化成基础类型
//                String result = args[k].getClass().getName();
//                Class s = map.get(result);

                // 当方法参数是封装类型
                Class s = args[k].getClass();

                classes[k] = s == null ? args[k].getClass() : s;
            }
        }
        ParameterNameDiscoverer pnd = new DefaultParameterNameDiscoverer();
        // 获取指定的方法,第二个参数可以不传,但是为了防止有重载的现象,还是需要传入参数的类型
        Method method = Class.forName(classType).getMethod(methodName, classes);
        // 参数名
        String[] parameterNames = pnd.getParameterNames(method);
        // 通过map封装参数和参数值
        HashMap<String, Object> paramMap = new HashMap();
        for (int i = 0; i < parameterNames.length; i++) {
            paramMap.put(parameterNames[i], args[i]);
        }
        return paramMap;
    }

    /**
     * 返回数据
     *
     * @param retVal
     * @return
     */
    private String postHandle(Object retVal) {
        if (null == retVal) {
            return "";
        }
        return JSON.toJSONString(retVal);
    }

    /**
     * 获取目标主机的ip
     *
     * @param request
     * @return
     */
    private String getRemoteHost(HttpServletRequest request) {
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
    }

}





4.3 AOP实现操作日志记录

​ 通过自定义方法注解,实现接口操作日志的记录。在调用带有自定义注解的接口时,会触发Aop实现类接口,后续进调用接口的相关信息进行记录持久化到数据库。

4.3.1、自定义注解类
package cn.zhidasifang.common.annotation;

import cn.zhidasifang.common.enums.BusinessType;
import lombok.extern.java.Log;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author: AD_Liu
 * @Description: 系统日志生成注解
 * @ClassName: SysLogAnnotation
 */

@Target(ElementType.METHOD) //注解范围在方法上
@Retention(RetentionPolicy.RUNTIME)  //注解运行时生效
public @interface SysLogAnnotation {
    String desc() default "接口操作描述!";

    BusinessType type() default BusinessType.OTHER;
}

4.2.2、切面类的封装
package cn.zhidasifang.common.utils;

import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil;
import cn.zhidasifang.common.entity.LogInfo;
import cn.zhidasifang.common.annotation.SysLogAnnotation;
import cn.zhidasifang.common.enums.BusinessType;
import cn.zhidasifang.common.service.ISysLogService;
import lombok.extern.flogger.Flogger;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * @author: AD_Liu
 * @Description: 系统日志Aspect切面类
 * @ClassName: SysLogAspect
 */
@Aspect  //切面类
@Component  //注入bean到Spring-ioc容器中
@Slf4j
public class SysLogAspect {

    /**
     * 注入日志操作Service层
     */
    @Autowired
    ISysLogService sysLogService;
    /**
     * 注入HttpServletRequest,request对象已经内置到了Spring的ioc中
     */
    @Autowired
    HttpServletRequest httpServletRequest;

    /**
     * 本地线程
     */
    private final ThreadLocal<Date> THREAD_LOCAL = new ThreadLocal<>();

    /**
     * @return void
     * @Description:--切点方法的配置信息
     * @Param []
     * @desc--之前的配置方式是@execution("类路径.类名.方法名"),现在方式@annotation("注解类路径"):实现使用该注解方法都会进行AOP操作
     */
    @Pointcut("@annotation(cn.zhidasifang.common.annotation.SysLogAnnotation)")
    public void aspectPointcutMethod_SysLog() {
    }

    @Before(value = "aspectPointcutMethod_SysLog()")
    public void sysLogDoBefore(JoinPoint joinPoint) throws InterruptedException {
        THREAD_LOCAL.set(new Date());
        //Thread.sleep(5000);
    }

    @After(value = "aspectPointcutMethod_SysLog() && @annotation(sysLogAnnotation)", argNames = "joinPoint,sysLogAnnotation")
    //携带参数annotation:syslogAnnotation
    public void sysLogDoAfter(JoinPoint joinPoint, SysLogAnnotation sysLogAnnotation) throws ClassNotFoundException, NoSuchMethodException {
        //获取注解中的信息【可以忽略掉,能直接获取注解信息的话!】
        //Map<String, Object> annotationInfos = getAnnotationInfos(joinPoint,sysLogAnnotation);

        //创建日志实体 SysLog
        LogInfo logInfo = new LogInfo()
                //忽略使用mybatis-Plus 主键策略Assign.UUID来完成!
                //.setLid(RandomUtil.randomLong(0,1000000000000000000L))
                //.setUserName("AOP中获取系统管理员")
                .setRequestUrl(httpServletRequest.getHeader("host"))
                //.setRequestType((String) annotationInfos.get("type"))
                .setRequestType((String) sysLogAnnotation.type().toString())
                //.setDesc((String) annotationInfos.get("desc"))
                .setDesc((String) sysLogAnnotation.desc())
                .setCreateTime(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(THREAD_LOCAL.get()));

        //持久化日志对象操作
        log.info("【AOP:后置操作,操作日志持久化操作】");
        sysLogService.addSysLog(logInfo);
    }

    /**
     * @return java.util.HashMap
     * @Description:获取方法上注解中的信息--【废弃】
     * @Param [joinPoint]
     */
    public Map<String, Object> getAnnotationInfos(JoinPoint joinPoint, SysLogAnnotation sysLogAnnotation) throws ClassNotFoundException, NoSuchMethodException {
        HashMap<String, Object> annotationInfo = new HashMap<>();
        //获取方法所在类名
        String className = joinPoint.getTarget().getClass().getName();
        //获取方法名
        String methodName = joinPoint.getSignature().getName();
        System.out.println("sysLogAnnotation.type() = " + sysLogAnnotation.type());
        System.out.println("sysLogAnnotation.desc() = " + sysLogAnnotation.desc());
        /*
        //获取方法所有参数
        Object[] args = joinPoint.getArgs();
        //获取该方法的类中的所有方法
        Method[] methods = Class.forName(className).getMethods();
        */
        /**
         * todo--带参数方法,通过反射获取时,需要添加参数相关的信息,否则获取为null,比如方法的重载。 获取带参方法失败!!!
         * todo--废弃该方法,直接将注解相关信息通过参数的方式传入到切面类中来,不在通过反射的方式在单独的去获取对应的方法,在通过方法实例在获取注解信息!
         *
         Object[] args = joinPoint.getArgs();
         System.out.println("args.toString() = " + args.toString());
         Method method = null;
         if (ObjectUtil.isNull(args)){
         method = Class.forName(className).getMethod(methodName);
         }else {
         method= Class.forName(className).getMethod(methodName,args.getClass());
         }*/
        //通过反射的机制来获取方法中的注解信息
        Method method = Class.forName(className).getMethod(methodName);
        String desc = method.getAnnotation(SysLogAnnotation.class).desc();
        String type = method.getAnnotation(SysLogAnnotation.class).type().toString();
        annotationInfo.put("desc", desc);
        annotationInfo.put("type", type);
        return annotationInfo;
    }
}

五、过滤器和拦截器的区别

​ 其实过滤器和拦截器都是AOP(面向切面编程)的具体实现,都可以实现诸如登录鉴权、日志记录等功能,但是也存在一些不同点。

5.1 实现原理不同

(1)过滤器(Filter)基于函数回调。
其中实现接口中的 doFilter( ServletRequest , ServletResponse , FilterChain ) 方法,其中的FilterChain类对象就是一个回调接口,该类中的doFilter方法就是回调方法。

(2)拦截器(Interceptor)基于Java反射机制(动态代理)实现。

在这里插入图片描述

5.2 使用范围不同

(1)自定义的过滤器需要实现javax.servlet.Filter接口,而这个接口是在Servlet规范中定义的,或者说过滤器需要依赖于Servlet容器,因此它只能在Web程序中使用。

(2)自定义的拦截器需要实现 org.springframework.web.servlet.HandlerInterceptor 接口,它存在于SpringMVC中,由Spring容器来管理,不依赖于Servlet容器,是可以单独使用的,因此不仅可以用在Web程序中,也可以用在Application、Swing等程序中。

5.3 触发时机不同

(1)过滤器(Filter)是在请求进入容器后,但在进入Servlet之前进行预处理,请求结束是在Servlet处理完之后。

(2)拦截器(Interceptor)是请求进入Servlet后,但在进入Controller之前进行预处理,请求结束是在Controller中渲染了对应的视图之后。

在这里插入图片描述

5.4 拦截的请求范围不同

​ 过滤器的init和destory方法只会调用一次,且分别在容器初始化和销毁时调用。而拦截器中的三个方法和过滤器中的doFilter方法则会在每次请求的时候都会调用。

拦截器可以在方法执行前调用(preHandle),方法执行后调用(postHandle),视图页面渲染后调用(afterCompletion)。

​ 实际上过滤器会对几乎所有进入容器的请求都起作用,但是拦截器只会对Controller中的请求或者访问static目录下的静态资源起作用(目标执行方法起作用)。

在这里插入图片描述

5.5 注入Bean的时机不同

​ 如果需要在分别在过滤器和拦截器中都注入Service层。

​ 在Interceptor的配置类中(实现 **WebMvcConfigurer ** 接口)的类中的 addInterceptors( ) 中的代码,在调用 addInterceptor 方法的时候如果手动new了一个 Interceptor 自定义拦截器对象。

@Configuration
public class WebAllHandlerConfig implements WebMvcConfigurer {


    /** 注入拦截器HandlerInterceptor */
    @Autowired
    private MyHandlerInterceptorTest handlerInterceptorTest;

    /**
    *@Description:配置拦截器,拦截器属于springframework,需要通过配置类进行添加!!!!
     *  1.通过@Component注解无效,还需要通过InterceptorRegistry注册: addInterceptor(xx)添加拦截器Interceptor
     *  2.通过addPathPatterns(xxx)添加拦截路径
    *@Param [registry]
    *@return void
    */
    @Override //重写该方法,通过 InterceptorRegistry 拦截器注册实例进行 addInterceptor(xx)添加拦截器Interceptor
    public void addInterceptors(InterceptorRegistry registry) {
        //配置拦截路径
        registry.addInterceptor(/*this.getInterceptor()*/ handlerInterceptorTest).addPathPatterns("/handler/testFilter","/**/**","/**");
    }
}

​ 如果在 Interceptor 自定义拦截器类上又添加了 @Component注解,表示让Spring容器来管理并生成 Interceptor对象。

@Slf4j
@Component
public class MyHandlerInterceptorTest implements HandlerInterceptor {
    	.....
}

​ 很明显这样注册的拦截器和Spring容器中存在的不是同一个,所以才会导致Spring无法管理进而出现空指针。解决办法就是去掉 Interceptor 自定义拦截器类上的 @Component 注解,同时在 InterceptorConfig 类中手动定义一个 Interceptor 对象,并将该拦截器注册一下。

5.6 执行流程不同

​ 实际开发过程中经常出现同时存在多个拦截器或者过滤器,通过不同的方式来控制它们执行的先后顺序。

(1)过滤器控制执行顺序

​ 可以在类上使用 @Order 注解来控制多个过滤器的执行顺序,其中 @Order 注解中的数字越小,那么优先级越高,越先执行。

(2)过滤器执行流程

​ 客户端发起的请求首先是经过了过滤器并处理了Request请求,之后去访问Servlet资源,当Servlet执行完毕后,Response响应也会经过过滤器,即也是经过了过滤器处理。

在这里插入图片描述

(3)拦截器控制执行顺序

​ 注册顺序(在实现 **WebMvcConfigurer ** 接口的配置类中的 addInterceptors() 方法)就是默认的执行顺序,当然也可以通过Order方法来进行控制, Order 方法中的数字越小,那么优先级越高,越先执行。

​ 对于多个拦截器来说,先声明的拦截器的 preHandle 方法先执行,但是它的postHandle 方法后执行,也就是说 postHandle 方法的执行顺序和 preHandle 方法的执行顺序是相反的。

(4)拦截器执行流程

程序先执行 preHandle() 方法,如果该方法返回值为true,那么程序将继续往下执行处理器中的方法,否则认为当前请求结束,不再往下执行;

​ 在Controller类(业务处理器)处理完请求后会执行 postHandle() 方法,之后通过DispatcherServlet 向客户端返回响应;

​ DispatcherServlet 处理完请求后执行 afterCompletion() 方法。

在这里插入图片描述

结束!

  • 24
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值