在 Web 开发中,拦截请求参数是一个常见需求,特别是在需要记录、验证或处理请求的场景中。本文将详细解析通过拦截器、包装器和过滤器来获取 Controller 请求参数的实现方式,并介绍关键细节与注意事项。
1. 背景
- 现代 Web 应用中,通常需要记录 Controller 接口的请求日志。对于 HTTP 请求,参数可能包括:
URL 路径
请求头信息
请求体(body)
- 其中,body 数据尤其重要,但由于 HTTP 请求体只能读取一次的限制(流会被消费),读取请求体需要特殊处理。
2. 主要实现类
- 本文代码实现分为三个关键部分:
- 拦截器(ControllerLogInterceptor):切面拦截请求日志。
- 请求包装器(BodyReaderHttpServletRequestWrapper):解决请求体只能读取一次的问题。
- 过滤器(BodyFilter):确保所有请求都能通过包装器读取多次。
2.1 拦截器:ControllerLogInterceptor
- 通过 Spring AOP 实现切面,拦截带有 @RestController 或 @Controller 注解的类。
- 核心代码解析:
@Aspect
public class ControllerLogInterceptor {
private static final Logger LOGGER = LoggerFactory.getLogger(ControllerLogInterceptor.class);
public ControllerLogInterceptor() {
}
@Pointcut("@within(org.springframework.web.bind.annotation.RestController)" +
"||@within(org.springframework.stereotype.Controller)")
private void businessLog() {
}
/**
* 日志获取切面
*
* @param pjp 切点
* @return 日志数据
* @throws Throwable 异常
*/
@Around("businessLog()")
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
// controller 参数
Object[] args = pjp.getArgs();
// 获取请求方法参数,目前只记录 HTTP 请求
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
LOGGER.debug("非 HTTP 请求跳过拦截");
return pjp.proceed(args);
}
// 获取请求路径
HttpServletRequest request = attributes.getRequest();
String apiCode = request.getServletPath();
// 获取请求body
Object requestBody = null;
String bodyStr = null;
if ((request instanceof BodyReaderHttpServletRequestWrapper)) {
BodyReaderHttpServletRequestWrapper requestWrapper = (BodyReaderHttpServletRequestWrapper) request;
// 请求 body
bodyStr = requestWrapper.getBodyString();
}
if (StringUtils.hasText(bodyStr)) {
// 目前只支持 json body
requestBody = JSON.parse(bodyStr);
} else if (args != null) {
requestBody = parseArgArray(args);
}
return requestBody;
}
private Object parseArgArray(Object[] args) {
// 解析 controller 接口方法的参数列表
Object[] filterArray = Arrays.stream(args)
.filter(arg -> !(arg instanceof HttpServletRequest || arg instanceof HttpServletResponse)).toArray();
return filterArray.length > 1 ? JSON.toJSON(args) : JSON.toJSON(args[0]);
}
}
- 功能细节
- 切入点声明:通过
@Pointcut定义拦截规则,仅拦截@RestController 或 @Controller注解的类。 - 读取请求体:利用自定义包装器,确保请求体可多次读取。
- 日志记录:记录请求路径和请求体(支持 JSON)。
- 当请求体为空时,通过 parseArgArray 提取方法参数。过滤掉 HttpServletRequest 和 HttpServletResponse 等不必要的对象。
- 单参数优化:如果参数列表只有一个,直接序列化该参数,简化记录内容。
2.2 请求包装器:BodyReaderHttpServletRequestWrapper
- 为了解决请求体只能读取一次的问题,自定义包装器类重写
HttpServletRequestWrapper。
public class BodyReaderHttpServletRequestWrapper extends HttpServletRequestWrapper {
private final byte[] body;
public BodyReaderHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
this.body = getBodyString(request).getBytes(StandardCharsets.UTF_8);
}
public String getBodyString() {
return new String(body, StandardCharsets.UTF_8);
}
/**
* 获取请求Body
*
* @param request 请求
* @return {@link String}
*/
private String getBodyString(final ServletRequest request) {
StringBuilder sb = new StringBuilder();
InputStream inputStream = null;
BufferedReader reader = null;
try {
inputStream = cloneInputStream(request.getInputStream());
reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
} catch (IOException e) {
LogUtils.error(e.getMessage(), e);
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
LogUtils.error(e.getMessage(), e);
}
}
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
LogUtils.error(e.getMessage(), e);
}
}
}
return sb.toString();
}
/**
* 复制输入流
*
* @param inputStream 输入流
* @return {@link InputStream}
*/
public InputStream cloneInputStream(ServletInputStream inputStream) {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
try {
while ((len = inputStream.read(buffer)) > -1) {
byteArrayOutputStream.write(buffer, 0, len);
}
byteArrayOutputStream.flush();
} catch (IOException e) {
LogUtils.error(e.getMessage(), e);
}
return new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
}
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
@Override
public ServletInputStream getInputStream() {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public int read() {
return byteArrayInputStream.read();
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
};
}
}
- 功能细节
- 缓存请求体:在构造函数中读取并缓存请求体。
- 多次读取支持:重写 getInputStream 和 getReader,返回缓存流。
2.3 过滤器:BodyFilter
- 将所有非文件上传的 HTTP 请求包装为
BodyReaderHttpServletRequestWrapper。
public class BodyFilter implements Filter {
private static final String MULTIPART = "multipart";
private static final String X_WWW_FORM_URLENCODED = "application/x-www-form-urlencoded";
@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 {
HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
// 判断是否为文件上传
if (isFileApi(httpRequest)) {
filterChain.doFilter(servletRequest, servletResponse);
return;
}
servletRequest = new BodyReaderHttpServletRequestWrapper(httpRequest);
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
Filter.super.destroy();
}
/**
* 判断是否为文件上传接口
*
* @param request 请求
* @return 是否为文件上传接口 boolean
*/
private boolean isFileApi(HttpServletRequest request) {
String contentType = request.getContentType();
if (StringUtils.hasText(contentType) && contentType.contains(MULTIPART)) {
return true;
}
if (StringUtils.hasText(contentType) && contentType.contains(X_WWW_FORM_URLENCODED)) {
return true;
}
return false;
}
}
- 功能细节
- 文件上传排除:通过 isFileApi 判断是否为文件上传请求。
- 包装请求:对非文件请求进行包装,确保请求体可重复读取。
3. 实现细节与注意事项
- 请求体读取限制:HttpServletRequest 的请求体只能读取一次,使用包装器可解决此问题。
- JSON 解析:当前实现仅支持解析 JSON 格式的请求体,其他格式需扩展。
- 性能考虑:对于大体积请求体,缓存可能会占用更多内存,需根据业务场景权衡。
- 文件上传排除:文件流的读取与普通请求体不同,需单独处理。
4. 总结
通过拦截器、包装器和过滤器的组合,实现了对 Controller 请求参数的完整拦截与日志记录。该方案兼顾了可读性与可扩展性,为 Web 应用的日志管理和参数处理提供了有效支持。