033-从零搭建微服务-日志插件(一)

写在最前

如果这个项目让你有所收获,记得 Star 关注哦,这对我是非常不错的鼓励与支持。

源码地址(后端):gitee.com/csps/mingyu...

源码地址(前端):gitee.com/csps/mingyu...

文档地址:gitee.com/csps/mingyu...

为什么要日志插件?

在Java应用程序中记录日志是一种良好的实践,它为开发、运维和支持团队提供了很多好处。以下是一些主要的理由:

  1. 故障排除和调试:

    • 日志是定位和解决问题的重要工具。通过在关键代码路径和操作中插入日志语句,开发人员可以追踪应用程序的执行流程,快速定位潜在的错误和异常。
  2. 性能分析:

    • 记录关键操作的执行时间、资源使用情况等信息,有助于性能分析和优化。通过分析日志,可以确定应用程序的瓶颈并改进性能。
  3. 安全审计:

    • 记录关键的安全事件和用户活动,以便进行审计和检测潜在的安全威胁。登录失败、访问敏感信息等事件的记录对于安全监控至关重要。
  4. 系统状态监控:

    • 通过记录系统状态和关键指标,可以实时监控应用程序的运行状况。这有助于及时发现和解决潜在的问题,以提高系统的稳定性和可用性。
  5. 版本追踪和审计:

    • 在代码中记录版本信息、变更历史和代码提交信息,有助于追踪应用程序的演变过程。审计日志还可以用于追溯特定功能或问题的起源。
  6. 用户行为分析:

    • 对于包含用户交互的应用程序,记录用户活动可以帮助了解他们的使用模式、偏好和行为。这对于改进用户体验和调整产品设计非常有帮助。
  7. 合规性和法规要求:

    • 许多行业和法规要求记录关键事件和操作,以确保企业的合规性。通过日志记录,可以满足这些法规的要求,并提供审计证据。
  8. 持久化数据:

    • 将日志存储在持久化介质中,例如文件或数据库,以便在应用程序重新启动后仍然可以访问日志。这有助于在系统故障或应用程序崩溃时还原状态并进行故障排除。

日志设计

日志类型

系统操作日志和用户登录日志是两种不同类型的日志,它们记录了系统中不同方面的活动

  • 系统操作日志:记录系统的各种操作,包括但不限于增删改查、上传与下载文件等。
  • 用户登录日志:记录用户登录和注销的信息。

记录方式

  • 系统操作日志:采用注解(非侵入)方式记录;
  • 用户登录日志:采用显式(侵入)方式记录;

日志插件

添加 mingyue-common-log 插件

xml 复制代码
<dependencies>
  <dependency>
    <groupId>com.csp.mingyue</groupId>
    <artifactId>mingyue-common-security</artifactId>
  </dependency>
</dependencies>

Log 注解

less 复制代码
@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
​
  /**
   * 模块
   */
  String module() default "";
​
  /**
   * 功能
   */
  BusinessType businessType() default BusinessType.OTHER;
​
  /**
   * 操作人类别
   */
  OperatorUserType operatorUserType() default OperatorUserType.MANAGE;
​
  /**
   * 是否保存请求的参数
   */
  boolean isSaveRequestData() default true;
​
  /**
   * 是否保存响应的参数
   */
  boolean isSaveResponseData() default true;
​
  /**
   * 排除指定的请求参数
   */
  String[] excludeParamNames() default {};
​
}

Log 切面

操作日志记录核心类

scss 复制代码
@Slf4j
@Aspect
@RequiredArgsConstructor
@AutoConfiguration
public class LogAspect {
​
  private final ServiceInstance serviceInstance;
​
  /**
   * 排除敏感属性字段
   */
  public static final String[] EXCLUDE_PROPERTIES = { "password", "oldPassword", "newPassword", "confirmPassword" };
​
  /**
   * 处理完请求后执行
   * @param joinPoint 切点
   */
  @AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")
  public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult) {
    handleLog(joinPoint, controllerLog, null, jsonResult);
  }
​
  /**
   * 拦截异常操作
   * @param joinPoint 切点
   * @param e 异常
   */
  @AfterThrowing(value = "@annotation(controllerLog)", throwing = "e")
  public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e) {
    handleLog(joinPoint, controllerLog, e, null);
  }
​
  protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult) {
    // 日志记录开始时间
    Long startTime = System.currentTimeMillis();
​
    // ========数据库日志========
    OperateLogEvent operateLog = new OperateLogEvent();
​
    try {
      // 请求信息
      String ip = ServletUtils.getClientIP();
      operateLog.setReqIp(ip);
      operateLog.setServiceId(serviceInstance.getServiceId());
      operateLog.setReqAddress(AddressUtils.getRealAddressByIP(ip));
      operateLog
          .setReqUrl(StrUtil.sub(Objects.requireNonNull(ServletUtils.getRequest()).getRequestURI(), 0, 255));
​
      operateLog.setStatus(BusinessStatus.SUCCESS.ordinal());
​
      // 用户信息
      LoginUser loginUser = LoginHelper.getLoginUser();
      operateLog.setUserId(loginUser.getUserId());
      operateLog.setUserName(loginUser.getUsername());
​
      if (e != null) {
        operateLog.setStatus(BusinessStatus.FAIL.ordinal());
        operateLog.setException(StrUtil.sub(e.getMessage(), 0, 2000));
      }
​
      // 设置方法名称
      String className = joinPoint.getTarget().getClass().getName();
      String methodName = joinPoint.getSignature().getName();
      operateLog.setMethod(className + "." + methodName + "()");
​
      // 设置User-Agent
      operateLog.setUserAgent(ServletUtils.getRequest().getHeader(HttpHeaders.USER_AGENT));
      // 设置请求方式
      operateLog.setReqMethod(ServletUtils.getRequest().getMethod());
      // 处理设置注解上的参数
      getControllerMethodDescription(joinPoint, controllerLog, operateLog, jsonResult);
    }
    catch (Exception exp) {
      // 记录本地异常日志
      log.error("异常信息:{}", exp.getMessage());
    }
    finally {
      Long endTime = System.currentTimeMillis();
      operateLog.setDuration(endTime - startTime);
      // 发布事件保存数据库
      SpringUtils.context().publishEvent(operateLog);
    }
  }
​
  /**
   * 获取注解中对方法的描述信息 用于Controller层注解
   * @param log 日志
   * @param operateLog 操作日志
   */
  public void getControllerMethodDescription(JoinPoint joinPoint, Log log, OperateLogEvent operateLog,
      Object jsonResult) throws Exception {
    // 设置标题
    operateLog.setModule(log.module());
    // 设置 action 动作
    operateLog.setBusinessType(log.businessType().ordinal());
    // 设置操作人类别
    operateLog.setUserType(log.operatorUserType().ordinal());
    // 是否需要保存 request,参数和值
    if (log.isSaveRequestData()) {
      // 获取参数的信息,传入到数据库中。
      setRequestValue(joinPoint, operateLog, log.excludeParamNames());
    }
    // 是否需要保存 response,参数和值
    if (log.isSaveResponseData() && ObjectUtil.isNotNull(jsonResult)) {
      R resp = JSONUtil.toBean(JSONUtil.toJsonStr(jsonResult), R.class);
      operateLog.setRespMsg(resp.getMsg());
      operateLog.setRespCode(resp.getCode());
      operateLog.setRespResult(StrUtil.sub(JSONUtil.toJsonStr(jsonResult), 0, 2000));
    }
  }
​
  /**
   * 获取请求的参数,放到log中
   * @param operLog 操作日志
   * @throws Exception 异常
   */
  private void setRequestValue(JoinPoint joinPoint, OperateLogEvent operLog, String[] excludeParamNames)
      throws Exception {
    Map<String, String> paramsMap = ServletUtils.getParamMap(ServletUtils.getRequest());
    String requestMethod = operLog.getReqMethod();
    if (MapUtil.isEmpty(paramsMap) && HttpMethod.PUT.name().equals(requestMethod)
        || HttpMethod.POST.name().equals(requestMethod)) {
      String params = argsArrayToString(joinPoint.getArgs(), excludeParamNames);
      operLog.setReqParams(StrUtil.sub(params, 0, 2000));
    }
    else {
      MapUtil.removeAny(paramsMap, EXCLUDE_PROPERTIES);
      MapUtil.removeAny(paramsMap, excludeParamNames);
      operLog.setReqParams(StrUtil.sub(JSONUtil.toJsonStr(paramsMap), 0, 2000));
    }
  }
​
  /**
   * 参数拼装
   */
  private String argsArrayToString(Object[] paramsArray, String[] excludeParamNames) {
    StringJoiner params = new StringJoiner(" ");
    if (ArrayUtil.isEmpty(paramsArray)) {
      return params.toString();
    }
    for (Object o : paramsArray) {
      if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
        String str = JSONUtil.toJsonStr(o);
        Dict dict = JsonUtils.parseMap(str);
        if (MapUtil.isNotEmpty(dict)) {
          MapUtil.removeAny(dict, EXCLUDE_PROPERTIES);
          MapUtil.removeAny(dict, excludeParamNames);
          str = JSONUtil.toJsonStr(dict);
        }
        params.add(str);
      }
    }
    return params.toString();
  }
​
  /**
   * 判断是否需要过滤的对象。
   * @param o 对象信息。
   * @return 如果是需要过滤的对象,则返回true;否则返回false。
   */
  @SuppressWarnings("rawtypes")
  public boolean isFilterObject(final Object o) {
    Class<?> clazz = o.getClass();
    if (clazz.isArray()) {
      return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
    }
    else if (Collection.class.isAssignableFrom(clazz)) {
      Collection collection = (Collection) o;
      for (Object value : collection) {
        return value instanceof MultipartFile;
      }
    }
    else if (Map.class.isAssignableFrom(clazz)) {
      Map map = (Map) o;
      for (Object value : map.values()) {
        return value instanceof MultipartFile;
      }
    }
    return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
        || o instanceof BindingResult;
  }
​
}

异步调用日志服务

less 复制代码
@Slf4j
@Component
@RequiredArgsConstructor
public class LogEventListener {
​
  private final RemoteLogService remoteLogService;
​
  /**
   * 保存系统日志记录
   */
  @Async
  @EventListener
  public void saveLog(OperateLogEvent operateLog) {
    log.info("保存系统日志记录落库「{}」", JSONUtil.toJsonStr(operateLog));
    remoteLogService.saveSysOperateLog(BeanUtil.copyProperties(operateLog, SysOperateLog.class));
  }
​
}

自动注入日志类

org.springframework.boot.autoconfigure.AutoConfiguration.imports

c 复制代码
com.csp.mingyue.common.log.event.LogEventListener
com.csp.mingyue.common.log.aspect.LogAspect

系统操作日志表设计

sql 复制代码
DROP TABLE IF EXISTS sys_operate_log;
CREATE TABLE sys_operate_log (
    operate_log_id    BIGINT(20)         NOT NULL                 COMMENT '操作日志ID',
    module            VARCHAR(50)        DEFAULT ''               COMMENT '模块',
    business_type     INT(2)             DEFAULT 0                COMMENT '业务类型(0其它 1新增 2修改 3删除)',
    method            VARCHAR(100)       DEFAULT ''               COMMENT '方法名称',
    service_id        VARCHAR(32)        DEFAULT NULL             COMMENT '服务ID',
    user_id           BIGINT(20)         NOT NULL                 COMMENT '用户ID',
    user_name         VARCHAR(50)        NOT NULL                 COMMENT '用户账号',
    user_type         TINYINT(1)         DEFAULT 0                COMMENT '用户类型(0其它 1系统用户)',
    user_agent        VARCHAR(1000)      DEFAULT NULL             COMMENT '用户代理',
    req_ip            VARCHAR(128)       DEFAULT ''               COMMENT '请求IP',
    req_address       VARCHAR(255)       DEFAULT ''               COMMENT '请求地点',
    req_url           VARCHAR(255)       DEFAULT ''               COMMENT '请求URL',
    req_method        VARCHAR(20)        DEFAULT NULL             COMMENT '请求方式',
    req_params        TEXT               DEFAULT NULL             COMMENT '请求参数',
    duration          BIGINT             NOT NULL                 COMMENT '执行时长,单位(ms)',
    resp_code         INT                DEFAULT NULL             COMMENT '结果码',
    resp_msg          VARCHAR(512)       NULL DEFAULT ''          COMMENT '结果提示',
    resp_result       VARCHAR(2000)      DEFAULT ''               COMMENT '返回参数',
    status            CHAR(1)            DEFAULT 0                COMMENT '操作状态(0正常 1异常)',
    exception         TEXT               DEFAULT NULL             COMMENT '异常信息',
    operate_time      DATETIME           NOT NULL                 COMMENT '操作时间',
    is_deleted        CHAR(1)            DEFAULT '0'              COMMENT '删除标志(0正常,1删除)',
    create_by         VARCHAR(64)        DEFAULT ''               COMMENT '创建者',
    create_time       DATETIME           DEFAULT NULL             COMMENT '创建时间',
    update_by         VARCHAR(64)        DEFAULT ''               COMMENT '更新者',
    update_time       DATETIME           DEFAULT NULL             COMMENT '更新时间',
    PRIMARY KEY (operate_log_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=DYNAMIC COMMENT='系统操作日志';

测试日志注解

使用注解

@Log(module = "用户管理", businessType = BusinessType.DELETE)

less 复制代码
@DeleteMapping("{userId}")
@Log(module = "用户管理", businessType = BusinessType.DELETE)
@Operation(summary = "删除用户", parameters = { @Parameter(name = "userId", description = "用户ID", required = true) })
public R<Boolean> delUser(@PathVariable Long userId) {
  return R.ok(sysUserService.delUser(userId));
}

调用接口

调用完成后查看数据库是否存在该操作记录即可

arduino 复制代码
curl -X 'DELETE'   'http://192.168.63.114:7100/system/sysUser/111111111'   -H 'accept: */*'   -H 'Authorization: UWapduuggQcNSqg1oQZ17ZyfPHDxxt8Q'
相关推荐
盖世英雄酱581367 分钟前
java 深度调试【第一章:堆栈分析】
java·后端
lastHertz24 分钟前
Golang 项目中使用 Swagger
开发语言·后端·golang
渣哥25 分钟前
面试高频:Spring 事务传播行为的核心价值是什么?
javascript·后端·面试
调试人生的显微镜30 分钟前
iOS 代上架实战指南,从账号管理到使用 开心上架 上传IPA的完整流程
后端
本就一无所有 何惧重新开始34 分钟前
Redis技术应用
java·数据库·spring boot·redis·后端·缓存
低音钢琴1 小时前
【SpringBoot从初学者到专家的成长11】Spring Boot中的application.properties与application.yml详解
java·spring boot·后端
越千年1 小时前
用Go实现类似WinGet风格彩色进度条
后端
蓝色汪洋1 小时前
string字符集
java·开发语言
卿言卿语1 小时前
CC1-二叉树的最小深度
java·数据结构·算法·leetcode·职场和发展
淳朴小学生1 小时前
静态代理和动态代理
后端