过滤器 Filter
面试官:用过过滤器吧,介绍一下过滤器。
John 同学(心中窃喜):用过,我常常用它来净化水 😁…
面试官:明天的面试到此结束,回去等告诉吧。
John 同学:🙃…
Filter 根本介绍
过滤器 Filter 是 Sun 公司在 Servlet 2.3 标准中增加的新性能,其作用是对客户端发送给 Servlet 的申请以及对 Servlet 返回给客户端的响应做一些定制化的解决,例如校验申请的参数、设置申请 / 响应的 Header、批改申请 / 响应的内容等。
Filter 引入了过滤链(Filter Chain)的概念,一个 Web 利用能够部署多个 Filter,这些 Filter 会组成一种链式构造,客户端的申请在达到 Servlet 之前会始终在这个链上传递,不同的 Filter 负责对申请 / 响应做不同的解决。Filter 的解决流程如下图所示:
AOP 编程思维
在深刻了解 Filter 之前,咱们先聊一聊面向切面编程(Aspect Oriented Programming,AOP)。AOP 不是一种具体的技术,而是一种编程思维,它容许咱们在不批改源码的根底上实现办法逻辑的加强,也就是在办法执行前后增加一些自定义的解决。
Filter 是 AOP 编程思维的一种体现,其作用可认为是对 Servlet 性能的加强。Filter 能够对用户的申请做预处理,也能够对返回的响应做后处理,且这些解决逻辑与 Servlet 的解决逻辑是分隔开的,这使得程序中各局部业务逻辑之间的耦合度升高,从而进步了程序的可维护性和可扩展性。
创立 Filter
创立 Filter 须要实现 javax.servlet.Filter 接口,或者继承实现了 Filter 接口的父类。Filter 接口中定义了三个办法:
- init:在 Web 程序启动时被调用,用于初始化 Filter。
- doFilter:在客户端的申请达到时被调用,doFilter 办法中定义了 Filter 的次要解决逻辑,同时该办法还负责将申请传递给下一个 Filter 或 Servlet。
- destroy:在 Web 程序敞开时被调用,用于销毁一些资源。
上面咱们通过实现 Filter 接口来创立一个自定义的 Filter:
public class TestFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {System.out.println(filterConfig.getFilterName() + "被初始化");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {HttpServletRequest request = (HttpServletRequest) servletRequest;
System.out.println("Filter 拦挡到了申请:" + request.getRequestURL());
System.out.println("Filter 对申请做预处理...");
filterChain.doFilter(servletRequest, servletResponse);
System.out.println("Filter 批改响应的内容...");
}
@Override
public void destroy() {System.out.println("Filter 被回收");
}
}
- init 办法的 filterConfig 参数封装了以后 Filter 的配置信息,在 Filter 初始化时,咱们将 Filter 的名称打印在控制台。
- doFilter 办法定义了 Filter 拦挡到用户申请后的解决逻辑,
filterChain.doFilter(servletRequest, servletResponse);
指的是将申请传递给一下个 Filter 或 Servlet,如果不增加该语句,那么申请就不会向后传递,天然也不会被解决。在该语句之后,能够增加对响应的解决逻辑(如果要批改响应的 Header,可间接在该语句之前批改;如果要批改响应的内容,则须要在该语句之后,且须要自定义一个 response)。 - destroy 办法中,咱们输入 “Filter 被回收 ” 的提示信息。
配置 Filter
Spring 我的项目中,咱们能够应用 @Configuration + @Bean + FilterRegistrationBean
对 Filter 进行配置:
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<TestFilter> registryFilter() {FilterRegistrationBean<TestFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new TestFilter());
registration.addUrlPatterns("/*");
registration.setName("TestFilter");
registration.setOrder(0);
return registration;
}
}
上述代码中,setFilter 办法用于设置 Filter 的类型;addUrlPatterns 办法用于设置拦挡的规定;setName 办法用于设置 Filter 的名称;setOrder 办法用于设置 Filter 的优先级,数字越小优先级越高。
测试 Filter
接下来,咱们定义一个简略的 Web 服务,测试 Filter 是否失效:
@RestController
public class UserController {@RequestMapping(path = "/hello", method = RequestMethod.GET)
public String sayHello() {System.out.println("正在解决申请...");
System.out.println("申请解决实现~");
return "I'm fine, thank you.";
}
}
启动我的项目,在浏览器中拜访 localhost:8080/hello
,期待申请解决实现,而后敞开我的项目。整个过程中,控制台顺次打印了如下信息:
能够看到,自定义的 TestFilter 实现了拦挡申请、解决响应的指标。
创立 Filter 的其它形式
1. @WebFilter 注解 + 包扫描
除了 FilterRegistrationBean 外,Servlet 3.0 引入的注解 @WebFilter 也可用于配置 Filter。咱们只须要在自定义的 Filter 类上增加该注解,就能够设置 Filter 的名称和拦挡规定:
@WebFilter(urlPatterns = "/*", filterName = "TestFilter")
public class TestFilter implements Filter {// 省略局部代码}
因为 @WebFilter 并非 Spring 提供,因而若要使自定义的 Filter 失效,还需在配置类上增加 @ServletComponetScan 注解,并指定扫描的包:
@SpringBootApplication
@ServletComponentScan("com.example.filter")
public class DemoApplication {public static void main(String[] args) {SpringApplication.run(DemoApplication.class, args);
}
}
须要留神的是,@WebFilter 注解并不容许咱们设置 Filter 的执行程序,且在 Filter 类上增加 @Order 注解也是有效的。如果我的项目中有多个被 @WebFilter 润饰的 Filter,那么这些 Filter 的执行程序由其 “ 类名的字典序 ” 决定,例如类名为 “Axx” 的 Filter 的执行程序要先于类名为 “Bxx” 的 Filter。
增加了 @WebFilter 注解后就不要再增加 @Component 注解了,如果都增加,那么零碎会创立两个 Filter。
2. @Component 注解
Spring 我的项目中,咱们能够通过增加 @Component 注解将自定义的 Bean 交给 Spring 容器治理。同样的,对于自定义的 Filter,咱们也能够间接增加 @Component 注解使其失效,而且还能够增加 @Order 注解来设置不同 Filter 的执行程序。
@Component
@Order(1)
public class TestFilter implements Filter {// 省略局部代码}
此种配置形式个别不常应用,因为其无奈设置 Filter 的拦挡规定,默认的拦挡门路为 /*
。尽管不能配置拦挡规定,但咱们能够在 doFilter 办法中定义申请的放行规定,例如当申请的 URL 匹配咱们设置的规定时,间接将该申请放行,也就是立刻执行 filterChain.doFilter(servletRequest, servletResponse);
。
3. 继承 OncePerRequestFilter
OncePerRequestFilter 是一个由 Spring 提供的抽象类,在我的项目中,咱们能够采纳继承 OncePerRequestFilter 的形式创立 Filter,而后重写 doFilterInternal 办法定义 Filter 的解决逻辑,重写 shouldNotFilter 办法设置 Filter 的放行规定。对于多个 Filter 的执行程序,咱们也能够通过增加 @Order 注解进行设置。当然,若要使 Filter 失效,还需增加 @Component 注解将其注册到 Spring 容器。
@Component
@Order(1)
public class CSpringFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {// 解决逻辑}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {// 放行规定}
}
实际上,形式 2 和形式 3 实质上并没有什么区别,因为 OncePerRequestFilter 底层也是通过实现 Filter 接口来达到过滤申请 / 响应的目标,只不过 Spring 在 OncePerRequestFilter 中帮咱们封装了许多性能,因而更举荐采纳此种形式创立 Filter。
Filter 的优先级
上文中提到,应用配置类或增加 @Order 注解能够显式的设置 Filter 的执行程序,批改类名能够隐式的设置 Filter 的执行程序。如果我的项目中存在多个 Filter,且这些 Filter 由不同的形式创立,那么它们的执行程序是怎么的呢?
可能确定的是,Spring 依据 Filter 的 order 决定其优先级,如果咱们通过配置类或者通过 @Order 注解设置了 Filter 的 order,那么 order 值越小的 Filter 的优先级越高,无论 Filter 由何种形式创立。如果多个 Filter 的优先级雷同,那么执行程序为:
- 配置类中配置的 Filter 优先执行,如果配置类中存在多个 Filter,那么 Spring 依照其在配置类中配置的程序顺次执行。
- @WebFilter 注解润饰的 Filter 之后执行,如果存在多个 Filter,那么 Spring 依照其类名的字典序顺次执行。
- @Component 注解润饰的 Filter 最初执行,如果存在多个 Filter,那么 Spring 依照其类名的字典序顺次执行。
留神,以上优先级程序仅实用于 order 雷同的非凡状况。如果咱们不配置 Filter 的 order,那么 Spring 默认将其 order 设置为 LOWEST_PRECEDENCE = Integer.MAX_VALUE
,也就是最低优先级。因为被 @WebFilter 注解润饰的 Filter 无奈显式配置优先级,因而其 order 为 Integer.MAX_VALUE。本文所说的 Filter 的优先级指的是 Filter 对申请做预处理的优先级,对响应做后处理的优先级与之相同。
以上论断由笔者通过测试以及浏览源码得出,如有了解谬误,欢送批评指正 😁。对于源码局部,有趣味的小伙伴能够看看 ServletContextInitializerBeans 类和 AnnotationAwareOrderComparator 类的源码,笔者在这里就不具体分析了 😈。
<!– 这些 Filter 的初始化程序由 filterName(默认为类名的首字母小写)的 hash 值决定 –>
Filter 的利用场景
Filter 的常见利用场景包含:
- 解决跨域拜访:前后端拆散的我的项目往往存在跨域拜访的问题,Filter 容许咱们在 response 的 Header 中设置 “Access-Control-Allow-Origin”、”Access-Control-Allow-Methods” 等头域,以此解决跨域失败问题。
- 设置字符编码:字符编码 Filter 能够在 request 提交到 Servlet 之前或者在 response 返回给客户端之前为申请 / 响应设置特定的编码格局,以解决申请 / 响应内容乱码的问题。
- 记录日志:日志记录 Filter 能够在拦挡到申请后,记录申请的 IP、拜访的 URL,拦挡到响应后记录申请的解决工夫。当不须要记录日志时,也能够间接将 Filter 的配置正文掉。
- 校验权限:Web 服务中,客户端在发送申请时会携带 cookie 或者 token 进行身份认证,权限校验 Filter 能够在 request 提交到 Servlet 之前对 cookie 或 token 进行校验,如果用户未登录或者权限不够,那么 Filter 能够对申请做重定向或返回错误信息。
- 替换内容:内容替换 Filter 能够对网站的内容进行管制,避免输出 / 输入非法内容和敏感信息。例如在申请达到 Servlet 之前对申请的内容进行本义,避免 XSS 攻打;在 Servlet 将内容输入到 response 时,应用 response 将内容缓存起来,而后在 Filter 中进行替换,最初再输入到客户浏览器(因为默认的 response 并不能严格的缓存输入内容,因而须要自定义一个具备缓存性能的 response)。
Filter 利用场景的相干内容参考自《Java Web 整合开发之王者归来》,好中二的书名 🤣,对于自定义具备缓存性能的 response 可参考该书的 P175。
拦截器 Interceptor
Interceptor 根本介绍
本文所说的拦截器指的是 Spring MVC 中的拦截器。
拦截器 Interceptor 是 Spring MVC 中的高级组件之一,其作用是拦挡用户的申请,并在申请解决前后做一些自定义的解决,如校验权限、记录日志等。这一点和 Filter 十分类似,但不同的是,Filter 在申请达到 Servlet 之前对申请进行拦挡,而 Interceptor 则是在申请达到 Controller 之前对申请进行拦挡,响应也同理。
与 Filter 一样,Interceptor 也是 AOP 编程思维的体现,且 Interceptor 也具备链式构造,咱们在我的项目中能够配置多个 Interceptor,当申请达到时,每个 Interceptor 依据其申明的程序顺次执行。
创立 Interceptor
创立 Interceptor 须要实现 org.springframework.web.servlet.HandlerInterceptor 接口,HandlerInterceptor 接口中定义了三个办法:
- preHandle:在 Controller 办法执行前被调用,能够对申请做预处理。该办法的返回值是一个 boolean 变量,只有当返回值为 true 时,程序才会持续向下执行。
- postHandle:在 Controller 办法执行完结,DispatcherServlet 进行视图渲染之前被调用,该办法内能够操作 Controller 解决后的 ModelAndView 对象。
- afterCompletion:在整个申请解决实现(包含视图渲染)后被调用,通常用来清理资源。
留神,postHandle 办法和 afterCompletion 办法执行的前提条件是 preHandle 办法的返回值为 true。如果 Controller 抛出异样,那么 postHandle 办法将不会执行,afterCompletion 办法则肯定执行,详见 DispatcherServlet 类中的 doDispatch 办法。
上面咱们创立一个 Interceptor:
@Component
public class TestInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {System.out.println("Interceptor 拦挡到了申请:" + request.getRequestURL());
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {System.out.println("Interceptor 操作 modelAndView...");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {System.out.println("Interceptor 清理资源...");
}
}
配置 Interceptor
Interceptor 须要注册到 Spring 容器才可能失效,注册的办法是在配置类中实现 WebMvcConfigurer 接口,并重写 addInterceptors 办法:
@Configuration
public class TestInterceptorConfig implements WebMvcConfigurer {
@Autowired
private TestInterceptor testInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(testInterceptor)
.addPathPatterns("/*")
.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg")
.order(1);
}
}
上述代码中,addInterceptor 办法用于注册 Interceptor;addPathPatterns 办法用于设置拦挡规定;excludePathPatterns 办法用于设置放行规定,order 办法用于设置 Interceptor 的优先级,数字越小优先级越高。
测试 Interceptor
上面咱们通过一个简略的 Web 服务,来测试 Interceptor 是否失效:
@RestController
public class UserController {@RequestMapping(path = "/hello", method = RequestMethod.GET)
public String sayHello() {System.out.println("正在解决申请...");
System.out.println("申请解决实现~");
return "I'm fine, thank you.";
}
}
启动我的项目,在浏览器中拜访 localhost:8080/hello
,申请解决实现后,控制台打印了如下信息:
能够看到,Interceptor 胜利拦挡到了拜访 Controller 的 /hello
申请和拜访动态资源的 /favicon.ico
申请,并在申请解决前后执行了相应的解决逻辑。
当须要设置多个 Interceptor 时,能够间接在配置类中增加 Interceptor 的配置规定,例如减少 TestInterceptor2:
@Configuration
public class TestInterceptorConfig implements WebMvcConfigurer {
@Autowired
private TestInterceptor testInterceptor;
@Autowired
private TestInterceptor2 testInterceptor2;
@Override
public void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(testInterceptor)
.addPathPatterns("/*")
.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg")
.order(1);
registry.addInterceptor(testInterceptor2)
.addPathPatterns("/*")
.excludePathPatterns("/**/*.css", "/**/*.js", "/**/*.png", "/**/*.jpg", "/**/*.jpeg")
.order(2);
}
}
Interceptor 的执行程序由其配置的 order 决定,order 越小越先执行,留神这里指的是 preHandle 办法的执行程序,postHandle 和 afterCompletion 的执行程序与 preHandle 相同,例如在上述示例中,执行程序为:
如果咱们不配置 order,那么 Spring 默认将 order 设置为 0(能够查看 InterceptorRegistration 类的源码)。如果不同 Interceptor 具备雷同的 order,那么其执行程序为配置类中的注册程序。
Interceptor 的利用场景
Interceptor 的利用场能够参考上文中介绍的 Filter 的利用场景,能够说 Filter 能做到的事 Interceptor 都能做。因为 Filter 在 Servlet 前后起作用,而 Interceptor 能够在 Controller 办法前后起作用,例如操作 Controller 解决后的 ModelAndView,因而 Interceptor 更加灵便,在 Spring 我的项目中,如果能应用 Interceptor 的话尽量应用 Interceptor。
Filter 和 Interceptor 的区别
Filter 和 Interceptor 都是 AOP 编程思维的提现,且都能实现权限查看、日志记录等性能,但二者也有许多不同之处:
1. 标准不同
Filter 在 Servlet 标准中定义,依赖于 Servlet 容器(如 Tomcat);Interceptor 由 Spring 定义,依赖于 Spring 容器(IoC 容器)。
2. 适用范围不同
Filter 仅可用于 Web 程序,因为其依赖于 Servlet 容器;Interceptor 不仅能够用于 Web 程序,还能够用于 Application、Swing 等程序。
3. 实现原理不同
Filter 是基于函数回调来实现的,Interceptor 则是基于 Java 的反射机制(动静代理)来实现的。
下文中咱们重点介绍一下 Filter 的回调机制。
4. 触发机会不同
Filter 在申请进入 Servlet 容器,且达到 Servlet 之前对申请做预处理;在 Servlet 解决完申请后对响应做后处理。
Interceptor 在申请进入 Servlet,且达到 Controller 之前对申请做预处理;在 Controller 解决完申请后对 ModelAndView 做后处理,在视图渲染实现后再做一些收尾工作。
下图展现了二者的触发机会:
当 Filter 和 Interceptor 同时存在时,Filter 对申请的预处理要先于 Interceptor 的 preHandle 办法;Filter 对响应的后处理要后于 Interceptor 的 postHandle 办法和 afterCompletion 办法。
对于 Filter 和 Interceptor 的补充阐明
1. Filter 的回调机制
在介绍 Filter 的回调机制之前,咱们先理解一下回调函数的概念。如果将函数(C++ 中的函数指针,Java 中的匿名函数、办法援用等)作为参数传递给主办法,那么这个函数就称为回调函数,主办法会在某一时刻调用回调函数。
为了便于辨别,咱们应用 “ 主办法 ” 和 “ 函数 ” 来分辨主函数和回调函数。
应用回调函数的益处是可能实现函数逻辑的解耦,主办法内能够定义通用的解决逻辑,局部特定的操作则交给回调函数来实现。例如 Java 中 Arrays 类的 sort(T[] a, Comparator<? super T> c)
办法容许咱们传入一个比拟器来自定义排序规定,这个比拟器的 compare 办法就属于回调函数,sort 办法会在排序时调用 compare 办法。
接下来介绍 Filter 的回调机制,上文中提到,咱们自定义的 xxFilter 类须要实现 Filter 接口,且须要重写 doFilter 办法:
public class TestFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// ...
filterChain.doFilter(servletRequest, servletResponse);
}
}
Filter 接口的 doFilter 办法接管一个 FilterChain 类型的参数,这个 FilterChain 对象可认为是传递给 doFilter 办法的回调函数,严格来说应该是这个 FilterChain 对象的 doFilter 办法,留神这里提到了两个 doFilter 办法。Filter 接口的 doFilter 办法在执行完结或执行完某些步骤后会调用 FilterChain 对象的 doFilter 办法,即调用回调函数。
FilterChain 对象的理论类型为 ApplicationFilterChain,其 doFilter() 办法的解决逻辑如下(省略局部代码):
public final class ApplicationFilterChain implements FilterChain {
@Override
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
// ...
internalDoFilter(request,response);
}
private void internalDoFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {if (pos < n) {
// 获取第 pos 个 filter, 即 xxFilter
ApplicationFilterConfig filterConfig = filters[pos++];
Filter filter = filterConfig.getFilter();
// ...
// 调用 xxFilter 的 doFilter 办法
filter.doFilter(request, response, this);
}
}
}
可见,ApplicationFilterChain 的 doFilter 办法首先依据索引查问到咱们定义的 xxFilter,而后调用 xxFilter 的 doFilter 办法,在调用时,ApplicationFilterChain 会将本人作为参数传递进去。xxFilter 的 doFilter 办法执行完某些步骤后,会调用回调函数,即 ApplicationFilterChain 的 doFilter 办法,这样 ApplicationFilterChain 就能够获取到下一个 xxFilter,并调用下一个 xxFilter 的 doFilter 办法,如此循环上来,直到所有的 xxFilter 全副被调用。整个流程如下图所示:
xxFilter 执行回调函数的过程就像是给了 ApplicationFilterChain 一个告诉,即告诉 ApplicationFilterChain 能够执行下一个 xxFilter 的解决逻辑了。
2. 在 Filter 和 Interceptor 注入 Bean 的注意事项
有些文章在介绍 Filter 和 Interceptor 的区别时强调 Filter 不能通过 IoC 注入 Bean,如果咱们采纳本文中的第一种创立 Filter,那么的确不能注入胜利:
// 自定义的 Filter, 未增加 @Component 注解
public class TestFilter implements Filter {
@Autowired
private UserService userService;
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {HttpServletRequest request = (HttpServletRequest) servletRequest;
System.out.println(userService);
filterChain.doFilter(servletRequest, servletResponse);
}
// ...
}
// 配置类
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<TestFilter> registryFilter() {FilterRegistrationBean<TestFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new TestFilter());
registration.addUrlPatterns("/*");
registration.setName("TestFilter");
registration.setOrder(0);
return registration;
}
}
上述代码执行后,userService 输入为 null,因为注册到 IoC 容器中的是 new 进去的一个 TestFilter 对象(registration.setFilter(new TestFilter());
),并不是 Spring 主动拆卸的。若要使 userService 注入胜利,可改为如下写法:
// 自定义的 Filter, 未增加 @Component 注解
@Component
public class TestFilter implements Filter {
@Autowired
private UserService userService;
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {HttpServletRequest request = (HttpServletRequest) servletRequest;
System.out.println(userService);
filterChain.doFilter(servletRequest, servletResponse);
}
// ...
}
// 配置类
@Configuration
public class FilterConfig {
@Autowired
private TestFilter testFilter;
@Bean
public FilterRegistrationBean<TestFilter> registryFilter() {FilterRegistrationBean<TestFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(testFilter);
registration.addUrlPatterns("/*");
registration.setName("TestFilter");
registration.setOrder(0);
return registration;
}
}
与第一种写法的区别在于,TestFilter 类上增加了 @Component 注解,且配置类中通过 @Autowired 注入 TestFilter 对象。除了应用配置类外,本文介绍的其它几种形式(增加 @Component 注解或 @WebFilter 注解)都能够间接注入 Bean。
所以还是采纳继承 OncePerRequestFilter 的形式创立 Filter 比拟不便。
另外,应用本文介绍的创立 Interceptor 的写法是能够间接注入 Bean 的,该写法也是先在自定义的 Interceptor 上增加 @Component 注解,而后在配置类中应用 @Autowired 注入自定义的 Interceptor。
3. Interceptor 拦挡动态申请
有文章提到 Interceptor 不能拦挡动态申请,其实在 Spring 1.x 的版本中的确是这样的,但 Spring 2.x 对动态资源也进行了拦挡,例如上文中咱们在测试 TestInterceptor 是否失效时,发现其拦挡到了 /favicon.ico
申请,该申请是一个由浏览器主动发送的动态申请。
参考资料
书籍:《Java web 整合开发王者归来》
Spring Boot 实战:拦截器与过滤器
过滤器和拦截器的 6 个区别,别再傻傻分不清了