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(); } }

结束。


__EOF__

本文作者ZOLMK
本文链接https://www.cnblogs.com/zolmk/p/16586276.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   zolmk  阅读(2421)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示