背景
近期在对系统排除报错比较多的地方,并进行优化,期间发现有一个错误频繁出现,
排查过程
根据报错信息,找到接收参数的DTO类,一共有两个接口引用。原本应该是List<Long>
类型的参数,却一直接收到错误的String
类型的参数,导致无法排查是哪个接口所以一开始排查的思路:是前端的请求有问题。
故而就找了前端的同事进行了初步的排查,发现果然有一个接口,当数据为空的时候,直接传了空字符串,会导致报错。发现后,前端同事着手进行了修复。
持续报错
按理来说,前端已经修复了这个bug,这个错误依然在持续输出,该接口又是需要登录的用户才能进行请求,那就只有一个可能,我们站点的用户,利用自己的token,通过api接口进行请求,而不是通过页面交互进行请求。
又由于前面提到的问题,无法定位是哪个接口报错,毕竟是在参数解析映射的时候就已经报错,错误拦截日志又没有针对性的打印请求的uri和请求的完整参数。所以需要我们在对参数进行解析映射为DTO类之前就进行拦截打印。
实现思路
spring切面AOP
一开始使用切面aop,希望能在这之前进行拦截:代码如下:
package com.example.methodtest.reqeustparamlogger.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 请求前置处理日志注解
*
* @author AaronCgt
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestParamLogger {
}
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class ParameterAspect {
@Before("@annotation(RequestParamLogger)") // 拦截带有 @RequestParamLogger 注解的方法
public void beforeMethod(JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
// 假设我们想要拦截第一个参数
if (args.length > 0 && args[0] instanceof YourParameterClass) {
YourParameterClass param = (YourParameterClass) args[0];
// 在这里可以对参数进行验证或修改
if (param.getSomeField() == null) {
throw new IllegalArgumentException("Missing required field in parameter");
}
// 可以修改参数
param.setSomeField("modifiedValue");
}
}
}
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class YourController {
@PostMapping("/your-endpoint")
@RequestParamLogger // 使用自定义注解
public String yourMethod(@RequestBody YourParameterClass param) {
// 这里的 param 会被切面处理过
return "Processed: " + param.getSomeField();
}
}
初始尝试失败
此时发现,当参数请求符合规范,确实能在到达controller进行拦截打印,但是当请求参数的类型不对时,依然进行报错,即便是变更为环绕通知,也无法实现;
重整思路
这个思路不对,此时思考,为什么会拦截不了,是执行顺序的问题吗,至此,去重新温习了一下tomcat容器的内容,发现了这一张图
引用:https://blog.csdn.net/zzwpublic/article/details/111571569
该图引用自 博主 :zzwpublic
感谢他博文的解惑
过滤器是在请求进入容器后,但请求进入servlet之前进行预处理的。请求结束返回也是在servlet处理完后,返回给前端之前
过滤器实现
这也是为什么我用aop,拦截器,无法对参数进行解析映射为DTO类之前拦截的原因,在拦截器中,request就无法变更了,并且之后就已经完成了这一步映射解析,我需要在这之前进行拦截,处理reqeust才能完整获取。所以故此,思路就变成了用过滤器进行拦截,过滤器代码如下:
package com.example.methodtest.reqeustparamlogger.filter;
import com.example.methodtest.reqeustparamlogger.ReusableInputStreamWrapper;
import com.example.methodtest.reqeustparamlogger.annotation.RequestParamLogger;
import com.example.methodtest.utils.ApplicationBeanUtil;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.condition.PathPatternsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.lang.reflect.AnnotatedElement;
import java.util.Map;
import java.util.Set;
/**
* 实现将带有RequestParamLogger注解的请求进行处理,将流保存下来
*
* @author AaronCgt
*/
@Component
public class RequestParamFilter implements Filter {
@Override
public void destroy() {}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String requestURI = httpRequest.getRequestURI();
// 判断请求是否带有RequestParamLogger注解
if (checkHasPointAnnotation(requestURI)) {
// 处理动态配置的逻辑
ServletRequest requestWrapper = null;
requestWrapper = new ReusableInputStreamWrapper((HttpServletRequest) request);
chain.doFilter(requestWrapper, response);
}else{
chain.doFilter(request, response);
}
}
@Override
public void init(FilterConfig arg0) throws ServletException {}
private boolean checkHasPointAnnotation(String uri) {
RequestMappingHandlerMapping handlerMapping = ApplicationBeanUtil.getBean("requestMappingHandlerMapping");
Map<RequestMappingInfo, HandlerMethod> handlerMethods = handlerMapping.getHandlerMethods();
for (Map.Entry<RequestMappingInfo, HandlerMethod> entry : handlerMethods.entrySet()) {
RequestMappingInfo mappingInfo = entry.getKey();
HandlerMethod handlerMethod = entry.getValue();
PathPatternsRequestCondition patternsCondition = mappingInfo.getPathPatternsCondition();
assert patternsCondition != null;
Set<String> patterns = patternsCondition.getDirectPaths();
for (String pattern : patterns) {
if (uri.matches(pattern)) {
if (HasPointAnnotation(handlerMethod.getBeanType()) || HasPointAnnotation(handlerMethod.getMethod())) {
return true;
}
}
}
}
return false;
}
private boolean HasPointAnnotation(AnnotatedElement element) {
return AnnotatedElementUtils.hasAnnotation(element, RequestParamLogger.class);
}
}
Java流读取问题
上述代码可以看到,这里只是通过对拥有特定注解的方法进行拦截,并将request进行了替换,为什么要这么做,因为这里又涉及了Java的流的知识点,只能被读取一次
如果我需要获取请求参数,势必要进行流的读取,就会导致流的关闭,导致程序无法继续往下执行。
所以需要实现流的临时保存和读取,实现代码如下:
package com.example.methodtest.reqeustparamlogger;
import org.springframework.util.StreamUtils;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
/**
* 实现将流保存下来
*
* @author AaronCgt
* @since 2024/7/30/030
*/
public class ReusableInputStreamWrapper extends HttpServletRequestWrapper {
//用于将流保存下来
private byte[] requestBody = null;
public ReusableInputStreamWrapper(HttpServletRequest request, byte[] content) throws IOException {
super(request);
requestBody = content;
}
public ReusableInputStreamWrapper(HttpServletRequest request) throws IOException {
super(request);
requestBody = StreamUtils.copyToByteArray(request.getInputStream());
}
@Override
public ServletInputStream getInputStream() {
final ByteArrayInputStream bais = new ByteArrayInputStream(requestBody);
return new ServletInputStream() {
@Override
public int read(){
// 读取 requestBody 中的数据
return bais.read();
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) { }
};
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
}
拦截器实现和bean注册
至此,Java流关闭问题解决,此时再配合拦截器,就可以将参数进行打印,代码如下所示:
package com.example.methodtest.reqeustparamlogger.interceptor;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.example.methodtest.reqeustparamlogger.annotation.RequestParamLogger;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
/**
* 参数解析前拦截器
* 用于打印请求参数,请求地址等信息,方便排查问题
* @author AaronCgt
* @since 2024/7/29/029
*/
@Slf4j
public class RequestParamInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
if (handlerMethod.hasMethodAnnotation(RequestParamLogger.class)) {
StringBuilder sb = new StringBuilder();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream(), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
} catch (IOException e) {
log.error("Read request body error", e);
}
JSONObject jsonObject = JSON.parseObject(sb.toString());
Map<String,Object> logMap = new HashMap<>();
// 接口地址
logMap.put("Request URL", request.getRequestURL().toString());
// IP地址
logMap.put("Request IP Address", request.getRemoteAddr());
// 打印请求参数
Map<String,String> paramMap = new HashMap<>();
for (String key : request.getParameterMap().keySet()) {
paramMap.put(key,request.getParameter(key));
}
logMap.put("Request Parameters", JSON.toJSONString(paramMap));
logMap.put("Request Parameters body", JSON.toJSONString(jsonObject));
logMap.put("Request User", "{userId:此处可以根据需要进行检查}");
logMap.put("Request Method", handlerMethod.getMethod().getName());
log.info("Request Info:{}", JSON.toJSONString(logMap));
}
}
return true;
}
}
package com.example.methodtest.reqeustparamlogger.config;
import com.example.methodtest.reqeustparamlogger.filter.RequestParamFilter;
import com.example.methodtest.reqeustparamlogger.interceptor.RequestParamInterceptor;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 请求前置处理自动配置
*
* @author AaronCgt
*/
@AutoConfiguration
public class Configuration implements WebMvcConfigurer {
@Bean
public RequestParamInterceptor requestParamInterceptor() {
return new RequestParamInterceptor();
}
@Bean
public RequestParamFilter requestParamFilter() {
return new RequestParamFilter();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(requestParamInterceptor()).addPathPatterns("/**");
}
}
至此,将 RequestParamLogger 注解放置到对应的controller请求方法上面,即可以对请求参数,来源进行日志打印;
当然也有人说,可以针对参数解析错误进行拦截,在报错后进行日志打印也可以,这也是一个不错的思路,后续我也可以尝试实现一下;
结果
通过上述实现,于生产中发现了有一个用户,每3秒钟请求一次,请求参数一直是错误的,导致服务器一直报错,博主将对应的ip,用户报告给主管后,主管评估后觉得影响不大,并表示可以去掉这个日志记录先。该优化工作告一段落。