SpringBoot 过滤器和拦截器---实现全局接口日志输出
SpringBoot 过滤器和拦截器---实现全局接口日志输出
首先,看一张图:
Tomcat收到请求之后,会先通过过滤器 Filter,该过滤器属于 Java Http 框架(过滤器采用接口回调的方式来运行);然后请求被发送的 Servlet,SpringBoot收到请求之后,调用拦截器 Interceptor(使用了反射机制实现),最后请求才会被发送到Controller。
一、过滤器
过滤器是容器层面的,请求到达过滤器之后,我们可以使用包装器对请求进行包装,以供后续使用。
过滤器是基于接口回调的方式实现的,我们可以在 doFilter 函数中实现一些特定功能。
该接口的示例代码如下:
public interface Filter {
// 初始化
default void init(FilterConfig filterConfig) throws ServletException {
}
// 回调函数
void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) throws IOException, ServletException;
// 销毁
default void destroy() {
}
}
Filter 的示例实现如下:
...
void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) throws IOException, ServletException {
var3.doFilter(new ServletRequestWrapper(var1),new ServletResponseWrapper(var2));
}
...
上述代码中,利用接口传入的 FilterChain 对请求对象和响应对象进行了包装,方便后续使用。例如在到达拦截器之后,我们可以将 ServletRequest case 为我们包装的对象。
接下来,忘掉上面的例子,我们重新开始,有个需求:需要在拦截器中对 RequestBody 进行二次读取,以及需要在拦截器中读取 ResponseBody。
这里有个很隐蔽的点,就是 ServletRequest 中的输入流只能读一次(是直接从 tcp 流中读取的)。因此我们要对 ServletRequest 和 ServletResponse 进行包装。
首先,我们定义两个流和两个包装器:
- CachingServletInputStream: 对 ServletInputStream 输入流进行了包装,用于缓存 RequestBody
- CachingServletOutputStream: 对 ServletOutputStream 输出流进行了包装,用来缓存 ResponseBody
- CachingRequestWrapper: 对 HttpServletRequest 进行了包装,用来缓存输入流
- CachingResponseWrapper: 对 HttpServletResponse 进行了包装,用来缓存输出流
package com.fy.aspact.filters;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import java.io.ByteArrayInputStream;
/**
* 用于对 BODY 内容的缓存,是一个字节数组输入流
* @author zhufeifei 2022/8/13
**/
public class CachingServletInputStream extends ServletInputStream {
private final ByteArrayInputStream inputStream;
public CachingServletInputStream(byte[] body) {
this.inputStream = new ByteArrayInputStream(body);
}
@Override
public boolean isFinished() {
return inputStream.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
}
@Override
public int read() {
return inputStream.read();
}
}
package com.fy.aspact.filters;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
/**
* ServletOutputStream 缓存输出流
* 在字节数组流中保存输出流中的信息
* @author zhufeifei 2022/8/14
**/
public class CachingServletOutputStream extends ServletOutputStream {
private final ByteArrayOutputStream outputStream;
private final OutputStream responseOutputStream;
public CachingServletOutputStream(OutputStream outputStream) {
super();
this.outputStream = new ByteArrayOutputStream();
this.responseOutputStream = outputStream;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setWriteListener(WriteListener writeListener) {
}
@Override
public void write(int b) throws IOException {
responseOutputStream.write(b);
outputStream.write(b);
}
public String getBody() {
return outputStream.toString();
}
@Override
public void flush() throws IOException {
responseOutputStream.flush();
}
@Override
public void write(byte[] b, int off, int len) throws IOException {
outputStream.write(b, off, len);
responseOutputStream.write(b, off, len);
}
@Override
public void close() throws IOException {
outputStream.close();
responseOutputStream.close();
super.close();
}
}
package com.fy.aspact.filters;
import org.springframework.util.StreamUtils;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
/**
* 包装ServletRequest,实现了对 RequestBody 的二次读取(打印日志读取一次,接口读取一次)
* @author zhufeifei 2022/8/13
**/
public class CachingRequestBodyWrapper extends HttpServletRequestWrapper {
/** 保存 BODY 内容 **/
private final byte[] body;
/** 缓存 BODY 的输入流,供接口读取 BODY **/
private final CachingServletInputStream inputStream;
public CachingRequestBodyWrapper(HttpServletRequest request) throws IOException {
super(request);
body = StreamUtils.copyToByteArray(request.getInputStream());
inputStream = new CachingServletInputStream(body);
}
@Override
public ServletInputStream getInputStream() throws IOException {
if (inputStream != null) {
return inputStream;
}
return super.getInputStream();
}
public String getBody() {
return new String(body, StandardCharsets.UTF_8);
}
}
package com.fy.aspact.filters;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import java.io.IOException;
/**
* @author zhufeifei 2022/8/14
**/
public class CachingResponseBodyWrapper extends HttpServletResponseWrapper {
private final CachingServletOutputStream servletOutputStream;
public CachingResponseBodyWrapper(HttpServletResponse response) throws IOException {
super(response);
servletOutputStream = new CachingServletOutputStream(response.getOutputStream());
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
if (servletOutputStream != null) {
return servletOutputStream;
}
return super.getOutputStream();
}
public String getBody() {
return servletOutputStream.getBody();
}
}
- LogFilter: 定义过滤器,使用上面的包装器
package com.fy.aspact.filters;
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;
/**
* Request 先进入过滤器,然后到达拦截器,最后才会到达接口
* @author zhufeifei 2022/8/13
**/
@Component
// 这个注解 SpringBoot 可以感知到,用来对过滤器做配置
@WebFilter(filterName = "logFilter", urlPatterns = "/*")
public class LogFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
// 在这个地方对 request response 进行包装
filterChain.doFilter(new CachingRequestBodyWrapper((HttpServletRequest) servletRequest), new CachingResponseBodyWrapper((HttpServletResponse) servletResponse));
}
@Override
public void destroy() {
Filter.super.destroy();
}
}
现在在过滤器层面,我们已经对 Request 和 Response 进行了包装,将它们包装成了 CachingRequestWrapper 和 CachingResponseWrapper 对象。
二、拦截器
拦截器在 SpringBoot 声明周期内,通过反射的方式来执行
拦截器 HandlerInterceptor 接口示例如下:
public interface HandlerInterceptor {
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return true;
}
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
}
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
}
}
- preHandle:在 Filter 之后,Controller 之前调用
- postHandle:在 Controller 接口返回之后,到达 Filter 之前调用
- afterCompletion:在请求完成之后调用
下面是实现 HandlerInterceptor 的代码:
package com.fy.aspact.interceptors;
import com.fy.aspact.filters.CachingRequestBodyWrapper;
import com.fy.aspact.filters.CachingResponseBodyWrapper;
import com.fy.threads.TaskExecutors;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
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.lang.reflect.Method;
/**
* 统一打印接口日志 拦截器
* @author zhufeifei 2022/8/13
**/
@Component
public class LogInterceptor implements HandlerInterceptor {
private final ControllerLogger logger;
public LogInterceptor(ControllerLogger controllerLogger) {
this.logger = controllerLogger;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Class<?> clazz = handlerMethod.getBeanType();
Method method = handlerMethod.getMethod();
if (isHttpMethod(method) && request instanceof CachingRequestBodyWrapper) {
// case 为我们实现的特定的包装器类型
CachingRequestBodyWrapper bodyWrapper = (CachingRequestBodyWrapper) request;
final String body = bodyWrapper.getBody().replace("\n", "").replace("\t", "");;
TaskExecutors.getExecutor().commit(() -> logger.before(body));
}
return HandlerInterceptor.super.preHandle(request, response, handler);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Class<?> clazz = handlerMethod.getBeanType();
Method method = handlerMethod.getMethod();
if (isHttpMethod(method) && response instanceof CachingResponseBodyWrapper) {
// case 为我们实现的特定的包装器类型
CachingResponseBodyWrapper wrapper = (CachingResponseBodyWrapper) response;
final String body = wrapper.getBody().replace("\n", "").replace("\t", "");;
TaskExecutors.getExecutor().commit(() -> logger.after(body));
}
}
/**
* 判断是否为 http 中的 post get 请求
* @param method 方法
* @return boolean
*/
private boolean isHttpMethod(Method method) {
return method.isAnnotationPresent(PostMapping.class)
|| method.isAnnotationPresent(GetMapping.class)
|| method.isAnnotationPresent(RequestMapping.class);
}
}
上述代码中我们自动注入了一个专门的 ControllerLogger 接口,该接口代码如下:
package com.fy.aspact.interceptors;
/**
* @author zhufeifei 2022/8/13
**/
public interface ControllerLogger {
void before(String requestBody);
void after(String response);
}
通过实现该接口,并且将其注入为 SpringBoot 组建,可以实现自定义日志输出。
配置拦截器
我们实现 WebMvcConfigurer 接口来对拦截器进行配置,代码如下:
package com.fy.aspact.interceptors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* SpringMVC 配置器
* @author zhufeifei 2022/8/13
**/
@Configuration
public class MyWebMvcConfigurer implements WebMvcConfigurer {
@Autowired
@Lazy
private ControllerLogger logger;
public MyWebMvcConfigurer() {
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LogInterceptor(logger))
.addPathPatterns("/*")
.order(0);
}
@Bean
@ConditionalOnMissingBean
public ControllerLogger controllerLogger() {
return new SimpleControllerLogger();
}
}
结束。