使用SpringAop记录日志

日志打印

  • 日志作为项目运行中出错的第一手资料,打印的详细与否直接决定了bug解决的快慢。
  • 新建一个注解值记录的bean
    `java
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;

/**

  • 功能描述:
  • 【注解值bean】
    *
  • @author chihiro
  • @version V1.0
  • @date 2019/08/18 14:23
    */
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class WebLogValue {

    public String value;
    public String name;

}

- 输出日志
```java
/**
 * 功能描述:
 * 【接口入出参日志打印】
 *
 * @author chihiro
 * @version V1.0
 * @date 2019/08/18 01:06
 */
@Aspect
@Order(99)
@Component
@Slf4j
public class WebLogAspect {

    /**
     * Controller层切点 使用到了spring原生的RequestMapping 作为切点表达式。
     */
    @Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
    public void webLog() {
    }

    @Before("webLog()")
    public void doBefore(JoinPoint joinPoint) {
        try {
            StringBuilder params = new StringBuilder();

            Object[] args = joinPoint.getArgs();
            for (int i = 0; i < args.length; i++) {
                Object object = args[i];
                // 此处过滤掉一些无需打印的参数
                if (object instanceof MultipartFile || object instanceof HttpServletRequest || object instanceof HttpServletResponse) {
                    continue;
                }
                params.append(JSON.toJSONString(object));
                if (i < args.length - 1) {
                    params.append(",");
                }
            }
            log.info("类信息[{}],请求参数[{}]", getRequestMappingAnnotationValue(joinPoint), params.toString());
        } catch (Exception e) {
            //记录本地异常日志
            log.error("===前置Controller通知异常===");
            log.error("异常信息:{}", e.getMessage());
        }

    }

    @AfterReturning(returning = "response", pointcut = "webLog()")
    public void doAfterReturning(JoinPoint joinPoint, Object response) throws Throwable {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        if (response != null) {
            try {
                log.info("类信息[{}],请求地址[{}],返回参数[{}]", getRequestMappingAnnotationValue(joinPoint), request.getRemoteAddr(), JSON.toJSONString(response));
            } catch (Exception e) {
                //记录本地异常日志
                log.error("===后置Controller通知异常===");
                log.error("异常信息:{}", e.getMessage());
            }
        }
    }

    /**
     * 获取RequestMapping中注解值
     */
    private static WebLogValue getRequestMappingAnnotationValue(JoinPoint joinPoint) throws Exception {
        String targetName = joinPoint.getTarget().getClass().getName();
        String methodName = joinPoint.getSignature().getName();
        Object[] arguments = joinPoint.getArgs();
        Class targetClass = Class.forName(targetName);
        Method[] methods = targetClass.getMethods();
        WebLogValue webLogValue = new WebLogValue();
        for (Method method : methods) {
            if (method.getName().equals(methodName)) {
                Class[] parameterTypes = method.getParameterTypes();
                if (parameterTypes.length == arguments.length) {
                    String[] value = method.getAnnotation(RequestMapping.class).value();
                    String name = method.getAnnotation(RequestMapping.class).name();
                    webLogValue.setValue(Arrays.toString(value));
                    webLogValue.setName(name);
                    break;
                }
            }
        }
        return webLogValue;
    }

日志入库

  • 日志表的DDL信息
    CREATE TABLE `pm_log` (
    `log_id` varchar(32) NOT NULL COMMENT '主键id',
    `user_name` varchar(50) DEFAULT NULL COMMENT '用户名',
    `ip` varchar(20) DEFAULT NULL COMMENT 'ip信息',
    `params` varchar(255) DEFAULT NULL COMMENT '请求参数',
    `result` longtext COMMENT '返回结果',
    `method` varchar(150) DEFAULT NULL COMMENT '方法名',
    `operation` varchar(50) DEFAULT NULL COMMENT '操作',
    `unique_code` varchar(20) DEFAULT NULL COMMENT '唯一标识',
    `error` char(2) DEFAULT NULL COMMENT '是否异常00:异常,01:正常',
    `stack` longtext COMMENT '异常堆栈',
    `take_time` bigint(20) DEFAULT NULL COMMENT '请求耗时',
    `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    PRIMARY KEY (`log_id`) USING BTREE,
    KEY `unique_code` (`unique_code`) USING BTREE
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
    

    此处数据库设计未经详细考虑,有更好想法的朋友欢迎留言讨论。

  • 核心代码
    `java
    /**

    • 功能描述:
    • 【操作日志记录切面】
      *
    • @author chihiro
    • @version V1.0
    • @date 2019/08/18 01:06
      */
      @Aspect
      @Order(100)
      @Component
      @Slf4j
      public class SysLoggerAspect {

      @Autowired
      private LogRecordHandler logRecordHandler;

      @Pointcut(“@annotation(org.springframework.web.bind.annotation.RequestMapping)”)
      public void sysLogger() {
      }

      @Before(“sysLogger()”)
      public void doBeforeController(JoinPoint joinPoint) {

      // 开始时间
      long startTime = System.currentTimeMillis();
      ThreadLocalUtil.setValue(KeyConstants.START_TIME.getKey(), String.valueOf(startTime));
      
      MethodSignature signature = (MethodSignature) joinPoint.getSignature();
      Method method = signature.getMethod();
      SysLog sysLog = new SysLog();
      RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
      if (requestMapping != null) {
          //注解上的描述
          sysLog.setOperation(requestMapping.name());
      }
      //请求的方法名
      String className = joinPoint.getTarget().getClass().getName();
      String methodName = signature.getName();
      sysLog.setMethod(className + "." + methodName + "()");
      //请求的参数
      Object[] args = joinPoint.getArgs();
      StringBuilder sbParams = new StringBuilder();
      for (int i = 0; i < args.length; i++) {
          Object object = args[i];
          if (object instanceof MultipartFile || object instanceof HttpServletRequest || object instanceof HttpServletResponse) {
              continue;
          }
          sbParams.append(JSON.toJSONString(object));
          if (i < args.length - 1) {
              sbParams.append(",");
          }
      }
      String params = sbParams.toString();
      if (StrUtil.isNotBlank(params)) {
          sysLog.setParams(params);
      }
      //请求的用户
      String userName = ThreadLocalUtil.getValue(KeyConstants.USER_NAME.getKey());
      if (StrUtil.isNotBlank(userName)) {
          sysLog.setUserName(userName);
      }
      //用户的IP
      sysLog.setIp(WebUtil.getIpAddress());
      // 设置唯一标识
      sysLog.setUniqueCode(ThreadLocalUtil.getValue(KeyConstants.UNIQUE_CODE.getKey()));
      ThreadLocalUtil.setValue(KeyConstants.SYS_LOG.getKey(), JSON.toJSONString(sysLog));
      

      // logRecordHandler.recordLog(sysLog);
      }

      @AfterReturning(value = “sysLogger()”, returning = “res”)
      public void doAfterReturning(JoinPoint joinPoint, Object res) {

      long takeTime = System.currentTimeMillis() - Long.valueOf(ThreadLocalUtil.getValue(KeyConstants.START_TIME.getKey()));
      SysLog sysLog = new SysLog();
      SysLog sys = JSON.parseObject(ThreadLocalUtil.getValue(KeyConstants.SYS_LOG.getKey()), SysLog.class);
      BeanUtil.copyProperties(sys, sysLog);
      // 设置返回结果
      sysLog.setResult(JSON.toJSONString(res));
      // 设置请求耗时
      sysLog.setTakeTime(takeTime);
      sysLog.setError("01");
      ThreadLocalUtil.removeValue(KeyConstants.START_TIME.getKey());
      ThreadLocalUtil.removeValue(KeyConstants.SYS_LOG.getKey());
      // 发送MQ消息
      logRecordHandler.recordLog(sysLog);
      

      }

      @AfterThrowing(value = “sysLogger()”, throwing = “throwable”)
      public void doAfterThrowing(JoinPoint joinPoint, Throwable throwable) {

      long takeTime = System.currentTimeMillis() - Long.valueOf(ThreadLocalUtil.getValue(KeyConstants.START_TIME.getKey()));
      SysLog sysLog = new SysLog();
      SysLog sys = JSON.parseObject(ThreadLocalUtil.getValue(KeyConstants.SYS_LOG.getKey()), SysLog.class);
      BeanUtil.copyProperties(sys, sysLog);
      // 设置请求耗时
      sysLog.setTakeTime(takeTime);
      // 设置堆栈信息
      sysLog.setStack(Arrays.toString(throwable.getStackTrace()));
      sysLog.setError("00");
      ThreadLocalUtil.removeValue(KeyConstants.START_TIME.getKey());
      ThreadLocalUtil.removeValue(KeyConstants.SYS_LOG.getKey());
      // 发送MQ消息
      logRecordHandler.recordLog(sysLog);
      

      }

}

- 日志实体
```java
/**
 * 功能描述:
 * 【日志实体】
 *
 * @author chihiro
 * @version V1.0
 * @date 2019/08/18 01:59
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SysLog implements Serializable {
    private static final long serialVersionUID = 5L;

    /**
     * 主键id
     */
    private String logId;
    /**
     * 用户名
     */
    private String userName;
    /**
     * 用户操作
     */
    private String operation;
    /**
     * 请求方法
     */
    private String method;
    /**
     * 请求参数
     */
    private String params;
    /**
     * 请求结果
     */
    private String result;
    /**
     * IP地址
     */
    private String ip;
    /**
     * 唯一标识
     */
    private String uniqueCode;
    /**
     * 是否异常
     * 00:异常
     * 01:正常
     */
    private String error;
    /**
     * 异常堆栈
     */
    private String stack;
    /**
     * 请求耗时
     */
    private Long takeTime;

    /**
     * 创建时间
     */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime createTime;

}
  • key枚举

    /**
    * 功能描述:
    * 【项目key枚举】
    *
    * @author chihiro
    * @version V1.0
    * @date 2019/09/04 11:33
    */
    public enum KeyConstants {
    
      // 用户编号记录
      USER_ID("userId"),
      // 用户名记录
      USER_NAME("username"),
      // 用户所属唯一标识
      UNIQUE_CODE("uniqueCode"),
      // 请求开始时间
      START_TIME("startTime"),
      // 日志对象
      SYS_LOG("sysLog");
    
      private String key;
    
      public String getKey() {
          return key;
      }
    
      KeyConstants(String key) {
          this.key = key;
      }
    }
    
  • MQ处理器
    `java
    /**

    • 功能描述:
    • 【日志记录处理器】
      *
    • @author chihiro
    • @version V1.0
    • @date 2019/09/04 11:07
      */
      @Component
      @Slf4j
      public class LogRecordHandler {

      @Autowired
      private AmqpTemplate rabbitTemplate;

      /**

      • 将操作日记发往MQ处理
        *
      • @param sysLog 日志实体
        */
        public void recordLog(SysLog sysLog) {
        rabbitTemplate.convertAndSend(MqConstants.QUEUE_LOG_RECODE.getTopic(), JSON.toJSONString(sysLog));
        }

}

- 本地线程工具类
```java
/**
 * 本地线程工具类
 *
 * @author chihiro
 * @version V1.0
 * @date 2019/09/04 11:10
 */
public final class ThreadLocalUtil {

    private static final ThreadLocal<Map<String, String>> THREAD_CONTEXT = ThreadLocal.withInitial(HashMap::new);

    /**
     * 根据key获取值
     *
     * @param key 键值
     * @return value
     */
    public static String getValue(String key) {
        if (THREAD_CONTEXT.get() == null) {
            return null;
        }
        return THREAD_CONTEXT.get().get(key);
    }

    /**
     * 存储
     *
     * @param key   键值
     * @param value 值
     */
    public static String setValue(String key, String value) {
        Map<String, String> cacheMap = THREAD_CONTEXT.get();
        if (cacheMap == null) {
            cacheMap = new HashMap<>();
            THREAD_CONTEXT.set(cacheMap);
        }
        return cacheMap.put(key, value);
    }

    /**
     * 根据key移除值
     *
     * @param key 键值
     */
    public static void removeValue(String key) {
        Map<String, String> cacheMap = THREAD_CONTEXT.get();
        if (cacheMap != null) {
            cacheMap.remove(key);
        }
    }

    /**
     * 重置
     */
    public static void reset() {
        if (THREAD_CONTEXT.get() != null) {
            THREAD_CONTEXT.get().clear();
        }
    }

}
  • http工具类
    `java
    /**

    • 功能描述:
    • 【Http工具类】
      *
    • @author chihiro
    • @version V1.0
    • @date 2019/09/04 11:58
      */
      public class WebUtil {

      public static Map<String, String> queryStringToMap(String queryString, String charset) {

      try {
          Map<String, String> map = new HashMap<>();
      
          String[] decode = URLDecoder.decode(queryString, charset).split("&");
          for (String keyValue : decode) {
              String[] kv = keyValue.split("[=]", 2);
              map.put(kv[0], kv.length > 1 ? kv[1] : "");
          }
          return map;
      } catch (UnsupportedEncodingException e) {
          throw new UnsupportedOperationException(e);
      }
      

      }

      /**

      • 尝试获取当前请求的HttpServletRequest实例
        *
      • @return HttpServletRequest
        */
        public static HttpServletRequest getHttpServletRequest() {
        try {
         return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        
        } catch (Exception e) {
         return null;
        
        }
        }

      public static Map<String, String> getParameters(HttpServletRequest request) {

      Map<String, String> parameters = new HashMap<>();
      Enumeration enumeration = request.getParameterNames();
      while (enumeration.hasMoreElements()) {
          String name = String.valueOf(enumeration.nextElement());
          parameters.put(name, request.getParameter(name));
      }
      return parameters;
      

      }

      public static Map<String, String> getHeaders(HttpServletRequest request) {

      Map<String, String> map = new LinkedHashMap<>();
      Enumeration<String> enumeration = request.getHeaderNames();
      while (enumeration.hasMoreElements()) {
          String key = enumeration.nextElement();
          String value = request.getHeader(key);
          map.put(key, value);
      }
      return map;
      

      }

      private static final String[] IP_HEADERS = {

          "X-Forwarded-For",
          "X-Real-IP",
          "Proxy-Client-IP",
          "WL-Proxy-Client-IP"
      

      };

      /**

      • 获取请求客户端的真实ip地址
        *
      • @param request 请求对象
      • @return ip地址
        */
        public static String getIpAddress(HttpServletRequest request) {

        // 获取请求主机IP地址,如果通过代理进来,则透过防火墙获取真实IP地址
        String ip = request.getHeader(IP_HEADERS[0]);

        if (ip == null || ip.length() == 0 || “unknown”.equalsIgnoreCase(ip)) {

         if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
             ip = request.getHeader("Proxy-Client-IP");
         }
         if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
             ip = request.getHeader("WL-Proxy-Client-IP");
         }
         if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
             ip = request.getHeader("HTTP_CLIENT_IP");
         }
         if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
             ip = request.getHeader("HTTP_X_FORWARDED_FOR");
         }
         if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
             ip = request.getRemoteAddr();
         }
        

        } else if (ip.length() > 15) {

         String[] ips = ip.split(",");
         for (int index = 0; index < ips.length; index++) {
             String strIp = (String) ips[index];
             if (!("unknown".equalsIgnoreCase(strIp))) {
                 ip = strIp;
                 break;
             }
         }
        

        }
        return ip;
        }

      /**

      • 获取请求客户端的真实ip地址
        *
      • @return ip地址
        */
        public static String getIpAddress() {
        // 获取请求主机IP地址,如果通过代理进来,则透过防火墙获取真实IP地址
        return getIpAddress(getHttpServletRequest());
        }

      /**

      • web应用绝对路径
        *
      • @param request 请求对象
      • @return 绝对路径
        */
        public static String getBasePath(HttpServletRequest request) {
        String path = request.getContextPath();
        return request.getScheme() + “://“ + request.getServerName() + “:” + request.getServerPort() + path + “/“;
        }

}
`

  • 以上,代码展示完毕,日志的入库使用MQ进行异步。经测试,功能无误,尚未上线测试,未知性能问题。

    后记

  • MQ的使用可参考这篇文章
  • 欢迎指正博客内不足之处

  目录