Spring04 - filter和interceptor

filter和interceptor

文章目录

一:舶来品和原住民

1:Filter:

进入servlet之前和退出servlet之后

1.1:基本概念

当我们仔细阅读源码之后会发现filter这个概念竟然是一个源自于Servlet的舶来品(遵循Servlet规范):

java 复制代码
javax.servlet.Filter

可以看到Filter本身是用在Tomcat等Web容器进行Servlet相关处理时使用的工具,并非是Spring原生的工具

从这一发现中我们不难揣测在Spring中为什么Filter和Interceptor在职能上是如此的相近,因为这两者的作者并非一人,在构建各自体系时产生相同的想法和思路也是可以理解的,毕竟君子所见略同也是时有发生的事情

后续Spring为了引入和兼容Tomcat容器的处理逻辑,将两个较为相似地概念放置在同一个应用上下文中(注意,Spring并没有做合并处理,只是兼容),导致开发者时常迷糊也变得情有可原。

为了更好地了解Filter的职能,这里我们引入官方注释来帮助理解:

A filter is an object that performs filtering tasks on either the request to a resource (a servlet or static content), or on the response from a resource, or both.

过滤器是对资源请求(servlet 或静态内容)或来自资源的响应或两者执行过滤任务的对象。

从上面的定义中我们可以得到两点有用信息:

  • 执行时机:Filter的执行时机有两个,分别是对资源的请求被执行前(进入servlet之前)和将来自资源的响应返回前(出servlet之后)
  • 执行内容:过滤器本质是在执行一个过滤任务,而过滤条件需要根据对资源的请求或者来自资源的响应进行判断(责任链模式)

在实际开发场景中,对于资源请求的预处理或者资源响应的后置处理可能不单只会有一类过滤任务,所以Tomcat在编码设计中使用了责任链模式来完成对于需要使用多个不同类型过滤器处理请求或者响应的场景,这一点在上面的流程图中也有所体现。

这里需要注意一点,由于使用了链式结构这一线性数据结构,在filter的实际执行过程中就会存在执行顺序的问题,这就意味着我们在实现自定义过滤器时不能出现过滤器依赖颠倒的情况,当然如果过滤器之间不存在依赖关系则无需考虑顺序问题。

在Tomcat中使用org.apache.catalina.core.ApplicationFilterChain来实现上面提到的责任链模式,这里我们可以结合部分代码简单了解一下:

java 复制代码
/**
 * 执行过滤操作的主要方法
 * 
 * @param request 代表请求对象,用于访问请求数据
 * @param response 代表响应对象,用于向客户端发送数据
 * 
 * @throws IOException 如果发生输入/输出错误
 * @throws ServletException 如果在执行过滤过程中发生Servlet异常
 * 
 * 此方法首先检查全局变量IS_SECURITY_ENABLED以确定是否启用安全性
 * 如果启用安全性,则使用AccessController.doPrivileged执行特权操作,
 * 其中内部调用internalDoFilter方法进行实际的过滤操作
 * 如果在特权操作过程中抛出异常,则捕获并重新抛出相应的异常类型
 * 如果未启用安全性,则直接调用internalDoFilter方法执行过滤操作
 */
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
    if (Globals.IS_SECURITY_ENABLED) {
        ServletRequest req = request;
        ServletResponse res = response;

        try {
            AccessController.doPrivileged(() -> {
                this.internalDoFilter(req, res);
                return null;
            });
        } catch (PrivilegedActionException pe) {
            Exception e = pe.getException();
            if (e instanceof ServletException) {
                throw (ServletException)e;
            }

            if (e instanceof IOException) {
                throw (IOException)e;
            }

            if (e instanceof RuntimeException) {
                throw (RuntimeException)e;
            }

            throw new ServletException(e.getMessage(), e);
        }
    } else {
        this.internalDoFilter(request, response);
    }
}


/**
 * 执行过滤器链中的过滤器或目标Servlet
 * 该方法根据当前过滤器的位置,决定是执行过滤器还是直接执行目标Servlet
 * 
 * @param request Servlet请求对象,用于传递请求信息
 * @param response Servlet响应对象,用于传递响应信息
 * @throws IOException 如果发生I/O错误
 * @throws ServletException 如果Servlet遇到异常
 */
private void internalDoFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
    // 检查是否还有过滤器需要执行
    if (this.pos < this.n) {
        // 获取当前过滤器配置并递增位置指针
        ApplicationFilterConfig filterConfig = this.filters[this.pos++];

        try {
            // 获取当前过滤器实例
            Filter filter = filterConfig.getFilter();
            // 如果请求支持异步且过滤器不支持异步,则设置请求属性为异步不支持
            if (request.isAsyncSupported() && "false".equalsIgnoreCase(filterConfig.getFilterDef().getAsyncSupported())) {
                request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", Boolean.FALSE);
            }

            // 如果启用了安全性,则以特权身份执行过滤器的doFilter方法
            if (Globals.IS_SECURITY_ENABLED) {
                Principal principal = ((HttpServletRequest)request).getUserPrincipal();
                Object[] args = new Object[]{request, response, this};
                SecurityUtil.doAsPrivilege("doFilter", filter, classType, args, principal);
            } else {
                // 否则直接执行过滤器的doFilter方法
                filter.doFilter(request, response, this);
            }

        } catch (ServletException | RuntimeException | IOException e) {
            // 直接抛出这些异常
            throw e;
        } catch (Throwable e) {
            // 处理其他类型的异常
            Throwable res = ExceptionUtils.unwrapInvocationTargetException(e);
            ExceptionUtils.handleThrowable(res);
            throw new ServletException(sm.getString("filterChain.filter"), res);
        }
    } else {
        // 如果没有过滤器需要执行,则直接执行目标Servlet的service方法
        try {
            if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
                lastServicedRequest.set(request);
                lastServicedResponse.set(response);
            }

            // 如果请求支持异步且Servlet不支持异步,则设置请求属性为异步不支持
            if (request.isAsyncSupported() && !this.servletSupportsAsync) {
                request.setAttribute("org.apache.catalina.ASYNC_SUPPORTED", Boolean.FALSE);
            }

            // 如果启用了安全性且请求和响应类型匹配,则以特权身份执行Servlet的service方法
            if (request instanceof HttpServletRequest && response instanceof HttpServletResponse && Globals.IS_SECURITY_ENABLED) {
                Principal principal = ((HttpServletRequest)request).getUserPrincipal();
                Object[] args = new Object[]{request, response};
                SecurityUtil.doAsPrivilege("service", this.servlet, classTypeUsedInService, args, principal);
            } else {
                // 否则直接执行Servlet的service方法
                this.servlet.service(request, response);
            }
        } catch (ServletException | RuntimeException | IOException e) {
            // 直接抛出这些异常
            throw e;
        } catch (Throwable e) {
            // 处理其他类型的异常
            Throwable e = ExceptionUtils.unwrapInvocationTargetException(e);
            ExceptionUtils.handleThrowable(e);
            throw new ServletException(sm.getString("filterChain.servlet"), e);
        } finally {
            // 清理线程本地变量
            if (ApplicationDispatcher.WRAP_SAME_OBJECT) {
                lastServicedRequest.set((Object)null);
                lastServicedResponse.set((Object)null);
            }
        }
    }
}

从上面的代码中我们可以看到Tomcat使用了pos指针来完成对于过滤器链中过滤器执行位置的记录

在完成链中所有过滤器执行并且通过之后,requestresponse对象才会提交给servlet实例进行对应服务的处理。

需要注意,此时并没有涉及到某个具体handler

也就是说filter的处理并不能细化到某一类具体的handler请求/响应,只能较为模糊处理整个servlet实例维度的请求/响应。

当然,从上面的代码我们可以发现另外一个问题:代码中好像只有针对资源请求维度的过滤处理而没有对于资源响应的过滤处理。

其实对于资源响应的过滤处理被隐藏在每个过滤器的doFilter方法中了

在实现自定义过滤器时我们需要按照以下逻辑来编写代码才能完成对于资源响应的处理:

java 复制代码
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    // TODO 前置处理
    // 调用applicationFilterChain对象的doFilter方法(这里实际上是一个回调逻辑),这里一定要加上,否则链式结构就会从这里断开。
    chain.doFilter(request, response);
    // TODO 后置处理
}

结合ApplicationFilterChaininternalDoFilter方法可以发现这里隐含了一个入栈出栈(其实就是方法栈)的逻辑。

对于资源请求的预处理过程实际上是一个入栈的过程,当所有的预处理过滤器入栈完毕则就会开始执行servlet.service(request, response)

在完成servlet服务处理之后,就会进入到出栈过程,此时会从最后一个过滤器的后置处理逻辑逐一执行并退出方法。

不得不说,这里的逻辑对于刚入门的新手来说确实不是非常友好

1.2:Spring Boot中的使用

在SpringBoot中提供了两种方法完成这一操作:

  • 在自定义过滤器上使用@Component注解;
  • 在自定义过滤器上使用@WebFilter注解,并在启动类上使用@ServletComponentScan注解;

更推荐使用第二种方式来完成过滤器的注入,因为Spring在兼容过滤器的处理过程时还提供了原有Tomcat不存在的功能,即url匹配能力。

结合@WebFilter注解中的urlPattern字段,Spring能够将过滤器的处理粒度进一步细化,让开发人员在使用上变得更加灵活。

除此之外,为了确定过滤器注入的顺序,我们还可以使用Spring提供的@Order注解来自定义过滤器的顺序

java 复制代码
package com.study.study_demo_of_spring_boot.filter;

import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

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

/**
 * <p>
 * 功能描述:跨域拦截
 * filter的处理并不能细化到某一类具体的handler请求/响应,只能较为模糊处理整个servlet实例维度的请求/响应
 * </p>
 *
 * @author cui haida
 * @date 2024/04/06/7:54
 */
@Order(1) // 定义过滤器的顺序
@Component
// 结合`@WebFilter`注解中的`urlPattern`字段,Spring能够将过滤器的处理粒度进一步细化,让开发人员在使用上变得更加灵活。
@WebFilter(urlPatterns = "/*", filterName = "cooCorsFilter") // 拦截所有请求
public class CoocorsFilter implements Filter {

    String origin = "origin";

    @Override
    public void init(FilterConfig filterConfig) {

    }

    /**
     * 过滤器的执行方法,用于处理跨域请求。
     *
     * @param servletRequest 客户端请求的ServletResponse对象。
     * @param servletResponse 用于向客户端发送响应的ServletResponse对象。
     * @param filterChain 过滤器链,包含当前过滤器之后的所有过滤器。
     * @throws IOException 如果发生输入/输出错误。
     * @throws ServletException 如果处理请求时发生Servlet相关错误。
     */
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        // 将通用的ServletRequest和ServletResponse转换为HttpServletRequest和HttpServletResponse,以便获取更多特定的HTTP信息
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletRequest;

        // 获取并处理请求头中的Origin字段,用于指定允许的跨域请求来源
        String origin = request.getHeader(this.origin);

        // 设置响应头,以允许跨域请求并指定允许的方法、凭证和请求头
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        // 设置返回结果的允许请求来源就是请求中的请求来源
        response.setHeader("Access-Control-Allow-Origin", origin);
        // 允许的方法,如果是允许所有的方法,指定成为*
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
        // 是否允许携带验证头信息,这里设置true表示允许
        response.setHeader("Access-Control-Allow-Credentials", "true");
        // 允许的请求头
        response.setHeader("Access-Control-Allow-Headers", "Content-Type,Authorization,token,userId,sysCode,requestSourceType");

        // 继续处理请求,将请求传递给过滤器链中的下一个过滤器
        filterChain.doFilter(request, response);
    }


    @Override
    public void destroy() {

    }
}

2:Interceptor:

Spring的原住民:servlet内部

2.1:基本概念

完了filter,我们再将目光转回到Interceptor上。

这一次我们可以发现Interceptor这样一个概念是Spring原创的,其对应的具体接口类为HandlerInterceptor

在阅读完对应的源码之后我们可以发现,区别于Filter只提供了一个简单的doFilter方法

HandlerInterceptor当中明确提供了三个与执行时机相关的方法:

  • preHandle(): 在执行对应handler之前会执行该方法进行前置处理;
  • postHandle(): 在对应handler完成请求处理之后且在ModelAndView对象被渲染之前会执行该方法来进行一些关于ModelAndView对象的后置处理;
  • afterCompletion(): 在ModelAndView对象完成渲染之后且在响应返回之前会执行该方法对结果进行后置处理;

相比Filter类中只是简单提供了一个doFilter方法,HandlerInterceptor中的方法定义显得更加明确和友好。

在不阅读源码和参考使用范例的情况下,我们也能大致猜测到需要如何实现自定义拦截器。



可以看到此时interceptor的执行逻辑都是包含在servlet实例当中

结合上面filter的执行过程我们不难发现,filter就像夹心饼干的两个饼干一样将servlet和interceptor夹在中间

interceptor的执行时机是要晚于filter的前置处理并且早于filter的后置处理的。

除此以外,在阅读源码过程中我们可以发现Spring在使用interceptor时同样也是用了责任链模式

不得不说在这种需要逐个执行不同任务处理逻辑的场景下责任链模式还是非常好用的。

需要注意,由于Spring在定义拦截器时已经明确了不同阶段执行的方法,所以在实际执行拦截器时并没有采用和过滤器一样的入栈出栈方式。

2.2:Spring Boot中的使用

在SpringBoot当中使用interceptor除了需要实现HandlerInterceptor接口,还需要显示注册Spring的Web配置当中,具体代码如下

java 复制代码
package filter.interceptor;

import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

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

/**
 * <p>
 * 功能描述:token拦截器
 * </p>
 *
 * @author cui haida
 * @date 2023/10/27/17:17
 */
public class TokenInterceptor implements HandlerInterceptor {
    /** 前置处理 **/
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 核心业务逻辑,判断是否登录等
        String token = request.getHeader("token");
        // 正常token是的登录后签发的,前端携带过来
        return StringUtils.hasLength(token);
        
    }

    /** 后置处理 **/
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
        // 后置处理逻辑
        System.out.println("controller 执行完了");
    }

    /** 完成之后 **/
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        System.out.println("我获取到了一个返回的结果:"+response);
        System.out.println("请求结束了");
    }
}
java 复制代码
package com.study.study_demo_of_spring_boot.interceptor;

import com.study.study_demo_of_spring_boot.interceptor.TokenInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * <p>
 * 功能描述:web配置
 * </p>
 *
 * @author cui haida
 * @date 2024/04/06/8:16
 */
@Configuration
public class WebConfig implements WebMvcConfigurer {
    /**
     * 添加拦截器
     * 本方法用于向Spring MVC框架注册一个拦截器,以便在处理HTTP请求之前进行预处理。
     * 具体在此处实现了对所有请求的拦截,除了登录请求外,都进行token的验证。
     *
     * @param registry 拦截器注册器,用于向Spring MVC注册拦截器
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //拦截,除了login请求,其他都拦截,进行token判断
        registry.addInterceptor(new TokenInterceptor()) // 添加token拦截器
                .addPathPatterns("/**") // 对所有请求进行拦截
                .excludePathPatterns("/login"); // 排除 login请求
    }

    /**
     * 创建并配置CorsFilter以解决跨域问题。
     * 该函数不接受任何参数。
     *
     * @return CorsFilter 一个用于处理跨域请求的过滤器实例。
     */
    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        // 配置跨域请求过滤器,使其对所有请求生效
        source.registerCorsConfiguration("/**", buildConfig());
        return new CorsFilter(source);
    }


    /**
     * 构建并返回CORS配置信息。
     * 该方法用于配置跨域资源共享(CORS)的规则。
     *
     * @return CorsConfiguration 返回配置好的CORS配置对象。
     */
    private CorsConfiguration buildConfig() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        // 设置允许所有来源的请求访问,这在开发阶段非常有用,但应在生产环境中进行严格限制
        corsConfiguration.addAllowedOrigin("*");
        corsConfiguration.setAllowCredentials(true);
        // 允许所有的请求头和请求方法
        corsConfiguration.addAllowedHeader("*");
        corsConfiguration.addAllowedMethod("*");

        return corsConfiguration;
    }
}

Spring也给自定义拦截器提供了和filter一样路径匹配功能,通过这样一个功能自定义拦截器可以针对更细粒度的handler请求和响应处理。

相关推荐
TDengine (老段)6 分钟前
TDengine 数学函数 CRC32 用户手册
java·大数据·数据库·sql·时序数据库·tdengine·1024程序员节
心随雨下25 分钟前
Tomcat日志配置与优化指南
java·服务器·tomcat
Kapaseker31 分钟前
Java 25 中值得关注的新特性
java
wljt35 分钟前
Linux 常用命令速查手册(Java开发版)
java·linux·python
撩得Android一次心动38 分钟前
Android 四大组件——BroadcastReceiver(广播)
android·java·android 四大组件
canonical_entropy41 分钟前
Nop平台到底有什么独特之处,它能用在什么场景?
java·后端·领域驱动设计
chilavert31844 分钟前
技术演进中的开发沉思-174 java-EJB:分布式通信
java·分布式
不是株1 小时前
JavaWeb(后端进阶)
java·开发语言·后端
编程火箭车2 小时前
【Java SE 基础学习打卡】02 计算机硬件与软件
java·电脑选购·计算机基础·编程入门·计算机硬件·软件系统·编程学习路线