统一的日志处理
接着上一个crud项目
目录
- 基于AOP
AOP+注解
AOP 扫包 - 基于前置过滤器,后置过滤器
1. 基于AOP
AOP+注解
这种方式比较灵活,不受文件夹跟包的限制,给需要记录日志的接口加上注解,不需要记录的不加。
- 创建一个LogAutoRecord注解
import java.lang.annotation.*;
/**
* @author zhoust
* @Date 2021/9/17 22:49
* @Desc 日志记录切入点
*/
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogAutoRecord {
// 强制记录方法功能
String methodDesc();
}
- 创建aop切面 LogAspect
package com.zhoust.fastdome.aspect;
import com.alibaba.fastjson.JSONObject;
import com.zhoust.fastdome.annnotation.LogAutoRecord;
import com.zhoust.fastdome.business.entity.CommonLog;
import com.zhoust.fastdome.business.service.CommonLogService;
import com.zhoust.fastdome.common.CommonRequestBody;
import com.zhoust.fastdome.common.CommonResponse;
import com.zhoust.fastdome.utils.IPUtils;
import com.zhoust.fastdome.utils.SnowflakeIdWorker;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.Map;
/**
* @author zhoust
* @Date 2021/9/17 22:49
* @Desc 日志记录切面
*/
@Slf4j
@Aspect
@Component
public class LogAspect {
final CommonLogService commonLogService;
final HttpServletRequest request;
public LogAspect(CommonLogService commonLogService, HttpServletRequest request) {
this.commonLogService = commonLogService;
this.request = request;
}
/**
* 定义切入点
*/
@Pointcut("@annotation(com.zhoust.fastdome.annnotation.LogAutoRecord)")
public void getPointCutByAnnotation(){
}
@Around("getPointCutByAnnotation()")
public Object saveLog(ProceedingJoinPoint joinPoint){
Object result = "";
// 执行目标 获取返回值
try {
result = joinPoint.proceed();
saveLog(JSONObject.toJSONString(result),joinPoint);
} catch (Throwable throwable) {
throwable.printStackTrace();
log.error("执行方法失败,没有记录返回报文,不报错");
}
return result;
}
/**
* 获取参数插入日志表
* @param responseBody
* @param joinPoint
*/
private void saveLog(String responseBody,ProceedingJoinPoint joinPoint){
String logId = String.valueOf(SnowflakeIdWorker.generateId());
// 获取 工号信息 (一般在session获取,根据自己业务)
String operaType = "common";
String operaName = "测试";
String operaNo = "6666";
String requestIp = IPUtils.getIpAddr(request);
String requestBody = getRequestBody(joinPoint);
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
String methodName = method.getName();
LogAutoRecord annotation = method.getAnnotation(LogAutoRecord.class);
String methodDesc = annotation.methodDesc();
String requestURI = request.getRequestURI();
CommonLog commonLog = new CommonLog();
commonLog.setLogId(logId);
commonLog.setOperaType(operaType);
commonLog.setOperaNam(operaName);
commonLog.setOperaNo(operaNo);
commonLog.setMethodName(methodName);
commonLog.setRemoteIp(requestIp);
commonLog.setRequestUri(requestURI);
commonLog.setRequestBody(requestBody);
commonLog.setResponseBody(responseBody);
commonLog.setUriDesc(methodDesc);
commonLog.setCreateTime(new Date());
// 建议使用异步线程记录,避免堵塞(切记request的操作要放到主线程中,不能作为参数传到子线程中进行操作,不然后报错。)
commonLogService.save(commonLog);
}
/**
* 新项目建议定义好DTO请求参数统一使用一个继承一个公共的对象,
* 我这里定义了一个 CommonRequestBody,用来存公共的流水,及系统来源。
* 在别的项目中 参数可能是在parameter中,也可能在请求的请求体中。
* 根据实际项目去判断
* @param joinPoint
* @return
*/
private String getRequestBody(ProceedingJoinPoint joinPoint){
String requestBody = "";
Object[] args = joinPoint.getArgs();
for(Object arg :args){
// 如果获取到自定义对象,就将自定义对象存储
if(arg instanceof CommonRequestBody){
requestBody = JSONObject.toJSONString(arg);
break;
}
}
// 如果body 中没有,就去parameter中取
if(StringUtils.isEmpty(requestBody)){
Map<String, String[]> parameterMap = request.getParameterMap();
requestBody = JSONObject.toJSONString(parameterMap);
}
return requestBody;
}
}
- 上面从body中获取参数使用了自定义统一入参,所有请求参数DTO都继承该类 CommonRequestBody
@Data
public class CommonRequestBody {
private String transId;
private String systemId;
}
- 使用方式
@Slf4j
@RestController
public class UserTestLogController {
private final UserMapper userMapper;
private final UserService userService;
public UserTestLogController(UserMapper userMapper, UserService userService) {
this.userMapper = userMapper;
this.userService = userService;
}
@GetMapping("/getUserByParameter")
@LogAutoRecord(methodDesc = "根据用户ID查询用户信息")
public CommonResponse getUserById(String id){
String userById = userService.getUserById(id);
return CommonResponse.succeed(userById);
}
@GetMapping("/getUserByCommon")
@LogAutoRecord(methodDesc = "根据统一请求查询用户信息")
public CommonResponse getUserByCommonRequest(@RequestBody UserRequestDTO userRequestDTO){
String userById = userService.getUserById(userRequestDTO.getUserId());
return CommonResponse.succeed(userById);
}
}
只用在需要记录日志的地方加上注解@LogAutoRecord(methodDesc = "")即可,方法描述可以根据自己的需求是否记录
AOP扫包
AOP扫包跟注解的形式差不多,本质都是通过AOP,但确定哪些包下的请求需要记录日志,就可以字节配置包路径即可,不用一个一个的写注解。
/**
* 1、execution(): 表达式主体。
* 2、第一个*号:表示返回类型, *号表示所有的类型。
* 3、包名:表示需要拦截的包名,后面的两个句点表示当前包和当前包的所有子包,com.zhoust.fastdome.business.controller.scan包、子孙包下所有类的方法。
* 4、第二个*号:表示类名,*号表示所有的类。
* 5、*(..):最后这个星号表示方法名,*号表示所有的方法,后面括弧里面表示方法的参数,两个句点表示任何参数
*/
@Pointcut("execution(* com.zhoust.fastdome.business.controller.scan.*.*(..))")
public void getPointCutByScanPackage(){
}
通过 execution 指定要记录日志的包。
表中数据记录
2. 基于拦截器
在spring项目中实现拦截器有两种方式,在项目中使用,可以有两种方式,选择实现HandlerInterceptor接口或者继承HandlerInterceptorAdapter类,两种方式类似。HandlerInterceptorAdapter实现了AsyncHandlerInterceptor,AsyncHandlerInterceptor又继承了HandlerInterceptor,本质这三者都可以。
- 考虑到返回值不容易获取,可以从response 中获取,也可以用一些野路子。
我这里将返回值放到 setAttribute 中了 ,使用@ControllerAdvice。实现ResponseBodyAdvice的beforeBodyWrite方法
@ControllerAdvice
public class LogAdvice implements ResponseBodyAdvice {
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
/**
* 将返回报文放入
* @param body
* @param returnType
* @param selectedContentType
* @param selectedConverterType
* @param request
* @param response
* @return
*/
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
//通过RequestContextHolder获取request
HttpServletRequest httpServletRequest = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
httpServletRequest.setAttribute("responseBody", body);
return body;
}
- 实现拦截器
@Slf4j
@Component
public class LogInterceptor implements HandlerInterceptor{
private final CommonLogService commonLogService;
public LogInterceptor(CommonLogService commonLogService) {
this.commonLogService = commonLogService;
}
/**
* 前置拦截器
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//通过前置拦截器获取请求数据(如果是地址栏参数从parameter中获取,其他的从body中获取,另外还有地址栏跟body中都有的,这种变态报文尽量在定义接口的时候就杜绝掉)
String logId = String.valueOf(SnowflakeIdWorker.generateId());
request.setAttribute("logId",logId);
commonLogService.saveLogByHandler(request);
return HandlerInterceptor.super.preHandle(request, response, handler);
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("后置处理器开始");
Object responseBody = request.getAttribute("responseBody");
String s = JSONObject.toJSONString(responseBody);
commonLogService.updateLogByHandler(request,s);
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
- 注册拦截器
@Configuration
public class WebAdapterConfig implements WebMvcConfigurer {
private final LogInterceptor interceptor;
public WebAdapterConfig(LogInterceptor interceptor) {
this.interceptor = interceptor;
}
/**
* 注册拦截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(interceptor).addPathPatterns("/**");
WebMvcConfigurer.super.addInterceptors(registry);
}
}
基于拦截器的日志记录在获取请求参数在body中的数据有点困难。
请求参数在parameter中还好,直接get就能拿到,但是在body (前台传的json串)中的数据不是那个好拿,因为request 的 getInputStream() 或只能调用一次,多次调用会报错,下面会记录报错信息。然而我们在controller中一般会使用@RequestBody获取数据映射为实体,在这个过程中会调用getInputStream()。所以就会导致两者只能取其一。(所以才会强调请求统一格式,要么都在parameter要么都以json传递)
针对这种方式我找到了两种方式
1 重写request2 修改controller的获取请求方式
- 在filter在重写request
这种方式不需要改动controller方法,适合已经有很多接口的controller项目。
1 重写request2 新建过滤器3 从Request中获取请求
- 重写request
java EE 提供了HttpServletRequestWrapper 便于我们构造自定义的servletRequest。这里新建MyRequestUtils.java
package com.zhoust.fastdome.utils;import lombok.extern.slf4j.Slf4j;import javax.servlet.ReadListener;import javax.servlet.ServletInputStream;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletRequestWrapper;import java.io.*;/** * @author zhoust * @Date 2021/9/28 16:01 * @Desc <TODO DESC> */@Slf4jpublic class MyRequestUtils extends HttpServletRequestWrapper { private final String body; public MyRequestUtils(HttpServletRequest request) throws IOException { super(request); this.body = getBody(request); } /** * 从 request 中获取请求体(每次请求request.getReader()只能获取一次,所以这个在后面通过@RequestBody映射实体时会报错。后面有解决方法) * @param request * @return * @throws IOException */ private String getBody(HttpServletRequest request) throws IOException{ StringBuilder body = new StringBuilder(); BufferedReader reader = request.getReader(); String readLine = reader.readLine(); while (readLine != null){ body.append(readLine); readLine = reader.readLine(); } return body.toString(); } public String getBody() { return body; } @Override public ServletInputStream getInputStream() { final ByteArrayInputStream bais = new ByteArrayInputStream(body.getBytes()); return new ServletInputStream() { @Override public boolean isFinished() { return false; } @Override public boolean isReady() { return false; } @Override public void setReadListener(ReadListener readListener) { } @Override public int read(){ return bais.read(); } }; } @Override public BufferedReader getReader(){ return new BufferedReader(new InputStreamReader(this.getInputStream())); }}
- 新建过滤器
新建过滤器MyFilter。(在spring项目中记得注入到ioc容器中,我忘了,报错了)
package com.zhoust.fastdome.filter;import com.zhoust.fastdome.utils.MyRequestUtils;import lombok.extern.slf4j.Slf4j;import org.springframework.stereotype.Component;import javax.servlet.*;import javax.servlet.http.HttpServletRequest;import java.io.IOException;/** * @author zhoust * @Date 2021/9/28 16:56 * @Desc <TODO DESC> */@Component@Slf4jpublic class MyFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { log.info(">>>>>>>>>>过滤器出生<<<<<<<<<<<<"); Filter.super.init(filterConfig); } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { log.info(">>>>>>>>>>开始奋斗的过滤器<<<<<<<<<<<<"); // 更改request MyRequestUtils myRequestUtils = new MyRequestUtils((HttpServletRequest) request); chain.doFilter(myRequestUtils,response); log.info(">>>>>>>>>>奋斗完成的过滤器<<<<<<<<<<<<"); } @Override public void destroy() { log.info(">>>>>>>>>>过滤器度过光荣的一生<<<<<<<<<<<<"); Filter.super.destroy(); }}
- 从Request中获取请求
在MyRequestUtils中将读取到的requestbody信息放到了body 中,后续只需要getBody就可以获取。
String requestBody = "";try { MyRequestUtils myRequestUtils = (MyRequestUtils)request; requestBody = myRequestUtils.getBody();}catch (Exception e){ log.info("获取请求报文失败,{}",e);}
- 拦截器获取后
这种方式需要改动controller的取值方式,不使用@RequestBody,直接从Request中获取。
// 伪代码 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //通过前置拦截器获取请求数据(如果是地址栏参数从parameter中获取,其他的从body中获取,另外还有地址栏跟body中都有的,这种变态报文尽量在定义接口的时候就杜绝掉) String logId = String.valueOf(SnowflakeIdWorker.generateId()); request.setAttribute("logId",logId); // 获取放回放到setAttribute中 String requestBody = getBody(request); request.setAttribute("requestBody",requestBody); commonLogService.saveLogByHandler(request); return HandlerInterceptor.super.preHandle(request, response, handler); } // 获取请求体 private String getBody(HttpServletRequest request) throws IOException{ StringBuilder body = new StringBuilder(); BufferedReader reader = request.getReader(); String readLine = reader.readLine(); while (readLine != null){ body.append(readLine); readLine = reader.readLine(); } return body.toString(); } public String getBody() { return body; } // controller 这里用了阿里的fast json @GetMapping("/getUserById") public CommonResponse getUserById(HttpServletRequest request){ // 从Attribute 中获取数据 String requestBody = request.getAttribute("requestBody"); UserRequestDTO userRequestDTO = JSONObject.parseObject(requestBody, UserRequestDTO.class); String userById = userService.getUserById(userRequestDTO.getUserId()); return CommonResponse.succeed(userById); }
日志记录结果
报错记录
- getInputStream() has already been called for this request
request 的 getInputStream() 只能调用一次,多次调用会报错。()
上面在拦截器记录日志中记录了两种方式获取。
java.lang.IllegalStateException: getInputStream() has already been called for this request at org.apache.catalina.connector.Request.getReader(Request.java:1212) ~[tomcat-embed-core-9.0.13.jar:9.0.13] at org.apache.catalina.connector.RequestFacade.getReader(RequestFacade.java:504) ~[tomcat-embed-core-9.0.13.jar:9.0.13] at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_20] at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_20] at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_20] at java.lang.reflect.Method.invoke(Method.java:483) ~[na:1.8.0_20] at org.springframework.beans.factory.support.AutowireUtils$ObjectFactoryDelegatingInvocationHandler.invoke(AutowireUtils.java:305) ~[spring-beans-5.1.3.RELEASE.jar:5.1.3.RELEASE] at com.sun.proxy.$Proxy65.getReader(Unknown Source) ~[na:na]
- request 不能在多线程下作为参数传递,request只能在主线程中使用。(No thread-bound request found)
Exception in thread "Thread-2" java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet/DispatcherPortlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.
若想获取主线程中request中的信息,请先获取出来,然后赋值给共享对象,传递给子线程
非AOP拦截到的异常。
另外,有些异常可能不是在方法(controller)执行中发生的,比如请求在拦截器中校验用户的真实性,该用户是否有权限访问该接口等。一些不属不会被我们的Aop 拦截到的异常我们也是需要记录的,并且不会往前台返回一大串的异常信息,通常是<操作失败,日志流水:xxxxxxxx>,类似这种。
0001-Java项目中统一返回统一异常处理.md一笔记中记录到统一异常处理。借助@ControllerAdvice
@Slf4j@ControllerAdvicepublic class ExceptionHandle { /** * 处理未知异常 * @param e * @return */ @ExceptionHandler(Exception.class) @ResponseBody public CommonResponse handleException(Exception e){ log.error("系统异常:{}",e.getMessage()); return CommonResponse.error(e.getMessage()); } /** * 处理主动抛出的自定义异常 * @param e * @return */ @ExceptionHandler(BusinessException.class) @ResponseBody public CommonResponse handleBusinessException(BusinessException e){ log.error("自定义异常:{}",e.getErrMassage()); return CommonResponse.error(e.getErrCode(),e.getErrMassage()); }}
所以当我们在AOP 中没有拦截到的异常可以在这里记录日志。
package com.zhoust.fastdome.exception;import com.alibaba.fastjson.JSONObject;import com.zhoust.fastdome.business.entity.CommonLog;import com.zhoust.fastdome.business.service.CommonLogService;import com.zhoust.fastdome.common.CommonResponse;import com.zhoust.fastdome.utils.IPUtils;import com.zhoust.fastdome.utils.SnowflakeIdWorker;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.util.StringUtils;import org.springframework.web.bind.annotation.ControllerAdvice;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.ResponseBody;import javax.servlet.http.HttpServletRequest;import java.util.Date;/** * @author zhoust * @Date * @Desc */@Slf4j@ControllerAdvicepublic class ExceptionHandle { private final HttpServletRequest request; private final CommonLogService commonLogService; public ExceptionHandle(HttpServletRequest request,CommonLogService commonLogService) { this.request = request; this.commonLogService = commonLogService; } /** * 处理未知异常 * @param e * @return */ @ExceptionHandler(Exception.class) @ResponseBody public CommonResponse handleException(Exception e){ e.printStackTrace(); log.error("系统异常:{}",e); CommonLog commonLog = commonLogService.getCommonLog(request, ""); CommonResponse error = CommonResponse.error("操作异常,日志流水:"+commonLog.getLogId()); error.setLogId(commonLog.getLogId()); // 失败原因可以不返回前台,根据自己业务需求 error.setDate(e.getMessage()); commonLog.setResponseBody(JSONObject.toJSONString(error)); saveLog(commonLog); return error; } /** * 处理主动抛出的自定义异常 * @param e * @return */ @ExceptionHandler(BusinessException.class) @ResponseBody public CommonResponse handleBusinessException(BusinessException e){ log.error("自定义异常:{}",e.getErrMassage()); CommonLog commonLog = commonLogService.getCommonLog(request, ""); CommonResponse error = CommonResponse.error(e.getErrCode(), e.getErrMassage()+",日志流水:"+commonLog.getLogId()); error.setLogId(commonLog.getLogId()); error.setDate(e); commonLog.setResponseBody(JSONObject.toJSONString(error)); saveLog(commonLog); return error; } private void saveLog(CommonLog commonLog){ commonLogService.save(commonLog); }}
getCommonLog方法
@Override public CommonLog getCommonLog(HttpServletRequest request,String requestBody){ CommonLog commonLog = new CommonLog(); String logId = ""; Object logId1 = request.getAttribute("logId"); if(StringUtils.isEmpty(logId1)){ logId = String.valueOf(SnowflakeIdWorker.generateId()); }else{ logId = (String) logId; } // 获取 工号信息 (一般在session获取,根据自己业务) String operaType = "common"; String operaName = "测试"; String operaNo = "6666"; String requestIp = IPUtils.getIpAddr(request); String requestURI = request.getRequestURI(); commonLog.setLogId(logId); commonLog.setOperaType(operaType); commonLog.setOperaNam(operaName); commonLog.setOperaNo(operaNo); commonLog.setRemoteIp(requestIp); commonLog.setRequestUri(requestURI); commonLog.setRequestBody(requestBody); commonLog.setCreateTime(new Date()); return commonLog; }