解决问题场景
业务类异常通常使用aop进行日志记录处理,但是非业务异常(请求还没到方法中出现的异常),aop日志是不能记录请求参数的.现在要求,对于系统异常的请求进行请求参数日志输出,比如说请求参数异常或是请求方式不正确进行获取请求参数信息.
get请求可以直接使用request.getParameter(),关键是处理put或post等其他请求,此类请求方式参数都存储于请求体中,获取参数的主要方式就是从inputStream中进行获取,至于这种处理就会出现一个经典问题:过滤器不能重复获取inputStream中流信息.
过滤器不能重复获取inputStream中流信息.主要原因是:InputStream的read()方法内部有一个postion,标志当前流被读取到的位置,每读取一次,该标志就会移动一次,如果读到最后,read()会返回-1,表示已经读取完了。如果想要重新读取则需要调用reset()方法,position就会移动到上次调用mark的位置,mark默认是0,所以就能从头再读了。
处理方案
1.创建自定义HttpServletRequestWrapper包装类,缓存请求参数信息;
2.自定义包装类过滤器;将原生的HttpServletRequest对象替换为自定义HttpServletRequestWrapper对象
3.配置自定义过滤器
相关代码
自定义包装类
public class PersonalRequestWrapper extends HttpServletRequestWrapper{
private final byte[] body;
public PersonalRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
body = StreamUtils.copyToByteArray(request.getInputStream());
}
public String getBodyString(InputStream inputStream) {
StringBuilder sb = new StringBuilder();
BufferedReader reader = null;
try {
reader = new BufferedReader(new InputStreamReader(inputStream, "utf-8"));
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
} catch (IOException e) {
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
}
}
}
return sb.toString();
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream bais = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public int read() throws IOException {
return bais.read();
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
};
}
}
自定义包装类过滤器:
public class PersonalRequestFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
ServletRequest requestWrapper = new PersonalRequestWrapper((HttpServletRequest) request);
chain.doFilter(requestWrapper, response);
}
@Override
public void destroy() {
}
}
日志信息组装过滤器:
public class RequestDetailFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("RequestDetailFilter init");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 设置请求详情信息
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
// 请求IP
String ip = IpUtil.getRequestIp(httpServletRequest);
// 请求路径
String path = httpServletRequest.getRequestURI();
// 请求参数
PersonalRequestWrapper personalRequestWrapper = new PersonalRequestWrapper(httpServletRequest);
// 请求参数
String bodyString;
if(httpServletRequest.getMethod().equalsIgnoreCase("get")){
Map<String, String[]> parameterMap = httpServletRequest.getParameterMap();
bodyString= JSON.toJSONString(parameterMap);
bodyString=bodyString.replace("[","").replace("]","");
}else {
PersonalRequestWrapper personalRequestWrapper = new PersonalRequestWrapper(httpServletRequest);
bodyString = personalRequestWrapper.getBodyString(httpServletRequest.getInputStream());
}
RequestDetail requestDetail = new RequestDetail()
.setIp(ip)
.setPath(path)
.setRequestParam(bodyString);
// 设置请求详情信息
RequestDetailThreadLocal.setRequestDetail(requestDetail);
chain.doFilter(request, response);
// 释放
RequestDetailThreadLocal.remove();
}
@Override
public void destroy() {
log.info("RequestDetailFilter destroy");
}
}
过滤器配置
@Configuration
public class PersonalWebMvcConfig implements WebMvcConfigurer {
/**
* RequestDetailFilter配置
*
* @return
*/
@Bean
public FilterRegistrationBean requestDetailFilter() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
filterRegistrationBean.setFilter(new RequestDetailFilter());
filterRegistrationBean.setEnabled(true);
filterRegistrationBean.addUrlPatterns("/*");
filterRegistrationBean.setOrder(2);
filterRegistrationBean.setAsyncSupported(true);
}
/**
* PersonalRequestFilter配置
*
* @return
*/
@Bean
public FilterRegistrationBean cachingRequestFilter() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
filterRegistrationBean.setFilter(new PersonalRequestFilter());
filterRegistrationBean.setEnabled(true);
filterRegistrationBean.addUrlPatterns("/*");
filterRegistrationBean.setOrder(1);
filterRegistrationBean.setAsyncSupported(true);
return filterRegistrationBean;
}
}
系统异常捕获日志处理:
@ExceptionHandler(value = Exception.class)
@ResponseStatus(HttpStatus.OK)
public ApiResult<Boolean> exceptionHandler(Exception exception) {
// 异常请求信息打印
printRequestDetail(exception);
// 异常响应信息返回.
return ApiResult.fail(ApiCode.SYSTEM_EXCEPTION);
// 日志打印
private void printRequestDetail(Exception exception) {
RequestDetail requestDetail = RequestDetailThreadLocal.getRequestDetail();
if (requestDetail != null) {
log.error("异常来源:ip: {}, path: {},requestParam:{},exception: {}", requestDetail.getIp(), requestDetail.getPath(),requestDetail.getRequestParam(),exception.getMessage());
}
}
}
自定义本地线程存储请求信息,记录请求详情信息到当前线程中,可在任何地方获取.
public class RequestDetailThreadLocal {
private static ThreadLocal<RequestDetail> threadLocal = new ThreadLocal<>();
/**
* 设置请求信息到当前线程中
*
* @param requestDetail
*/
public static void setRequestDetail(RequestDetail requestDetail) {
threadLocal.set(requestDetail);
}
/**
* 从当前线程中获取请求信息
*/
public static RequestDetail getRequestDetail() {
return threadLocal.get();
}
/**
* 销毁
*/
public static void remove() {
threadLocal.remove();
}
}
请求封装对象
public class RequestDetail {
/**
* 请求ip地址
*/
private String ip;
/**
* 请求路径
*/
private String path;
/**
* 请求参数
*/
private String requestParam;
}
测试
@PostMapping("/testQuestParam")
public ApiResult testQuestParam(@RequestBody @Validated Person person) throws Exception{
System.out.println(person);
return ApiResult.ok();
}
请求对象信息:
public class Person {
@NotBlank(message = "不允许为空")
private String name;
@Min(value = 2,message = "年龄不允许为1")
private int age;
// 省略get/set
}
请求示例:
日志输出:
2022-01-04 23:36:28.557 ERROR [nio-8080-exec-1] [] i.g.s.handler.GlobalExceptionHandler [295] : 异常来源:ip: 192.168.25.4, path: /api/test/testQuestParam,requestParam:{ "name":"", "age":2},exception: Validation failed for argument [0] in public io.geekidea.springbootplus.framework.common.api.ApiResult com.kawaxiaoyu.api.share.controller.TestUploadController.testQuestParam(com.kawaxiaoyu.api.share.controller.Person) throws java.lang.Exception: [Field error in object 'person' on field 'name': rejected value []; codes [NotBlank.person.name,NotBlank.name,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [person.name,name]; arguments []; default message [name]]; default message [不允许为空]]
2022-01-04 23:36:28.562 ERROR [nio-8080-exec-1] [] i.g.s.handler.GlobalExceptionHandler [79] : errorCode: 5001, errorMessage: 请求参数校验异常:["不允许为空"]
如果有更好的方案,评论区欢迎留言,多多交流!