0002-java项目统一的日志记录.md

统一的日志处理

接着上一个crud项目

目录
  1. 基于AOP
    AOP+注解
    AOP 扫包
  2. 基于前置过滤器,后置过滤器
1. 基于AOP
AOP+注解

这种方式比较灵活,不受文件夹跟包的限制,给需要记录日志的接口加上注解,不需要记录的不加。

  1. 创建一个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();
    }
    
    
  2. 创建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;
        }
    }
    
    
  3. 上面从body中获取参数使用了自定义统一入参,所有请求参数DTO都继承该类 CommonRequestBody

    @Data
    public class CommonRequestBody {
    
        private String transId;
    
        private String systemId;
    }
    
  4. 使用方式

    @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实现了AsyncHandlerInterceptorAsyncHandlerInterceptor又继承了HandlerInterceptor,本质这三者都可以。

    1. 考虑到返回值不容易获取,可以从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;
          }
      
      
    2. 实现拦截器

      @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);
          }
      
    3. 注册拦截器

      @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的获取请求方式
    
    1. 在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);}
        
    2. 拦截器获取后

      这种方式需要改动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);    }
      
    日志记录结果

    日志记录

报错记录

  1. 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]
    
  2. 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;    }

源码地址

posted @ 2021-09-23 13:07  神经哇  阅读(229)  评论(0)    收藏  举报