0002-java项目统一的日志记录.md
统一的日志处理
接着上一个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; }

浙公网安备 33010602011771号