步骤
-
实现HandlerInterceptor接口;或继承HandlerInterceptorAdapter类;
-
实现WebMvcConfigurer接口,在addInterceptors方法中注册拦截器
说明
-
HandlerInterceptorAdapter类实现了AsyncHandlerInterceptor接口,该接口继承自HandlerInterceptor接口,比HandlerInterceptor多了一个afterConcurrentHandlingStarted方法
-
方法执行顺序:
-
preHandle在执行controller方法前执行,当preHandle返回为true时,继续往下执行,否则返回
-
postHandle在执行controller方法后,返回结果前执行;具体是在渲染视图前,参数有一个ModelAndVie 对象,可以对试图进行处理
-
afterCompletion在整个请求流程处理完成后执行,可以用于记录接口耗时等
-
如果controller方法的返回值是java.util.concurrent.Callable类型,执行顺序是:preHandle -> controller方法 -> afterConcurrentHandlingStarted, 用一个新的线程执行 preHandle -> postHandle -> afterCompletion
-
-
如果有多个拦截器,执行顺序由注册顺序决定(除了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;
}