首页 > 其他分享 >SpringBoot 过滤器和拦截器---实现全局接口日志输出

SpringBoot 过滤器和拦截器---实现全局接口日志输出

时间:2022-08-14 20:57:57浏览次数:67  
标签:拦截器 SpringBoot void throws --- Override import servlet public

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

结束。

标签:拦截器,SpringBoot,void,throws,---,Override,import,servlet,public
From: https://www.cnblogs.com/zolmk/p/16586276.html

相关文章

  • 3、构建实时数据仓库-ods和dim层构建
    3、构建实时数据仓库项目平台搭建架构及其总体流程1、flink整合hive的catalog因为本项目中的对应kafka中的表都存在hive的元数据库中,所以需要创建一个hive的catalo......
  • K-D Tree
    \(k-DTree\)是一种可以高效处理\(k\)维空间信息的数据结构。在结点数远大于\(k\)时,应用\(k-DTree\)的时间效率很好。——OIWiki对于建树,主流写法是平衡树......
  • 环境搭建: Vue3+Echarts5+vue-eharts + 移动端rem适配
    对于数据可视化的最后一站,就是移动数据报表的展示,毕竟手机端的适普性,便携性,灵活性更高.包括我自己也是更多在移动端进行轻量办公.而用主流的商业BI平台在PC端的......
  • 数位DP-2出现的次数
    问题描述编写一个方法,计算从0到n(含n)中数字2出现的次数。示例:输入:25输出:9解释:(2,12,20,21,22,23,24,25)(注意22应该算作两次)提示:n<=10......
  • CodeForces-1702G Passable Paths
    PassablePathsLCA在树上找到形容一条链,只用找到链的两个端点即可,因此这题的初始想法就是找端点第一个端点:深度最深的地方第二个端点:离第一个端点最远的那个点找到两......
  • 根据时间戳格格式化字符转(dddd-mm-dd )
    exportfunctiongetDateTime(value){varb=newDate(value);varyear=b.getFullYear()+'-';varmonth=(b.getMonth()+1);vardate=b.getDat......
  • Django-rest-framework开发api接口
    django-rest-framework开发api接口(1)创建django项目drfdemo1并且创建一个名为app的应用django-adminstartprojectdrfdemo1pythonmanage.pystartappapp(2)安......
  • SpringBoot-----SpringBoot @Conditional注解自动配置报告
    一、@Conditional简介@Conditional是Spring4新提供的注解,它的作用是按照一定的条件进行判断,满足条件给容器注册Bean。SpringBoot是根据配置文件的内容决定是否创建Bean,以......
  • 洛谷P6812「MCOI-02」Ancestor 先辈
    洛谷P6812对于题目的区间加法明显可以用线段树或树状数组进行并且由题可得,先辈序列即为不下降序列,需满足ai<aj&&i<j判断一个序列是否为先辈我们比较的是一个元素和前一......
  • 洛谷 P6789 - 寒妖王(子集卷积+矩阵树定理)
    洛谷题面传送门像极了我验的那道牛客多校(第六场CForest)……考虑对于每条边,计算其在最大生成基环森林中的概率,乘以边权求和就是答案。现在问题在于如何计算每条边在最大......