彻底搞懂过滤器、拦截器、监听器、AOP,看这一篇就足够了:https://blog.csdn.net/xiaorui51/article/details/108753111
SpringBoot 过滤器、拦截器、监听器对比及使用场景:https://blog.csdn.net/qq_38020915/article/details/116431612
一、关系图

二、Servlet
1、定义概念
在Java程序员10年以前做Web开发的时候,所有的请求都是由Servlet来接受并响应的。每一个请求,就要写一个Servlet。这种方式很麻烦,大家就想能不能根据请求的路径以及参数不同,映射到不同的方法上去执行,这样就可以在一个Servlet类里面处理多个请求,每个请求就是一个方法。这个思想后来就逐渐发展为Structs、SpringMVC等框架。
2、使用场景
目前来看,Servlet使用的场景已经被springMVC封装架构全面覆盖,几乎没有什么需要使用原始Servlet进行开发的场景。但是不排除,老项目向SpringBoot项目迁移融合,需要支持Servlet的情况。
3、代码实现
下面我们就看一下,在SpringBoot里面如何实现Servlet
// 标记为servlet,以便启动器扫描。
@WebServlet(name = "firstServlet", urlPatterns = "/firstServlet")
public class FirstServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().append("firstServlet");
}
}
然后在启动类加入@ServletComponentScan注解即可。
@ServletComponentScan
@SpringBootApplication
public class XyzApplication {
public static void main(String[] args) {
SpringApplication.run(XyzApplication.class, args);
}
}
三、过滤器
- 滤器是在web应用启动的时候初始化一次, 在web应用停止的时候销毁
- 可以对请求的URL进行过滤, 对敏感词过滤
- 挡在拦截器的外层
- 实现的是 javax.servlet.Filter 接口 ,是 Servlet 规范的一部分
- 在请求进入容器后,但在进入servlet之前进行预处理,请求结束是在servlet处理完以后
- 依赖Web容器
- 会多次执行
1、Filter(Servlet)
1、定义概念
Servlet 过滤器是可用于 Servlet 编程的 Java 类,有以下目的:
- 在客户端的请求访问后端资源之前,拦截这些请求。
- 在服务器的响应发送回客户端之前,处理这些响应

2、使用场景
在实际的应用开发中,我们经常使用过滤器做以下的一些事情
- 基于一定的授权逻辑,对HTTP请求进行过滤,从而保证数据访问的安全。比如:判断请求的来源IP是否在系统黑名单中
- 对于一些经过加密的HTTP请求数据,进行统一解密,方便后端资源进行业务处理
- 或者我们社交应用经常需要的敏感词过滤,也可以使用过滤器,将触发敏感词的非法请求过滤掉
过滤器主要的特点在于:一是可以过滤所有请求,二是它能够改变请求的数据内容。
3、过滤器(Filter)生命周期
Filter 有三个阶段,分别是初始化,拦截和过滤,销毁。
初始化阶段:web 应用程序启动时,web 服务器将创建 Filter 的实例对象,并调用其 init 方法,完成对象的初始化功能,从而为后续的用户请求作好拦截的准备工作,filter 对象只会创建一次,init 方法也只会执行一次。通过 init 方法的参数,可获得代表当前 filter 配置信息的 FilterConfig 对象(永远只调用一次)
拦截和过滤阶段:只要请求资源的路径和拦截的路径相同,那么过滤器就会对请求进行过滤,这个阶段在服务器运行过程中会一直循环,不管第几次,都在调用doGet(),doPost() 方法之前。
销毁阶段:当服务器(Tomcat)关闭时,Web 容器调用 destroy 方法销毁 Filter。destroy 方法在 Filter 的生命周期中仅执行一次。在 destroy 方法中,可以释放过滤器使用的资源(永远只调用一次);
4、FilterConfig 和 FilterChain 说明
FilterConfig 和 FilterChain 这2个对象是由服务器 (Tomcat) 在创建和调用 Filter 对象时所传入的,这2个对象十分有用,FilterConfig 对象可以读取我们配置的初始参数,FilterChain 可以实现多个 Filter 之间的连接。
1、FilterConfig
public interface FilterConfig {
String getFilterName(); // 获取 filter 的名称
ServletContext getServletContext(); // 获取 ServletContext
String getInitParameter(String var1); // 获取配置的初始参数的值
Enumeration<String> getInitParameterNames(); // 获取配置的所有参数名称
}
2、FilterChain
public interface FilterChain {
void doFilter(ServletRequest var1, ServletResponse var2) throws IOException, ServletException;
}
我们查看源码,可以发现 FilterChain 就只有一个方法,其实这个方法就是用来对拦截进行放行的,如果有多个拦截器,那么就会继续调用下一个 Filter 进行拦截。doFilter 方法需要传入个参数,一个是 ServletRequest,一个是 ServletResponse 参数,这个直接传入进行。
Tomcat 在调用过滤器时,默认就会传入 Request 和 Response,这个参数封装了请求和响应,我们直接使用就行。ServletResquest 和 ServletResponse 可以直接强转成 HttpServletRequest 和 HttpServletResponse,然后使用相应的方法。
5、代码实现_注册方式一:利用 @WebFilter 注解配置
Servlet3.0 新增了注解 @WebFilter,原先实现过滤器需要在 web.xml 中进行配置,现在通过此注解,启动启动时会自动扫描自动注册。
| 属性名 | 类型 | 描述 |
|---|---|---|
| filterName | String | 指定过滤器的name属性,(springbean也是用该名称),等价于< filter-name> |
| urlPatterns | String[] | 指定一组过滤器的URL匹配模式。等价于< url-pattern> |
| value | String[] | 该属性等价于urlPatterns属性,但是两个不应该同时使用 |
| sevletNames | String[] | 指定过滤器将用于哪些servlet。取值是@WebServlet中的name属性的取值,或者是web.xml中< servlet-name> |
| dispatcherTypes | DispatcherType[] | 指定一组过滤器的转发模式。具体取值包括:ASYNC、ERROR、FORWARD、INCLUDE、REQUEST,默认REQUEST |
| initParams | WebInitParam[] | 指定一组过滤器初始化参数,等价于< init-param> |
| asyncSupported | boolean | 声明过滤器是否支持异步操作模式,等价于< async-supported>标签 |
| description | String | 过滤器的描述信息,等价于< description> |
| displayName | String | 过滤器的显示名,通常配合工具使用,等价于< display-name> |
// 注册器名称为customFilter,拦截的url为所有
@Slf4j
@WebFilter(filterName="customFilter",urlPatterns={"/*"})
public class CustomFilter implements Filter{
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("filter 初始化");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
log.info("customFilter 请求处理之前----doFilter方法之前过滤请求");
// 对request、response进行一些预处理, 比如设置请求编码
// request.setCharacterEncoding("UTF-8");
// response.setCharacterEncoding("UTF-8");
// 链路 直接传给下一个过滤器
chain.doFilter(request, response);
log.info("customFilter 请求处理之后----doFilter方法之后处理响应");
}
@Override
public void destroy() {
log.info("filter 销毁");
}
}
然后在启动类加入@ServletComponentScan注解即可。
@ServletComponentScan
@SpringBootApplication
public class XyzApplication {
public static void main(String[] args) {
SpringApplication.run(XyzApplication.class, args);
}
}
使用这种方法当注册多个过滤器时,无法指定过滤器的先后执行顺序。原本使用web.xml配置过滤器时,是可指定执行顺序的,但使用@WebFilter时,没有这个配置属性的(需要配合@Order进行),所以接下来介绍下通过FilterRegistrationBean进行过滤器的注册。
小技巧:通过过滤器的Java类名称,进行顺序的约定,比如LogFilter和AuthFilter,此时AuthFilter就会比LogFilter先执行,因为首字母A比L前面。
6、代码实现_注册方式二:FilterRegistrationBean 方式配置
FilterRegistrationBean 是 SpringBoot 提供的,此类提供 setOrder 方法,可以为 filter 设置排序值,让 Spring 在注册 Web filter 之前排序后再依次注册。首先要改写 filter, 其实就删掉 @WebFilter 注解即可,其他的都没有变化。然后的代码是 Filter 的注册代码
@Configuration
public class FilterRegistration {
@Bean
public FilterRegistrationBean filterRegistrationBean() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new CustomFilter()); // Filter可以new,也可以使用依赖注入Bean
registration.setName("customFilter"); // 过滤器名称
registration.addUrlPatterns("/*"); // 拦截路径
registration.setOrder(10); // 设置优先级 ,数字越小,优先级越高
return registration;
}
}
注册多个时,就注册多个FilterRegistrationBean即可,启动后,效果和第一种是一样的。可以访问应用内的任意资源进行过滤器测试,因为过滤器是针对所有的请求和响应。可以输入Filter中的log信息。
7、Filter 执行顺序设置
SptingBoot 过滤器 Filter 的使用方式:https://blog.csdn.net/JokerLJG/article/details/127634366
2、HttpServletRequestWrapper(Servlet)
1、产生背景
HttpServletRequest 不能对前端传来的参数进行修改,但实际场所像过滤XSS攻击,取认证token统一去除Token前缀等需要进行请求参数的处理,动态增加httpHeader参数,对特殊参数进行处理等操作,此时 HttpServletRequestWrapper 就应运而生了。HttpServletRequestWrapper:在请求到达之前对 request 进行修改
2、实现原理
HttpServletRequestWrapper 采用装饰者模式对 HttpServletRequest 进行包装,我们可以通过继承 HttpServletRequestWrapper 类去重写getParameterValues(),getParameter() 等方法,实际还是调用 HttpServletRequest 的相对应方法,但是可以对方法的结果进行改装。
3、代码实现1 处理 SprngBoot MVC 参数
1、新增 wrapper 类及 filter 类
public class MyFilter implements Filter {
@Override
public void doFilter(ServletRequest request,
ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
chain.doFilter(new MyHttpServletRequestWrapper(req), resp);
}
}
class MyHttpServletRequestWrapper extends HttpServletRequestWrapper {
/**
* @param request
*/
public MyHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
}
// 只有直接调用request.getParameter()时才会触发, SpringBoot MVC正常接收参数只会进入getParameterValues()
@Override
public String getParameter(String name) {
System.out.println("=============getParameter()=============");
return super.getParameter(name);
}
// SpringBoot MVC 非@RequestBody参数都会进入此方法
@Override
public String[] getParameterValues(String name) {
System.out.println("=============getParameterValues()=============");
if (name.equals("token")){
return new String[]{"======xxx======"};
}
String[] parameterValues = super.getParameterValues(name);
return parameterValues;
}
}
2、新增配置filter配置类注册filter
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean MyFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setDispatcherTypes(DispatcherType.REQUEST);
registration.setFilter(new MyFilter());
registration.addUrlPatterns("/*");
registration.setName("MyFilter");
registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE);
return registration;
}
}
3、新建Controller
@RestController
@RequestMapping("wrapper")
public class RequestWrapperController {
@RequestMapping("testWrapper")
public void testWrapper(String name, String token) {
log.info("----------- name :{} -------------", name);
log.info("----------- token :{} -------------", token);
}
}
4、访问API(GET):localhost:8080/wrapper/testWrapper?name=zhangsan
----------- name :zhangsan -------------
----------- token :======xxx====== -------------
4、代码实现2 解决流只能读取一次的问题
对于前端传来的 JSON 数据,我们在服务端基本上都是通过 IO 流来解析,如果是古老的 Servlet,那么我们直接解析 IO 流;如果是在 SpringMVC 中,我们往往通过 @RequestBody 注解来解析。如果通过 IO 流来解析参数,默认情况下,IO 流读一次就结束了,就没有了。而往往有些场景,需要我们多次读取参数。
// 如果在过滤器中读取过一次request.getReader().readLine(),会导致controller获取不到参数报错:
java.lang.IllegalStateException: getReader() has already been called for this request
1、创建 wrapper 类及 filter 类
@Slf4j
public class MyFilter implements Filter {
@Override
public void doFilter(ServletRequest request,
ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
MyHttpServletRequestWrapper myRequest = new MyHttpServletRequestWrapper(req, resp);
chain.doFilter(myRequest, resp);
System.out.println("测试读取流: ");
System.out.println(StreamUtils.copyToString(myRequest.getInputStream(), StandardCharsets.UTF_8));
}
}
/**
* 首先在构造的时候,就通过 IO 流将数据读取出来并存入到一个 byte 数组中,
* 然后重写 getReader 和 getInputStream 方法,在这两个读取 IO 流的方法中,
* 都从 byte 数组中返回 IO 流数据出来,这样就实现了反复读取了。
* 接下来定义一个过滤器,让这个装饰后的 Request 生效
*/
class MyHttpServletRequestWrapper extends HttpServletRequestWrapper {
private final byte[] requestBody;
public MyHttpServletRequestWrapper(HttpServletRequest request,
ServletResponse response) throws IOException {
super(request);
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
requestBody = StreamUtils.copyToByteArray(request.getInputStream());
}
@Override
public ServletInputStream getInputStream() {
final ByteArrayInputStream bais = new ByteArrayInputStream(requestBody);
return new ServletInputStream() {
@Override
public int read() {
return bais.read();
}
@Override
public int available() {
return requestBody.length;
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
};
}
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
}
2、新增配置filter配置类注册filter
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean MyFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setDispatcherTypes(DispatcherType.REQUEST);
registration.setFilter(new MyFilter());
registration.addUrlPatterns("/*");
registration.setName("MyFilter");
registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE);
return registration;
}
}
3、新建Controller测试
@RestController
@RequestMapping("wrapper")
public class RequestWrapperController {
@PostMapping("testWrapper")
public void testWrapperPost(@RequestBody String str) {
System.out.println(str);
}
}
4、访问API(POST):localhost:8080/wrapper/testWrapper,下面是request body参数
{"name": "Sam"}
5、打印结果如下
{"name": "Sam"}
测试读取流:
{"name": "Sam"}
5、代码实现3 动态添加RequestBody参数
在【代码实现 3】基础上进行修改:
1、创建 wrapper 类及 filter 类
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StreamUtils;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
@Slf4j
public class MyFilter implements Filter {
@Override
public void doFilter(ServletRequest request,
ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
MyHttpServletRequestWrapper myRequest = new MyHttpServletRequestWrapper(req, resp);
chain.doFilter(myRequest, resp);
// 增加和修改requestBody中的参数
myRequest.setRequestBody("city", "GZ");
System.out.println("测试读取流: " + StreamUtils.copyToString(myRequest.getInputStream(), StandardCharsets.UTF_8));
}
}
/**
* 首先在构造的时候,就通过 IO 流将数据读取出来并存入到一个 byte 数组中,
* 然后重写 getReader 和 getInputStream 方法,在这两个读取 IO 流的方法中,
* 都从 byte 数组中返回 IO 流数据出来,这样就实现了反复读取了。
* 接下来定义一个过滤器,让这个装饰后的 Request 生效
*/
class MyHttpServletRequestWrapper extends HttpServletRequestWrapper {
private byte[] requestBody;
public MyHttpServletRequestWrapper(HttpServletRequest request,
ServletResponse response) throws IOException {
super(request);
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
requestBody = StreamUtils.copyToByteArray(request.getInputStream());
}
@Override
public ServletInputStream getInputStream() {
final ByteArrayInputStream bais = new ByteArrayInputStream(requestBody);
return new ServletInputStream() {
@Override
public int read() {
return bais.read();
}
@Override
public int available() {
return requestBody.length;
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
};
}
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
/**
* 获取请求体
* @return 请求体
*/
public void setRequestBody(String k, String v) throws IOException {
ObjectMapper objectMapper = new ObjectMapper();
ObjectNode objectNode = objectMapper.readValue(requestBody, ObjectNode.class);
objectNode.put(k, v);
objectNode.put("name", "Java");
requestBody = objectMapper.writeValueAsBytes(objectNode);
}
}
2、访问API(POST):localhost:8080/wrapper/testWrapper,下面是request body参数
{"name": "Sam"}
3、打印结果如下
{"name": "Sam"}
测试读取流: {"name":"Java","city":"GZ"}
6、代码实现4 实现动态添加请求头信息
1、创建 wrapper 类及 filter 类
public class MyFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
HeaderMapRequestWrapper requestWrapper = new HeaderMapRequestWrapper(req);
// 自定义逻辑增加header
requestWrapper.addHeader("customHeader", "xxxxxx");
chain.doFilter(requestWrapper, resp);
}
class HeaderMapRequestWrapper extends HttpServletRequestWrapper {
private Map<String, String> headerMap = new HashMap<>();
/**
* construct a wrapper for this request
*/
public HeaderMapRequestWrapper(HttpServletRequest request) {
super(request);
}
/**
* add a header with given name and value
*/
public void addHeader(String name, String value) {
headerMap.put(name, value);
}
@Override
public String getHeader(String name) {
String headerValue = super.getHeader(name);
if (headerMap.containsKey(name)) {
headerValue = headerMap.get(name);
}
return headerValue;
}
@Override
public Enumeration<String> getHeaderNames() {
List<String> names = Collections.list(super.getHeaderNames());
for (String name : headerMap.keySet()) {
names.add(name);
}
return Collections.enumeration(names);
}
@Override
public Enumeration<String> getHeaders(String name) {
List<String> values = Collections.list(super.getHeaders(name));
if (headerMap.containsKey(name)) {
//values = Arrays.asList(headerMap.get(name));
values.add(headerMap.get(name));
}
return Collections.enumeration(values);
}
}
}
2、新增配置filter配置类注册filter
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean MyFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setDispatcherTypes(DispatcherType.REQUEST);
registration.setFilter(new MyFilter());
registration.addUrlPatterns("/*");
registration.setName("MyFilter");
registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE);
return registration;
}
}
3、新建Controller测试
@RestController
@RequestMapping("wrapper")
public class RequestWrapperController {
@GetMapping("testWrapper")
public void testWrapperHeader(@RequestHeader HttpHeaders headers) {
System.out.println(headers.getFirst("customHeader"));
}
}
4、访问API(GET):localhost:8080/wrapper/testWrapper
xxxxxx
7、参考文献 & 鸣谢
- HttpServletRequestWrapper 去除@RequestBody参数两端的空格:https://mp.weixin.qq.com/s/-72uNCI2nYbL9rwbigs-bQ
- HttpServletRequestWrapper 解决流只能读取一次的问题:https://blog.csdn.net/qq_43437874/article/details/122102362
- HttpServletRequestWrapper实现添加请求头:https://blog.csdn.net/weixin_43931625/article/details/104998427
- ServletRequestWrapper、ServletResponseWrapper:https://mp.weixin.qq.com/s/Y2YrjqgI-MMF9d4TeXE-Eg
3、OncePerRequestFilter(SpringBoot)
1、简单介绍
OncePerRequestFilter 类是一个实现了 javax.servlet.Filter 原生接口的抽象类。OncePerRequestFilter 可以保证一次外部请求,只执行一次过滤方法,对于服务器内部之间的 forward 等请求,不会再次执行过滤方法。OncePerRequestFilter 又称单次过滤器。
在学习”OncePerRequestFilter”前,我们先看一下过滤器是什么?
- 过滤器在WEB应用启动时初始化一次, 在WEB应用停止时销毁;
- 可以对请求的URL进行过滤, 对敏感词过滤;
- 过滤器将处理逻辑挡在拦截器的外面;
- 过滤器实现的是 javax.servlet.Filter原生接口,过滤器是SERVLET 规范的一部分;
- 过滤器在请求进入WEB容器后,到达SERVLET之前进行过滤器的逻辑处理;
- 过滤器依赖WEB容器,也会被多次执行。
- 过滤器的作用就是在业务逻辑执行前/后对请求和响应进行相应的处理,如果能配合HttpServletRequestWrapper和HttpServletResponseWrapper使用更加完美。
2、使用场景
OncePerRequestFilter 的主要目的是为了兼容不同的 WEB 容器,因为 Servlet 版本不同,执行的过程也不同,其实不是所有的容器一次请求只过滤一次。所以如果是在 SpringBoot 或者 Spring 项目中尽量使用 OncePerRequestFilter过滤器。
3、执行顺序
SpringBoot 中 OncePerRequestFilter 的执行顺序:
- 当定义了多个Filter时,OncePerRequestFilter总是在第一个Filter之后执行
- 多个OncePerRequestFilter之间也可以有@Order指定顺序
- 所有OncePerRequestFilter按顺序执行完毕之后,才执行第二个Filter
- OncePerRequestFilter也满足Filter队列的出队顺序
3、源码分析
public abstract class OncePerRequestFilter extends GenericFilterBean {
// ...
public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) {
HttpServletRequest httpRequest = (HttpServletRequest)request;
HttpServletResponse httpResponse = (HttpServletResponse)response;
// 获取是否执行过滤方法的一个属性KEY,作为是否执行过滤方法的一个标记
String alreadyFilteredAttributeName = this.getAlreadyFilteredAttributeName();
// 检测当前请求的属性中该标记的值是否为空,如果不为空则说明已执行过滤方法了
boolean hasAlreadyFilteredAttribute = request.getAttribute(alreadyFilteredAttributeName) != null;
if (!this.skipDispatch(httpRequest) && !this.shouldNotFilter(httpRequest)) {
// 已执行
if (hasAlreadyFilteredAttribute) {
if (DispatcherType.ERROR.equals(request.getDispatcherType())) {
this.doFilterNestedErrorDispatch(httpRequest, httpResponse, filterChain);
return;
}
filterChain.doFilter(request, response);
} else {
// 未执行
request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
try {
// 这个抽象方法需要子类去实现具体的过滤逻辑
this.doFilterInternal(httpRequest, httpResponse, filterChain);
} finally {
// 执行完毕移除标记
request.removeAttribute(alreadyFilteredAttributeName);
}
}
} else {
filterChain.doFilter(request, response);
}
} else {
throw new ServletException("OncePerRequestFilter just supports HTTP requests");
}
}
// ...
}
4、如何使用
package com.xyz.filter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;
/**
* 请求过滤器
* OncePerRequestFilter:顾名思义,它能够确保在一次请求中只通过一次filter.
*
* 大家常识上都认为,一次请求本来就只filter一次,为什么还要由此特别限定呢,
* 往往我们的常识和实际的实现并不真的一样,经过一番资料的查阅,此方法是为了兼容不同的web container,
* 也就是说并不是所有的container都入我们期望的只过滤一次,servlet版本不同,执行过程也不同,
* 因此,为了兼容各种不同运行环境和版本,默认自定义filter继承OncePerRequestFilter是一个比较稳妥的选择。
*/
@Slf4j
@Component
public class CustomFilter extends OncePerRequestFilter {
/**
* OncePerRequestFilter.doFilter方法中通过request.getAttribute判断当前过滤器是否已执行
* 若未执行过,则调用doFilterInternal方法,交由其子类实现
*/
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
try {
CustomRequestWrapper customRequestWrapper = new CustomRequestWrapper(httpServletRequest);
filterChain.doFilter(customRequestWrapper, httpServletResponse);
log.info("RequestFilter");
log.info(Arrays.toString(customRequestWrapper.getParameterValues("name")));
} catch (Exception exception) {
httpServletResponse.setCharacterEncoding("utf-8");
httpServletResponse.setContentType("application/json; charset=utf-8");
PrintWriter writer = httpServletResponse.getWriter();
writer.write(exception.toString());
}
}
}
@Configuration
public class FilterRegistration {
@Bean
public FilterRegistrationBean<CustomFilter> registrationBean() {
FilterRegistrationBean<CustomFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new CustomFilter()); // 注入filter对象
registrationBean.addUrlPatterns("/filter/*"); // 拦截路径
registrationBean.setName("CustomFilter"); // 设置执行顺序,数字越低优先级越高
registrationBean.setOrder(1); // 设置执行顺序,值越低优先级越高
registrationBean.setDispatcherTypes(DispatcherType.REQUEST);
return registrationBean;
}
}
番外配置:SpringBoot官方推荐的重复执行的FILTER只执行一次的解决方案:
@Configuration(proxyBeanMethods = false)
public class MyFilterConfiguration {
@Bean
public FilterRegistrationBean<MyFilter> registration(MyFilter filter) {
FilterRegistrationBean<MyFilter> registration = new FilterRegistrationBean<>(filter);
registration.setEnabled(false);
return registration;
}
}
4、DispatcherServlet(SpringMVC)
首先分析过滤器Filter的缺点:系统中各处理执行顺序为:过滤器 > 拦截器 > AOP。RequestBodyAdvice 增强器本质属于AOP,既然body的数据处理在最后,而采取过滤器这种方式又在最前边,对于我这种重度晚期强迫症患者是肯定不能忍的。
扩展前置控制器DispatcherServlet(SpringMVC):这种方式最为推荐,它的执行顺序介于过滤器和拦截器之间,是最理想的方式。
1、自定义扩展前置控制器DispatcherServlet,重写doDispatch方法
public class CustomDispatcherServlet extends DispatcherServlet {
@Override
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
/**
* 这里如果需要就加入逻辑,如果不需要则直接:super.doDispatch(request, response);
*/
HeaderMapRequestWrapper requestWrapper = new HeaderMapRequestWrapper(request);
// 自定义逻辑增加header
requestWrapper.addHeader("customHeader", "xxxxxx");
super.doDispatch(requestWrapper, response);
}
class HeaderMapRequestWrapper extends HttpServletRequestWrapper {
private Map<String, String> headerMap = new HashMap<>();
public HeaderMapRequestWrapper(HttpServletRequest request) {
super(request);
}
public void addHeader(String name, String value) {
headerMap.put(name, value);
}
@Override
public String getHeader(String name) {
String headerValue = super.getHeader(name);
if (headerMap.containsKey(name)) {
headerValue = headerMap.get(name);
}
return headerValue;
}
@Override
public Enumeration<String> getHeaderNames() {
List<String> names = Collections.list(super.getHeaderNames());
for (String name : headerMap.keySet()) {
names.add(name);
}
return Collections.enumeration(names);
}
@Override
public Enumeration<String> getHeaders(String name) {
List<String> values = Collections.list(super.getHeaders(name));
if (headerMap.containsKey(name)) {
//values = Arrays.asList(headerMap.get(name));
values.add(headerMap.get(name));
}
return Collections.enumeration(values);
}
}
}
2、注册自定义扩展前置控制器DispatcherServlet
@Configuration
public class CustomConfiguration {
/**
* 注册自定义DispatcherServlet配置
* @Qualifier:Qualifier: 的意思是合格者,通过这个标识,表明了哪个实现类才是我们所需要的,
* 添加@Qualifier注解,需要注意的是@Qualifier的参数名称为我们之前定义@Service注解的名称之一。
* 当然也可以不加此注解,直接在@Bean指明需要实现的类:
* @Bean(value = DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
*/
@Bean
@Qualifier(DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public DispatcherServlet dispatcherServlet() {
return new CustomDispatcherServlet();
}
}
3、新建Controller测试
@RestController
@RequestMapping("wrapper")
public class RequestWrapperController {
@GetMapping("testWrapper")
public void testWrapperHeader(@RequestHeader HttpHeaders headers) {
System.out.println(headers.getFirst("customHeader"));
}
}
4、访问API(GET):localhost:8080/wrapper/testWrapper
xxxxxx
四、拦截器
1、Interceptor拦截器简单介绍
在 Servlet 规范中并没有拦截器的概念,它是在Spring框架内衍生出来。
- 实现 org.springframework.web.servlet.HandlerInterceptor 接口,动态代理。拦截器应用场景, 性能分析, 权限检查, 日志记录
- Interceptor 是 Spring 组件,并由 Spring 容器管理,不依赖 Tomcat 等容器,可单独使用,不仅能应用在 Web 程序中,也可以用于Application、Swing等程序中
- 是在请求进入Servlet后,在进入Controller之前进行预处理的,Controller 中渲染了对应的视图之后请求结束

Spring中拦截器有三个方法:
- preHandle 表示被拦截的URL对应的控制层方法,执行前的自定义处理逻辑
- postHandle 表示被拦截的URL对应的控制层方法,执行后的自定义处理逻辑,此时还未将modelAndView进行页面渲染。
- afterCompletion 表示此时modelAndView已做页面渲染,执行拦截器的自定义处理。
2、拦截器与过滤器的核心区别
从请求处理的生命周期上看,拦截器Interceptor和过滤器filter的作用是类似的。过滤球能做的事情,拦截器几乎也都能做。
但是二者使用场景还是有一些区别的:
- 规范不同:Filter是在Servlet规范中定义的组件,在servlet容器内生效。而拦截器是Spring框架支持的,在Spring 上下文中生效。
- 拦截器可以获取并使用Spring IOC容器中的bean,但过滤器就不行。因为过滤器是Servlet的组件,而IOC容器的bean是Spring框架内使用,拦截器恰恰是Spring框架内衍生出来的。
- 拦截器可以访问Spring上下文值对象,如ModelAndView,过滤器不行。基于与上一点同样的原因。
- 过滤器在进入servlet容器之前处理请求,拦截器在servlet容器之内处理请求。过滤器比拦截器的粒度更大,比较适合系统级别的所有API的处理动作。比如:权限认证,Spring Security就大量的使用了过滤器。
- 拦截器相比于过滤器粒度更小,更适合分模块、分范围的统一业务逻辑处理。比如:分模块的、分业务的记录审计日志。(后面在日志的管理的那一章,我们会为介绍使用拦截器实现统一访问日志的记录)
比如:我们在 Filter 中使用注解@Autowired,注入某个Service,结果为null。因为过滤器无法使用Spring IOC容器Bean(当时这也不是绝对的,如果你在该 Filter 类上加一个@Component 注解,把 Filter 放进 Spring IOC 容器中,那么就可以注入Service使用了)
3、拦截器的实现
1、编写自定义拦截器类,此处我们用一个简单的例子让大家了解拦截器的生命周期。后面在日志的管理的那一章,我们会为介绍使用拦截器实现统一访问日志的记录的实战。
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Slf4j
@Component
public class CustomHandlerInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
log.info("preHandle:请求前调用");
User user = (User) request.getSession().getAttribute("user");
if (!ObjectUtils.isEmpty(user)) {
return true;
} else {
// 不管是转发还是重定向,必须返回false。否则出现多次提交响应的错误
redirect(request, response);
return false; // 返回 false 则请求中断
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response,
Object handler, ModelAndView modelAndView) throws Exception {
log.info("postHandle:请求后调用");
}
@Override
public void afterCompletion(HttpServletRequest req, HttpServletResponse resp, Object handler, Exception ex)
throws Exception {
log.info("afterCompletion:请求调用完成后回调方法,即在视图渲染完成后回调");
}
/*
* 对于请求是ajax请求重定向问题的处理方法
* @param request
* @param response
*
*/
public void redirect(HttpServletRequest request, HttpServletResponse response) throws IOException {
if ("XMLHttpRequest".equals(request.getHeader("X-Requested-With"))) {// ajax
// 获取当前请求的路径
response.setHeader("Access-Control-Expose-Headers", "REDIRECT,CONTENT_PATH");
// 告诉ajax我是重定向
response.setHeader("REDIRECT", "REDIRECT");
// 告诉ajax我重定向的路径
StringBuffer url = request.getRequestURL();
String contextPath = request.getContextPath();
String path = url.replace(url.indexOf(contextPath) + contextPath.length(), url.length(), "/").toString();
response.setHeader("CONTENT_PATH", path);
} else {// http
response.sendRedirect("/page/login");
}
response.getWriter().write(403);
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
}
}
2、通过实现WebMvcConfigurer 接口完成拦截器的注册。(旧版本中是继承WebMvcConfigurerAdapter注册拦截器,不过已废弃)
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;
@Configuration
public class MyWebMvcConfigurer implements WebMvcConfigurer {
@Resource
CustomHandlerInterceptor customHandlerInterceptor;
/*
* 拦截器依赖于Spring容器,此处拦截了所有,还可以对静态资源进行放行
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册拦截器拦截规则, 多个拦截器时依次添加执行顺序按添加顺序
registry.addInterceptor(customHandlerInterceptor).addPathPatterns("/*");
// 多个拦截器时也可以通过Order手动设置控制,值越小越先执行
registry.addInterceptor(customHandlerInterceptor).addPathPatterns("/v1").order(1);
registry.addInterceptor(customHandlerInterceptor).addPathPatterns("/v2").order(2);
// 此处拦截了所有,并且对静态资源进行放行
registry.addInterceptor(customHandlerInterceptor)
.addPathPatterns("/*")
.excludePathPatterns("/user/login", "/static/**");
}
/*
* 不要要写控制器即可完成页面跳转访问
* @param registry
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/v1").setViewName("v1");
registry.addViewController("/v2").setViewName("v2");
}
/**
* 自定义静态资源映射 SpringBoot 默认为我们提供了静态资源映射:
* classpath:/META-INF/resources
* classpath:/resources
* classpath:/static
* classpath:/public
* 优先级:META-INF/resources > resources > static > public
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
registry.addResourceHandler("/static/**").addResourceLocations("file:E:/static/");
}
}
3、我们在CustomHandlerInterceptor ,注入一个测试Service,结果是可以正确依赖注入并使用该Service的。
4、请求链路说明
随便请求一个API(因为我们配置的过滤器拦截器拦截所有请求),通过输出结果分析一下拦截器、过滤器中各接口函数的执行顺序。
CustomFilter : customFilter 请求处理之前----doFilter方法之前过滤请求
CustomHandlerInterceptor : preHandle:请求前调用
CustomHandlerInterceptor : postHandle:请求后调用
CustomHandlerInterceptor : afterCompletion:请求调用完成后回调方法,即在视图渲染完成后回调
CustomFilter : customFilter 请求处理之后----doFilter方法之后处理响应
请求链路调用顺序图如下所示:

5、注意事项
1、静态资源问题
SpringBoot2.x以后版本拦截器也会拦截静态资源,在配置拦截器是需要将姿态资源放行。
/* * 拦截器依赖于Spring容器,此处拦截了所有,需要对静态资源进行放行 */ @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new PageInterceptor()).addPathPatterns("/**") .excludePathPatterns("/page/login", "/user/login","/page/ajax","/static/**"); }SpringBoot2.x 自定义静态资源映射
spring: mvc: static-path-pattern: /static/**默认目录:
- classpath:/META-INF/resources
- classpath:/resources
- classpath:/static
- classpath:/public
- 优先级:META-INF/resources > resources > static > public
五、监听器
谈一谈 SpringBoot 怎么使用事件机制:https://www.codehome.vip/archives/springboot-sub-pub
1、监听器概述
什么事Web监听器?web监听器就是Servlet中特殊的类,他们能帮助开发者监听web中的特定事件,比如ServletContext、HttpSession、ServletRequest的创建和销毁;变量的创建、销毁和修改等。可以在某些动作前后增加处理,实现监控等等。web监听器的使用场景有很多,比如:
- javax.servlet.ServletContextListener:监听Servlet上下文用来初始化一些数据
- javax.servlet.http.HttpSessionListener:监听HttpSession用来获取当前在线的人数
- javax.servlet.ServletRequestListener:监听客户端请求的ServletRequest对象来获取用户的访问信息
1、事件监听的角色
首先我们要理解事件监听中需要的几个角色:事件发布者 (即事件源)、事件监听者、事件本身
2、事件监听的使用场景
为了将技术问题简单化,为大家举一个简单的例子。比如居委会发布停水通知。居委会就是事件源、停水就是事件本身、该居委会的辖区居民就是事件监听者。大家看这个例子,有这样几个特点:
- 异步处理:居委会工作人员发布通知之后,就可以去忙别的工作了,不会原地等待所有居民的反馈。
- 解耦:居委会和居民之间是解耦的,互相不干扰对方的工作状态与生活状态。
- 不规律性:对于停水的事件发生频率是不规律的,触发规则相对随机。
当你在一个系统的业务需求中,满足上面的几个特点中的2点,就应该考虑使用事件监听机制实现业务需求。当然实现事件监听机制有很的方法,比如:
- 使用消息队列中间件的发布订阅模式
- JDK自带的java.util.EventListener
- Spring环境下的实现事件发布监听的方法(本次介绍)
- 等等…
2、监听ServletContext
监听 Servlet 上下文对象可以用来初始化数据,用于缓存。 比如:
比如用户在点击某个站点的首页时,一般都会展现出首页的一些信息,而这些信息基本上或者大部分时间都保持不变的,但是这些信息都是来自数据库。如果用户的每次点击,都要从数据库中去获取数据的话,用户量少还可以接受,如果用户量非常大的话,这对数据库也是一笔很大的开销。
针对这种首页数据,大部分都不常更新的话,我们完全可以把它们缓存起来,每次用户点击的时候,我们都直接从缓存中拿,这样既可以提高首页的访问速度,又可以降低服务器的压力。如果做的更加灵活一点,可以再加个定时器,定期的来更新这个首页缓存。
1、针对这个功能,来写一个 Demo,在实际中可以完全套用该代码,来实现自己项目中的相关逻辑。首先写一个 Service,模拟一下从数据库查询数据:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
private Integer uid;
private String username;
private String pwd;
}
@Service
public class UserService {
/**
* 获取用户信息
*/
public User getUser(){
// 实际业务场景下,会从数据库中获取数据
return new User(10001,"张三","123456");
}
}
2、然后写一个监听器,实现 ApplicationListener<ContextRefreshedEvent> 接口,重写 onApplicationEvent 方法,将 ContextRefreshedEvent 对象传进去。如果我们想在加载或刷新应用上下文时,也重新刷新下我们预加载的资源,就可以通过监听 ContextRefreshedEvent 来做这样的事情。如下:
@Component
public class MyListener implements ApplicationListener<ContextRefreshedEvent> {
@Override
public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
// 首先获去application上下文
ApplicationContext applicationContext = contextRefreshedEvent.getApplicationContext();
// 获取对应的Service
UserService userService = applicationContext.getBean(UserService.class);
User user = userService.getUser();
// 获取application的域对象,将查到的信息放到application域中
ServletContext application = applicationContext.getBean(ServletContext.class);
application.setAttribute("user", user);
}
}
3、首先通过 contextRefreshedEvent 来获取 application 上下文,再通过 application 上下文来获取 UserService 这个 bean,项目中可以根据实际业务场景,也可以获取其他的 bean,然后再调用自己的业务代码获取相应的数据,最后存储到 application 域中,这样前端在请求相应数据的时候,我们就可以直接从 application 域中获取信息,减少数据库的压力。下面写一个 Controller 直接从 application 域中获取 user 信息来测试一下。
@RestController
@RequestMapping("/listener")
public class UserController {
@GetMapping("/user")
public User getUser(HttpServletRequest request){
ServletContext application = request.getServletContext();
User user = (User) application.getAttribute("user");
return user;
}
}
启动项目,在浏览器中输入 http://localhost:8080/listener/user 测试一下即可,如果正常返回 user 信息,那么说明数据已经缓存成功。不过 application 这种是缓存在内存中,对内存会有消耗。
3、监听HTTP会话Session对象
1、监听器还有一个比较常用的地方就是用来监听 session 对象,来获取在线用户数量,现在有很多开发者都有自己的网站,监听 session 来获取当前在下用户数量是个很常见的使用场景,下面来介绍一下如何来使用。
@Sl4j
@Component
public class MyHttpSessionListener implements HttpSessionListener {
public Integer count = 0; // 记录用户在线的数量
@Override
public void sessionCreated(HttpSessionEvent httpSessionEvent) {
log.info("=========新用户上线了========");
count++;
httpSessionEvent.getSession().getServletContext().setAttribute("count", count);
}
@Override
public void sessionDestroyed(HttpSessionEvent httpSessionEvent) {
log.info("==========用户下线了=========");
count--;
httpSessionEvent.getSession().getServletContext().setAttribute("count", count);
}
}
2、上述监听器需要实现 HttpSessionListener 接口,然后重写 sessionCreated 和 sessionDestroyed 方法,在 sessionCreated 方法中传递一个 HttpSessionEvent 对象,然后将当前 session 中的用户数量加1,sessionDestroyed 方法刚好相反,不再赘述。然后我们写一个 Controller 来测试一下。
@RestController
@RequestMapping("/listener")
public class HttpSessionListenerController {
@GetMapping("/total")
public String getTotalUser(HttpServletRequest request){
Integer count = (Integer) request.getSession().getServletContext().getAttribute("count");
return "当前在线人数:"+count;
}
}
3、该 Controller 中是直接获取当前 session 中的用户数量,启动服务器,在浏览器中输入 localhost:8080/listener/total 可以看到返回的结果是1,再打开一个浏览器,请求相同的地址可以看到 count 是 2 ,这没有问题。但是如果关闭一个浏览器再打开,理论上应该还是2,但是实际测试却是 3。原因是 session 销毁的方法没有执行(可以在后台控制台观察日志打印情况),当重新打开时,服务器找不到用户原来的 session,于是又重新创建了一个 session,那怎么解决该问题呢?我们可以将上面的 Controller 方法改造一下:
@RestController
@RequestMapping("/listener")
public class HttpSessionListenerController {
@GetMapping("/total")
public String getTotalUser(HttpServletRequest request, HttpServletResponse response){
Cookie cookie;
try {
cookie = new Cookie("JSESSIONID", URLEncoder.encode(request.getSession().getId(), "utf-8"));
cookie.setMaxAge(2*24*60*60); // 设置Cookie有效期
response.addCookie(cookie);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
Integer count = (Integer) request.getSession().getServletContext().getAttribute("count");
return "当前在线人数:"+count;
}
}
可以看出,该处理逻辑是让服务器记得原来那个 session,即把原来的 sessionId 记录在浏览器中,下次再打开时,把这个 sessionId 传过去,这样服务器就不会重新再创建了。重启一下服务器,在浏览器中再次测试一下,即可避免上面的问题。
4、监听客户端请求ServletRequest对象
1、使用监听器获取用户的访问信息比较简单,实现 ServletRequestListener 接口即可,然后通过 request 对象获取一些信息。如下:
@Sl4j
@Component
public class MyServletRequestListener implements ServletRequestListener {
@Override
public void requestInitialized(ServletRequestEvent servletRequestEvent) {
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequestEvent.getServletContext();
log.info("sessionId 为:", httpServletRequest.getRequestedSessionId());
log.info("request url 为", httpServletRequest.getRequestURL());
httpServletRequest.setAttribute("name", "张三");
}
@Override
public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
log.info("request end");
HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequestEvent.getServletContext();
log.info("request域中保存的name值为:", httpServletRequest.getAttribute("name"));
}
}
2、新建 Controller 测试
@RestController
@RequestMapping("/listener")
public class ServletRequestListenerController {
@GetMapping("/request")
public String getRequestInfo(HttpServletRequest httpServletRequest){
System.out.println("requestListener中的初始化数据:" + httpServletRequest.getAttribute("name"));
return "成功";
}
}
5、SpringBoot中自定义监听事件
在实际项目中,我们往往需要自定义一些事件和监听器来满足业务场景,比如在微服务中会有这样的场景:微服务 A 在处理完某个逻辑之后,需要通知微服务 B 去处理另一个逻辑,或者微服务 A 处理完某个逻辑之后,需要将数据同步到微服务 B,这种场景非常普遍,这个时候,我们可以自定义事件以及监听器来监听,一旦监听到微服务 A 中的某事件发生,就去通知微服务 B 处理对应的逻辑。
自定义监听器的实现步骤:
第一步:自定义事件。创建自定义事件类,继承 ApplicationEvent 对象
第二步:自定义监听器。创建自定义监听器类监听自定义事件,需要实现ApplicationListener接口,重写onApplicationEvent方法
第三步:设定事件触发类,使用 applicationContext.publishEvent(event) 手动触发监听事件
1、自定义事件
1、自定义事件需要继承 ApplicationEvent 对象,在事件中定义一个 User 对象来模拟数据,构造方法中将 User 对象传进来初始化
// 自定义事件: 继承自ApplicationEvent抽象类,然后定义自己的构造器
public class MyEvent extends ApplicationEvent {
// 模拟数据
private User user;
public MyEvent(Object source, User user) {
super(source);
this.user = user;
}
// 获取事件中用户信息
public User getUser(){
return user;
}
}
2、自定义事件监听器
自定义一个监听器来监听上面定义的 MyEvent 事件,自定义监听器需要实现 ApplicationListener 接口。重写 onApplicationEvent 方法,将自定义的 MyEvent 事件传进来,因为该事件中,我们定义了 User 对象(该对象在实际中就是需要处理的数据,在下文来模拟),然后就可以使用该对象的信息了。
SpringBoot 事件监听有四种方式(四种事件的实现方式监听是有序的。无论执行多少次都是这个顺序)
- 在ApplicationContext中添加监听器
- 在application.yml中配置监听器
- 使用@Component注解将监听器装载入Spring容器(推荐)
- 通过@EventListener注解实现事件监听(推荐)
方式1:创建MyListener1类,然后在SpringBoot启动类中获取ConfigurableApplicationContext上下文,装载监听
@Slf4j
public class MyListener1 implements ApplicationListener<MyEvent> {
public void onApplicationEvent(MyEvent event) {
log.info(String.format("%s监听到事件源:%s.", MyListener1.class.getName(), event.getSource()));
log.info("用户名: {}", event.getUser().getUsername());
log.info("密码: {}", event.getUser().getPwd());
}
}
@SpringBootApplication
public class BootLaunchApplication {
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(BootLaunchApplication.class, args);
// 装载监听
context.addApplicationListener(new MyListener1());
}
}
方式2:创建MyListener3类,然后在application.yml中配置监听
@Slf4j
public class MyListener3 implements ApplicationListener<MyEvent> {
public void onApplicationEvent(MyEvent event) {
log.info(String.format("%s监听到事件源:%s.", MyListener3.class.getName(), event.getSource()));
log.info("用户名: {}", event.getUser().getUsername());
log.info("密码: {}", event.getUser().getPwd());
}
}
context:
listener:
classes: com.zimug.bootlaunch.customlistener.MyListener3
方式3(推荐):创建MyListener2类,并使用@Component注解将该类装载入Spring容器中
@Slf4j
@Component
public class MyListener2 implements ApplicationListener<MyEvent> {
public void onApplicationEvent(MyEvent event) {
log.info(String.format("%s监听到事件源:%s.", MyListener2.class.getName(), event.getSource()));
log.info("用户名: {}", event.getUser().getUsername());
log.info("密码: {}", event.getUser().getPwd());
}
}
方式4(推荐):创建MyListener4类,该类无需实现ApplicationListener接口,使用@EventListener装饰具体方法
@Slf4j
@Component
public class MyListener4 {
@EventListener
public void listener(MyEvent event) {
log.info(String.format("%s监听到事件源:%s.", MyListener4.class.getName(), event.getSource()));
log.info("用户名: {}", event.getUser().getUsername());
log.info("密码: {}", event.getUser().getPwd());
}
}
3、监听手动发布的事件
1、定义好了事件和监听器之后,需要手动发布事件,这样监听器才能监听到,根据实际业务场景来触发,需要使用 applicationContext
@RestController
@RequestMapping("/listener")
public class GetRequestInfo {
@Resource
private ApplicationContext applicationContext;
@GetMapping("/publishEvent")
public String publishEvent() {
// 发布事件, new MyEvent("测试事件", new User(10001, "张三", "123456"));
MyEvent myEvent = new MyEvent(this, new User(10001, "张三", "123456"));
applicationContext.publishEvent(myEvent);
return "success";
}
}
2、访问API(GET):localhost:8080/listener/publishEvent
com.example.jpa.config.MyListener4监听到事件源:com.example.jpa.controller.GetRequestInfo@58ef9f08.
用户名: 张三
密码: 123456
六、最佳实践
参考文献:
- https://www.cnblogs.com/look-word/p/16568752.html
- SpringBoot中过滤器@WebFilter的使用以及简单介绍限流算法:https://blog.csdn.net/qq_41114884/article/details/130615162
1、打印接口耗时_过滤器方式
这种方式简单点 但是可配置性不高。注意:一定得扫描到 Spring 容器中
1、创建一个类实现 filter 接口
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
* init:该方法是对filter对象进行初始化的方法,仅在容器初始化filter对象结束后被调用一次,参数FilterConfig可以获得filter的初始化参数;
* doFilter:可以对request和response进行`<u>预处理</u>`。`其中FilterChain可以将处理后的`request和response对象传递到过滤链上的下一个资源。
* destroy():该方法在容器销毁对象前被调用。
**/
@Component
public class LogFilter implements Filter {
private static final Logger LOG = LoggerFactory.getLogger(LogFilter.class);
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
// 打印请求信息
HttpServletRequest request = (HttpServletRequest) servletRequest;
LOG.info("------------- LogFilter 开始 -------------");
LOG.info("请求地址: {} {}", request.getRequestURL().toString(), request.getMethod());
LOG.info("远程地址: {}", request.getRemoteAddr());
long startTime = System.currentTimeMillis();
filterChain.doFilter(servletRequest, servletResponse);
LOG.info("------------- LogFilter 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime);
}
}
2、新增配置filter配置类注册filter
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean MyFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setDispatcherTypes(DispatcherType.REQUEST);
registration.setFilter(new LogFilter());
registration.addUrlPatterns("/*");
registration.setName("MyFilter");
registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE);
return registration;
}
}
3、测试结果
2、打印接口耗时_拦截器方式
1、创建拦截器
/**
* 拦截器:Spring框架特有的,常用于登录校验,权限校验,请求日志打印 /login
**/
@Component
public class LogInterceptor implements HandlerInterceptor {
private static final Logger LOG = LoggerFactory.getLogger(LogInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// 打印请求信息
LOG.info("------------- LogInterceptor 开始 -------------");
LOG.info("请求地址: {} {}", request.getRequestURL().toString(), request.getMethod());
LOG.info("远程地址: {}", request.getRemoteAddr());
long startTime = System.currentTimeMillis();
request.setAttribute("requestStartTime", startTime);
return true;
}
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler,
ModelAndView modelAndView) throws Exception {
long startTime = (Long) request.getAttribute("requestStartTime");
LOG.info("------------- LogInterceptor 结束 耗时:{} ms -------------", System.currentTimeMillis() - startTime);
}
}
2、注册拦截器
@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {
@Resource
private LogInterceptor logInterceptor;
/**
* 注册拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry
.addInterceptor(logInterceptor)
.addPathPatterns("/**") // 对那些接口拦截
.excludePathPatterns("/login"); // 对哪些接机口放行
WebMvcConfigurer.super.addInterceptors(registry);
}
}
3、打印接口耗时_AOP的方式
实现思路:
- 引入aop依赖
- 自定义注解
- 定义切面,采用环绕通知
1、引入依赖
<!--aop-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2、自定义注解 LogAnnotation
/**
* 日志注解
* METHOD 方法上 type 类上
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogAnnotation {
/**
* 模块
*/
String module() default "";
/**
* 简单描述接口的作用
*/
String operation() default "";
}
3、定义切面
@Slf4j
@Aspect
@Component
public class LogAspect {
/**
* 切入点
*/
@Pointcut("@annotation(look.word.reggie.common.aop.LogAnnotation)")
public void logPointCut() {
}
/**
* 环绕通知
*
* @param point 连接点
*/
@Around("logPointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
long beginTime = System.currentTimeMillis();
//执行方法
Object result = point.proceed();
//执行时长(毫秒)
long time = System.currentTimeMillis() - beginTime;
//保存日志
recordLog(point, time);
return result;
}
private void recordLog(ProceedingJoinPoint joinPoint, long time) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// 获取当前方法
Method method = signature.getMethod();
LogAnnotation logAnnotation = method.getAnnotation(LogAnnotation.class);
log.info("=====================log start================================");
log.info("module:{}", logAnnotation.module());
log.info("operation:{}", logAnnotation.operation());
// 请求的方法名
String className = joinPoint.getTarget().getClass().getName();
String methodName = signature.getName();
log.info("request method:{}", className + "." + methodName + "()");
// 请求的参数
Object[] args = joinPoint.getArgs();
Stream<?> stream = ArrayUtils.isEmpty(args) ? Stream.empty() : Arrays.stream(args);
List<Object> logArgs = stream
.filter(arg -> (!(arg instanceof HttpServletRequest) && !(arg instanceof HttpServletResponse)))
.collect(Collectors.toList());
String params = JSON.toJSONString(logArgs);
log.info("params:{}", params);
//获取request 设置IP地址
HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
log.info("ip:{}", IpUtils.getIpAddr(request));
log.info("execute time : {} ms", time);
log.info("=====================log end================================");
}
}
4、功能测试
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 8629303@qq.com