过滤器_拦截器_切面


过滤器、拦截器、切面区别

(1)三种可获得的信息

可获得的信息 原始的Request和Response 拦截的类名/方法名 控制器的方法参数信息
过滤器 × ×
拦截器 ×
切面 ×

(2)拦截器、过滤器区别

  • 拦截器是基于java的反射机制的,而过滤器是基于函数回调。

  • 过滤器需要在servlet容器中实现,拦截器可以适用于javaEE,javaSE等各种环境

  • 拦截器可以调用IOC容器中的各种依赖,而过滤器不能

  • 过滤器只能在请求的前后使用,而拦截器可以详细到每个方法

  • Filter可以拦截所有请求。包括静态资源css,js;而拦截器只能拦截action请求。不包括静态资源

(3)AOP与过滤器、拦截器区别

  • 过滤器,拦截器拦截的是URL。AOP拦截的是类的元数据(包、类、方法名、参数等)。
  • 过滤器并没有定义业务用于执行逻辑前、后等,仅仅是请求到达就执行。
  • 拦截器有三个方法,相对于过滤器更加细致,有被拦截逻辑执行前、后等。 AOP针对具体的代码,能够实现更加复杂的业务逻辑。
  • 三者功能类似,但各有优势,从过滤器–》拦截器–》切面,拦截规则越来越细致。 执行顺序依次是过滤器、拦截器、切面。

(4)如何选择(使用场景)

过滤器

  • 统一设置编码
  • 过滤敏感字符
  • 登录校验
  • URL级别的访问权限控制
  • 数据压缩

拦截器

  • 日志记录
  • 权限校验
  • 登录校验
  • 性能检测[检测方法的执行时间]

其实拦截器和过滤器很像,有些使用场景。无论选用谁都能实现。需要注意的使他们彼此的使用范围,触发机制。

三者使用场景区别

在编写相对比较公用的代码时,优先考虑过滤器,然后是拦截器,最后是aop。

比如:

权限校验,一般情况下,所有的请求都需要做登陆校验,此时就应该使用过滤器在最顶层做校验;

日志记录,一般日志只会针对部分逻辑做日志记录,而且牵扯到业务逻辑完成前后的日志记录,因此使用过滤器不能细致地划分模块,此时应该考虑拦截器,然而拦截器也是依据URL做规则匹配,因此相对来说不够细致,因此我们会考虑到使用AOP实现,AOP可以针对代码的方法级别做拦截,很适合日志功能。

一、过滤器3种配置方式

0、可捕获的参数

  • request : 请求的参数,可以通过HttpServletRequestWrapper来修改传入的URI、头信息等;
  • response : 返回体
  • filterChain : 过滤链(待补充)

Filter的这个特性在生产环境中有很广泛的应用,如:修改请求和响应、防止xss攻击、包装二进制流使其可以多次读,等等。

以下三种方式配置过滤器对比

使用方式 排序 指定URL
@Component @Order ×
@WebFilter @ServletComponentScan ×
JavaConfig

实际使用过程中,可以按照业务需求选择合适的使用方式,比如:如果编写的过滤器要拦截所有请求,不需要指定URL,那选择最简单的 @Component+@Order 就非常合适。

1、配置类(推荐)

通过 Java 代码显式配置 Filter ,功能强大,配置灵活。只需要把每个自定义的 Filter 声明成 Bean 交给 Spring 管理即可,还可以设置匹配的 URL 、指定 Filter 的先后顺序。

1、定义过滤器

@Slf4j
public class TestFilter1 implements Filter {

    // 项目启动时执行
    @Override
    public void init(FilterConfig filterConfig) {
        log.info("过滤器1初始化!!!!");
    }

    // 请求被拦截时执行
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        // 请求执行前
        log.info("过滤器1开始!!!!");
        long start = System.currentTimeMillis();
        filterChain.doFilter(servletRequest, servletResponse);

        // 请求执行后
        log.info("过滤器1结束!!!!耗时:" + (System.currentTimeMillis() - start) + "ms");
    }

    //当Filter被移除或服务器正常关闭时
    @Override
    public void destroy() {
        log.info("过滤器1销毁!!!!");
    }
}

2、配置过滤器

@Configuration
public class FilterConfig {

    // 配置 filter1
    @Bean
    public FilterRegistrationBean registFilter1() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(new TestFilter1());//设置过滤器
        registration.addUrlPatterns("/*");//设置过滤URL
        registration.setName("TestFilter1");//设置过滤器名称
        registration.setOrder(1);//设置过滤器执行顺序
        return registration;
    }

    // 配置 filter2
    @Bean
    public FilterRegistrationBean registFilter2() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(new TestFilter2());
        registration.addUrlPatterns("/*");
        registration.setName("TestFilter2");
        registration.setOrder(2);
        return registration;
    }
}    

执行顺序

假设有order 分别为 1 和 2 的2个过滤器:

过滤器1开始 - 过滤器2开始 - controller - 过滤器2结束 - 过滤器1结束

2、@WebFilter

MyFilter上添加@WebFilter注解,并在启动类上增加@ServletComponentScan("com.zhengxl.filterdemo.filter")注解,参数就是Filter所在的包路径,相当于告诉 SpringBoot,去哪里扫描 Filter。

@WebFilter+@ServletComponentScan 注解方式支持对 Filter 匹配指定URL,但是不支持指定 Filter 的执行顺序。

@Slf4j
/**
 * 如果用此注解,一定要在配置类中加另外一个注解:@ServletComponetScan,指定扫描的包。
 * 过滤器执行顺序会按照filterName的字母顺序进行
 * @WebFilter指定的过滤器优先级都高于FilterRegistrationBean配置的过滤器
 */
@WebFilter(urlPatterns = "/*", filterName = "aaaa")
public class TestFilter1 implements Filter {
    @Override
    public void init(FilterConfig filterConfig) {
        log.info("过滤器1初始化!!!!");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        log.info("过滤器1开始!!!!");
        long start = System.currentTimeMillis();
        filterChain.doFilter(servletRequest, servletResponse);
        log.info("过滤器1结束!!!!耗时:" + (System.currentTimeMillis() - start) + "ms");
    }

    @Override
    public void destroy() {
        //当Filter被移除或服务器正常关闭时
        log.info("过滤器1销毁!!!!");
    }
}
@SpringBootApplication
// 指定扫描的包中的 servlet 组件
@ServletComponentScan("com.qjc.filter")
public class FilterInteceptorApplication {

    public static void main(String[] args) {
        SpringApplication.run(FilterInteceptorApplication.class, args);
    }

}

3、简易(仅全局)

@Component
@Order(1)
public class MyFilter implements Filter {
   @Override
   public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
       System.out.println("MyFilter");
       // 要继续处理请求,必须添加 filterChain.doFilter()
       filterChain.doFilter(servletRequest,servletResponse);
   }
}

没错就这么简单,这样 MyFilter 就生效了,写个Controller 调用一下就可以看到效果。

当有多个Filter时,这里的@Order(1)注解会指定执行顺序,数字越小,越优先执行,如果只写@Order,默认顺序值是Integer.MAX_VALUE

@Component + @Order 注解方式配置简单,支持自定义 Filter 顺序。缺点是只能拦截所有URL,不能通过配置去拦截指定的 URL。

二、拦截器

1、可捕获的参数

  • request : 请求的参数,可以通过HttpServletRequestWrapper来修改传入的URI、头信息等;
  • response : 返回体
  • handler :

2、代码实现

拦截器

@Slf4j
public class TestInterceptor1 implements HandlerInterceptor {

    // 请求执行前执行
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("请求执行前拦截器11111开始!!!!");
        long start = System.currentTimeMillis();

        // 获取拦截的类和方法信息
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        System.out.println(handlerMethod.getBean().getClass().getName());    //获取类名+信息
        System.out.println(handlerMethod.getMethod().getName());    //获取方法名

        // 向request设置值
        request.setAttribute("MyKey","MyValue");

        log.info("请求执行前拦截器11111结束!!!!耗时:" + (System.currentTimeMillis() - start) + "ms");

        // 可自定义返回数据
        //response.setContentType("application/json;charset=UTF-8");
        //response.getWriter().println(json);

        //当返回true的时候,才可以执行postHandle/afterCompletion,即表示是否进入controller层,返回false表示丢弃请求
        return true;
    }

    //请求完成后,视图渲染前执行
    //preHandler返回false/Controller的方法发生异常
    //这两种状况都不会调用 postHandler ;
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        // 从request 获取值
        System.out.println((String) request.getAttribute("MyKey"));

        log.info("请求执行完成后,拦截器11111开始!!!!");
        long start = System.currentTimeMillis();
        log.info("请求执行完成后,拦截器11111结束!!!!耗时:" + (System.currentTimeMillis() - start) + "ms");
    }

    // 视图渲染完成后
    //无论Controller的方法正常完成或发生异常, 都会调用, 而且可以拿到 Exception 对象!
    //若 handler成功, 则 ex==null; 若 handler抛出异常, ex有值(前提是: @ExceptionHandler没有处理抛出的异常, 如果处理了, 就到不了ex这里了);
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("视图渲染完成后,拦截器11111开始!!!!");
        long start = System.currentTimeMillis();
        log.info("视图渲染完成后,拦截器11111结束!!!!耗时:" + (System.currentTimeMillis() - start) + "ms");
    }
}

配置拦截器

@Configuration
//继承WebMvcConfigurer
public class mvcConfig implements WebMvcConfigurer {

    //添加自定义拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 链式编程,第一个参数为 自定义的拦截器
        registry.addInterceptor(new TestInterceptor1())
                //配置拦截路径
                .addPathPatterns("/**")
                //配置不拦截的路径
                .excludePathPatterns("/","/login","/css/**","/js/**","/fonts/**","images/**")
                //设置拦截器执行顺序
                .order(1);

        // 配置多个个拦截器
        registry.addInterceptor(new TestInterceptor2())
                .addPathPatterns("/**")
                .excludePathPatterns("/","/login","/css/**","/js/**","/fonts/**","images/**")
                .order(2);
    }
}

三、切面

1、可捕获的参数

  • 对应方法的参数
  • 方法的返回值

2、基本实现

<!-- AOP -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
@Aspect
@Component
public class OrderAspect {

    @Before("execution(public * com.rewind.controller.OrderController.*(..))")
    public void before(){
        System.out.println("前置增强,比如创建连接对象");
    }

    @After("execution(public * com.rewind.controller.OrderController.*(..))")
    public void after(){
        System.out.println("后置增强,比如关闭连接对象");
    }

    @AfterReturning("execution(public * com.rewind.controller.OrderController.*(..))")
    public void afterReturning(){
        System.out.println("方法执行成功通知,比如提交事务");
    }

    @AfterThrowing("execution(public * com.rewind.controller.OrderController.*(..))")
    public void afterThrowing(){
        System.out.println("方法执行异常通知,比如回滚事务");
    }

    // 获取到请求的参数/响应值,并且对参数和响应值做出一定修改并生效
    @Around("execution(public * com.rewind.controller.OrderController.*(..))")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        //获取方法参数值数组
        Object[] args = joinPoint.getArgs();
        //得到其方法签名
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        //获取方法参数类型数组
        Class[] paramTypeArray = methodSignature.getParameterTypes();
        if (EntityManager.class.isAssignableFrom(paramTypeArray[paramTypeArray.length - 1])) {
            //如果方法的参数列表最后一个参数是entityManager类型,则给其赋值
            args[args.length - 1] = entityManager;
        }
        log.info("请求参数为{}",args);
        //动态修改其参数
        //执行目标方法
        //注意,如果调用joinPoint.proceed()方法,则修改的参数值不会生效,必须调用joinPoint.proceed(Object[] args)
        Result result = (Result)joinPoint.proceed(args);

        if (result.getData() instanceof User) {
            User user = (User) result.getData();
            user.setUsername(user.getUsername() + " &");    // 设置值
        }
        log.info("响应结果为{}",result);
        //如果这里不返回result,则目标对象实际返回值会被置为null
        return result;
    }
}

切入点表达式

execution(<修饰符模式>?<返回类型模式><方法名模式>(<参数模式>)<异常模式>?)
execution(public * com.rewind.controller.OrderController.*(..))

execution(<返回类型模式><方法名模式>(<参数模式>))
execution(* com.rewind.controller.TestController.*(..))

其中:返回类型模式、方法名模式、参数模式是必选项

修饰符模式和异常模式可以省略,?表示可省略

3、看看就好

@Aspect
@Slf4j
@Component
public class TestAspect {

    @Pointcut("execution(* com.rewind.controller.TestController.*(..))")
//    @Pointcut("execution(* com.rewind.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;
    }

}

执行顺序

过滤器 -> 拦截器 -> 切面

: 过滤器1开始!!!!
: 过滤器2开始!!!!
: 请求执行前拦截器11111开始!!!!
: 请求执行前拦截器11111结束!!!!耗时:0ms
: 请求执行前拦截器22222开始!!!!
: 请求执行前拦截器22222结束!!!!耗时:0ms
: a715fda106894f5c83c68f21bc1d35d2 环绕通知,执行方法前,请求源IP:【127.0.0.1】,请求方法:【POST】,请求URL:【/test/find/dataList】,请求参数:【{"userName":"rewind"}】
: 前置通知!!!!方法执行前执行
: 查询数据成功!!!!
: a715fda106894f5c83c68f21bc1d35d2 环绕通知,执行方法后,请求源IP:【127.0.0.1】,请求URL:【/test/find/dataList】,返回参数:【{"data":"rewind","message":"查询成功","status":200}】,执行耗时 : 1ms
: 后置通知:方法执行完后执行
: 后置通知,方法执行完后执行,响应信息为:{"data":"rewind","message":"查询成功","status":200}
: 请求执行完成后,拦截器22222开始!!!!
: 请求执行完成后,拦截器22222结束!!!!耗时:0ms
: 请求执行完成后,拦截器11111开始!!!!
: 请求执行完成后,拦截器11111结束!!!!耗时:0ms
: 视图渲染完成后,拦截器22222开始!!!!
: 视图渲染完成后,拦截器22222结束!!!!耗时:0ms
: 视图渲染完成后,拦截器11111开始!!!!
: 视图渲染完成后,拦截器11111结束!!!!耗时:0ms
: 过滤器2结束!!!!耗时:15ms
: 过滤器1结束!!!!耗时:15ms

过滤器 - 拦截器 - 环绕通知 - 前置通知 - controller - 环绕通知 - 后置通知 - 拦截器 - 过滤器


  目录