StopWatch是Spring框架提供的一个简单而强大的计时工具类,用于精确测量代码执行时间。它可以帮助开发者快速分析程序的性能瓶颈,从而优化代码,提高程序运行效率。StopWatch基于纳秒级别的时间计算,支持多个任务的计时,并且可以方便地输出计时结果。
准备工作
请求数据重用工具(缓存请求体)
默认情况下,HttpServletRequest 的输入流 (getInputStream()) 只能被读取一次。
如果想多次读取请求体内容(例如本例中在请求处理之前记录时间,请求处理完成之后再次记录时间,计算时间差作为一次请求所需时长),就需要使用 HttpServletRequestWrapper 来封装并缓存请求体内容。
com/zibocoder/plugins/web/utils/RepeatedlyRequestWrapper.java
package com.zibocoder.plugins.web.utils;
import cn.hutool.core.io.IoUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.zibocoder.plugins.common.constant.CommonConst;
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import lombok.extern.slf4j.Slf4j;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
/**
* @description 允许请求体被多次读取
* 原生的 HttpServletRequest.getInputStream() 只能被读取一次。
* 该类通过将请求体缓存为字节数组 byte[] body,实现了多次读取的能力。
* @author zibocoder
* @date 2025/7/1 14:54:23
*/
@Slf4j
public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper {
private static final int MAX_BODY_SIZE = 1024 * 1024; // 1MB
private final byte[] body;
public RepeatedlyRequestWrapper(HttpServletRequest request, ServletResponse response) throws IOException {
super(request);
// 确保请求和响应使用 UTF-8 编码
request.setCharacterEncoding(CommonConst.UTF8);
response.setCharacterEncoding(CommonConst.UTF8);
int contentLength = request.getContentLength();
if (contentLength > MAX_BODY_SIZE) {
throw new IOException("请求体过大,超过允许的最大限制");
}
// 使用 Hutool 工具类一次性读取请求体并保存到内存中。
body = request.getInputStream() != null ? IoUtil.readBytes(request.getInputStream(), false) : new byte[0];
// 再次检查实际读取到的内容长度,防止伪造 Content-Length 或 chunked 编码下的恶意攻击
if (body.length > MAX_BODY_SIZE) {
throw new IOException("请求体过大(根据实际内容判断),超过允许的最大限制:" + body.length + " bytes");
}
if (!CommonConst.PROD_PROFILE.equals(SpringUtil.getActiveProfile())) {
log.info("缓存请求体大小: {} bytes", body.length); // 日志增强调试能力
}
}
/**
* 重写 getReader() 方法,返回缓存的请求体字节数组
* 使用 InputStreamReader 将字节数组转换为字符流,并返回 BufferedReader
*/
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
/**
* 重写 getInputStream() 方法,返回缓存的请求体字节数组
* 使用 ByteArrayInputStream 包装原始 body 字节数组,实现可重复读取的 ServletInputStream
*/
@Override
public ServletInputStream getInputStream() {
final ByteArrayInputStream bais = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public int read() {
return bais.read();
}
@Override
public int available() {
return body.length;
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
throw new UnsupportedOperationException("不支持异步读取,请勿调用 setReadListener()");
}
};
}
}
时间统计拦截器 StopWatch
com/zibocoder/plugins/web/interceptor/WebInvokeTimeInterceptor.java
package com.zibocoder.plugins.web.interceptor;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.json.JSONUtil;
import com.zibocoder.plugins.common.constant.CommonConst;
import com.zibocoder.plugins.web.utils.RepeatedlyRequestWrapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.util.StopWatch;
import org.springframework.web.servlet.HandlerInterceptor;
import java.io.BufferedReader;
import java.util.Map;
/**
* @Description 调用时间统计拦截器,统计 web 请求的调用时间
* 非 prod 环境下有效
* StopWatch 是 Spring 框架提供的一个简单而强大的计时工具类,用于精确测量代码执行时间 https://cloud.baidu.com/article/3277329
* @Author zibocoder
* @Date 2025/7/1 08:44:11
*/
@Slf4j
public class WebInvokeTimeInterceptor implements HandlerInterceptor {
// 线程安全的计时器缓存,存放 StopWatch 对象,用于统计 Web 请求的执行时间
private final static ThreadLocal<StopWatch> KEY_CACHE = new ThreadLocal<>();
/**
* 请求处理之前执行,返回 true 表示继续处理,返回 false 表示取消处理
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 非生产环境(获取当前的环境配置,当有多个环境配置时,只获取第一个)
if (!CommonConst.PROD_PROFILE.equals(SpringUtil.getActiveProfile())) {
String url = request.getMethod() + " " + request.getRequestURI();
// 打印请求参数
if (isJsonRequest(request)) { //数据类型为json的数据,通常需要从请求体中以输入流的方式读取并解析数据, 那么后续的控制器方法将无法再读取到数据
String jsonParam = "";
if (request instanceof RepeatedlyRequestWrapper) {
BufferedReader reader = request.getReader();
jsonParam = IoUtil.read(reader, true);
}
log.info("开始请求 => URL[{}],参数类型[json],参数:[{}]", url, jsonParam);
} else { //对于非 JSON 请求,直接使用 request.getParameterMap() 即可安全地读取参数,无需使用 RepeatedlyRequestWrapper, 不会受到输入流只能读取一次的限制
Map<String, String[]> parameterMap = request.getParameterMap();
if (MapUtil.isNotEmpty(parameterMap)) {
String parameters = JSONUtil.toJsonStr(parameterMap);
log.info("开始请求 => URL[{}],参数类型[param],参数:[{}]", url, parameters);
} else {
log.info("开始请求 => URL[{}],无参数", url);
}
}
StopWatch stopWatch = new StopWatch();
KEY_CACHE.set(stopWatch);
stopWatch.start();
}
return true;
}
/**
* 请求处理完成之后执行
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
StopWatch stopWatch = KEY_CACHE.get();
if (ObjectUtil.isNotNull(stopWatch)) {
stopWatch.stop();
log.info("结束请求 => URL[{}],耗时:[{}]毫秒", request.getMethod() + " " + request.getRequestURI(), stopWatch.getTotalTimeMillis());
KEY_CACHE.remove();
}
}
/**
* 判断本次请求的数据类型是否为json
*
* @param request request
* @return boolean
*/
private boolean isJsonRequest(HttpServletRequest request) {
String contentType = request.getContentType();
if (contentType != null) {
return StrUtil.startWithIgnoreCase(contentType, MediaType.APPLICATION_JSON_VALUE);
}
return false;
}
}
添加拦时间统计截器
web资源配置,时间统计拦截器添加到通用拦截器中
com/zibocoder/plugins/web/config/ResourcesConfigure.java
package com.zibocoder.plugins.web.config;
import com.zibocoder.plugins.web.interceptor.WebInvokeTimeInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @description web资源配置
* @author zibocoder
* @date 2025/7/1 15:38:30
*/
@Configuration
public class ResourcesConfigure implements WebMvcConfigurer {
/**
* 添加拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 全局访问性能拦截
registry.addInterceptor(new WebInvokeTimeInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/favicon.ico");
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
}
/**
* 跨域配置
*/
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
// 设置访问源地址
config.addAllowedOriginPattern("*");
// 设置访问源请求头
config.addAllowedHeader("*");
// 设置访问源请求方法
config.addAllowedMethod("*");
// 有效期 1800秒
config.setMaxAge(1800L);
// 添加映射路径,拦截一切请求
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
// 返回新的CorsFilter
return new CorsFilter(source);
}
}