Spring Boot中拦截器的编写

步骤

  1. 实现HandlerInterceptor接口;或继承HandlerInterceptorAdapter类;

  2. 实现WebMvcConfigurer接口,在addInterceptors方法中注册拦截器

说明

  1. HandlerInterceptorAdapter类实现了AsyncHandlerInterceptor接口,该接口继承自HandlerInterceptor接口,比HandlerInterceptor多了一个afterConcurrentHandlingStarted方法

  2. 方法执行顺序:

    1. preHandle在执行controller方法前执行,当preHandle返回为true时,继续往下执行,否则返回

    2. postHandle在执行controller方法后,返回结果前执行;具体是在渲染视图前,参数有一个ModelAndVie 对象,可以对试图进行处理

    3. afterCompletion在整个请求流程处理完成后执行,可以用于记录接口耗时等

    4. 如果controller方法的返回值是java.util.concurrent.Callable类型,执行顺序是:preHandle -> controller方法 -> afterConcurrentHandlingStarted, 用一个新的线程执行 preHandle -> postHandle -> afterCompletion

  3. 如果有多个拦截器,执行顺序由注册顺序决定(除了preHandle方法,其它方法正好跟注册顺序相反)
    如:有拦截器A和B,注册顺序为A -> B
    拦截器方法的执行顺序为:
    A.preHandle -> B.preHandle -> controller方法 -> B.postHandle -> A.postHandle -> B.afterCompletion -> A.afterCompletion
    或(继承了HandlerInterceptorAdapter类,且返回值为java.util.concurrent.Callable类型):
    A.preHandle -> B.preHandle -> controller方法 -> B.afterConcurrentHandlingStarted -> A.afterConcurrentHandlingStarted --启用另一个线程–> A.preHandle -> B.preHandle -> B.postHandle -> A.postHandle -> B.afterCompletion -> A.afterCompletion

另,如果需要在preHandle中打印请求参数,当需要从post的输入流中获取请求体时,由于输入流只能获取一次,会导致controller获取请求体时抛出异常(Required request body is missing),解决办法是继承HttpServletRequestWrapper类,重写getInputStream和getReader方法,并用Filter过滤器在Interceptor拦截器之前,将默认的ServletRequest对象替换为自定义的ServletRequest对象(HttpServletRequestWrapper实现了HttpServletRequest接口,该接口继承自ServletRequest接口)

代码

编写拦截器:

import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.lang.Nullable;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Locale;

@Slf4j
@Configuration
public class GlobalInterceptor implements HandlerInterceptor {
    private static final String[] SIZE_UNIT = {"B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB", "BB"};
    private static final ThreadLocal<Long> threadLocal = new ThreadLocal<>();

    @Autowired
    private PropertiesConfig propertiesConfig;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        threadLocal.set(System.currentTimeMillis());
        String body = getRequestBody(request);
        log.info("\n↓↓↓preHandle start↓↓↓\n接口请求信息\n请求方法:{}\nURI:{}\nQueryString:{}\nbody:\n{}\n↑↑↑preHandle end↑↑↑",
                request.getMethod(), request.getRequestURI(), request.getQueryString(), body);
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
        // 执行controller方法后,返回结果前执行
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
        log.info("\n↓↓↓afterCompletion start↓↓↓\n接口耗时情况\n请求方法:{}\nURI:{}\n用时:{}ms\n↑↑↑afterCompletion end↑↑↑",
                request.getMethod(), request.getRequestURI(), System.currentTimeMillis() - threadLocal.get());
        // 此处必须手动remove,否则会导致内存泄漏
        threadLocal.remove();
    }

    /**
     * 将输入流转换为字符串
     *
     * @param request 请求
     * @return 输入流转换后的字符串
     * @throws IOException 读取输入流异常
     */
    private String getRequestBody(HttpServletRequest request) throws IOException {
        String contentType = request.getContentType();
        if (contentType == null || !PropertiesConfig.TEXT_CONTENT_TYPES.contains(contentType.toLowerCase(Locale.ROOT))) {
            return "content-type为:" + contentType + ", 不在配置的文本请求体类型中, 不打印请求体";
        }
        if (request.getContentLengthLong() > propertiesConfig.getLogMaxBody()) {
            return "请求体大小为:" + expressSize(request.getContentLengthLong()) + ", 超出日志打印的body大小限制,不打印请求体";
        }
        ServletInputStream inputStream = request.getInputStream();
        byte[] bytes = new byte[1024];
        StringBuilder sb = new StringBuilder();
        int length = inputStream.read(bytes);
        while (length > 0) {
            sb.append(new String(bytes, 0, length, Charset.defaultCharset()));
            length = inputStream.read(bytes);
        }
        return sb.toString();
    }

    /**
     * 将字节大小转换为合适的表示单位
     * long能表达的最大字节大小为7EB
     *
     * @param size 字节大小
     * @return 转换后的字节大小
     */
    private static String expressSize(long size) {
        if (size <= 0) {
            return "0B";
        }
        int index = 0;
        while (size >= 1024) {
            size >>= 10;
            index++;
        }
        if (index >= SIZE_UNIT.length) {
            return "too big size...";
        }
        return size + SIZE_UNIT[index];
    }
}

注册拦截器:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class GlobalWebMvcConfigure implements WebMvcConfigurer {
    @Autowired
    private GlobalInterceptor globalInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(globalInterceptor)
                // 添加拦截路径
                .addPathPatterns("/**")
                // .addPathPatterns("/test/add1")
                // .addPathPatterns("/test/add2")
                // 添加不拦截的路径
                .excludePathPatterns("/test/exclude1")
                .excludePathPatterns("/test/exclude2");
    }
}

继承HttpServletRequestWrapper并重写相关方法:

import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import org.springframework.util.StreamUtils;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;

public class RepeatReadInputStreamHttpServletRequest extends HttpServletRequestWrapper {
    private byte[] bytes;

    public RepeatReadInputStreamHttpServletRequest(HttpServletRequest request) throws IOException {
        super(request);
        bytes = StreamUtils.copyToByteArray(request.getInputStream());
    }

    @Override
    public BufferedReader getReader() {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }

    @Override
    public ServletInputStream getInputStream() {
        final ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
        return new ServletInputStream() {
            @Override
            public int read() {
                return inputStream.read();
            }

            @Override
            public boolean isFinished() {
                return false;
            }

            @Override
            public boolean isReady() {
                return false;
            }

            @Override
            public void setReadListener(ReadListener readListener) {
            }
        };
    }
}

实现Filter接口,并注册拦截器:

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.support.WebApplicationContextUtils;

import java.io.IOException;
import java.util.Locale;

@Configuration
public class RequestFilter implements Filter {
    private PropertiesConfig propertiesConfig;

    /**
     * 过滤器中无法使用@Autowired进行自动注入(Filter归tomcat管理,bean归spring管理)
     *
     * @param filterConfig FilterConfig对象
     */
    @Override
    public void init(FilterConfig filterConfig) {
        ServletContext context = filterConfig.getServletContext();
        ApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(context);
        propertiesConfig = ctx.getBean(PropertiesConfig.class);
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        ServletRequest requestWrapper = null;
        if (servletRequest instanceof HttpServletRequest) {
            String contentType = servletRequest.getContentType();
            if (contentType != null && PropertiesConfig.TEXT_CONTENT_TYPES.contains(contentType.toLowerCase(Locale.ROOT))) {
                requestWrapper = new RepeatReadInputStreamHttpServletRequest((HttpServletRequest) servletRequest);
            }
        }
        if (requestWrapper == null) {
            filterChain.doFilter(servletRequest, servletResponse);
        } else {
            filterChain.doFilter(requestWrapper, servletResponse);
        }
    }

    @Bean
    public FilterRegistrationBean doFilterRegistration() {
        FilterRegistrationBean<RequestFilter> registration = new FilterRegistrationBean<RequestFilter>();
        registration.setFilter(new RequestFilter());
        registration.addUrlPatterns("/*");
        return registration;
    }
}

配置类:

import lombok.Getter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;

import java.util.Set;

@Getter
@Configuration
public class PropertiesConfig {
    /**
     * 常见的文本格式类型的body
     */
    public static final Set<String> TEXT_CONTENT_TYPES = Set.of("text/plain", "text/html", "text/xml",
            "application/xhtml+xml", "application/xml", "application/atom+xml", "application/json", "application/x-www-form-urlencoded");

    /**
     * 设置日志打印的body大小限制(默认64k)
     */
    @Value("${log.max-body:65536}")
    private long logMaxBody;
}

参考文章:

  1. Spring Boot 拦截器、过滤器、监听器

  2. springboot请求体中的流只能读取一次的问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值