spring boot 存储操作日志(mysql)

485 阅读9分钟

spring boot 存储操作日志(mysql)

记录聊天app, 操作日志存储实现

操作日志实体类

SQL(table 数据表)

create table if not exists `br_operate_log`
(
    `operate_type`   smallint unsigned null comment '操作人类别',
    `business_type`  smallint unsigned null comment '操作功能',
    `method`         varchar(16)       null comment '请求方法',
    `topic`          varchar(32)       null comment '主题',
    `summary`        varchar(120)      null comment '概况',
    `notes`          varchar(255)      null comment '备注',
    `url`            varchar(255)      null comment '请求url',
    `params`         varchar(514)      null comment '请求参数',
    `response`       varchar(2048)     null comment '请求响应体',
    `is_success`     boolean           null comment '成功处理',
​
    `id`             bigint(20) unsigned primary key,
    `create_at`      datetime          null comment '创建时间',
    `create_by`      bigint unsigned   null comment '创建人',
    `create_ip`      varchar(64)       null comment '创建ip',
    `create_address` varchar(64)       null comment '创建ip地址 "国家|区域|省|市|宽带"',
    `update_at`      datetime          null comment '最后更新时间',
    `update_by`      bigint unsigned   null comment '最后更新人',
    `update_ip`      varchar(64)       null comment '最后更新ip',
    `update_address` varchar(64)       null comment '最后更新ip地址 "国家|区域|省|市|宽带"',
    `remark`         varchar(256)      null comment '备注',
    `version`        int     default 0 null comment '乐观锁',
    `is_deleted`     boolean default false comment '逻辑删除 0 未删除 `null` 删除',
    `is_status`      boolean default true comment '是否启用 0 关闭  1 开启'
) auto_increment 100 comment '日志记录表';

BaseeEntity(基础实体类)

package com.bluereba.br_pojo.entity;
​
import com.baomidou.mybatisplus.annotation.*;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
​
import java.io.Serializable;
import java.time.LocalDateTime;
​
/**
 * @author tuuuuuun
 * @since 2022/10/10 0:12
 */@Data
@ApiModel(value = "基础实体类", description = "")
public class BaseEntity implements Serializable {
​
  private static final long serialVersionUID = 1L;
​
  @TableId(value = "id", type = IdType.ASSIGN_ID)
  private Long id;
​
  @ApiModelProperty("创建时间")
  @TableField(value = "create_at", fill = FieldFill.INSERT)
  private LocalDateTime createAt;
​
  @ApiModelProperty("创建人")
  @TableField(value = "create_by", fill = FieldFill.INSERT)
  private Long createBy;
​
  @ApiModelProperty("创建ip")
  @TableField(value = "create_ip", fill = FieldFill.INSERT)
  private String createIp;
​
  @ApiModelProperty("创建ip地址 "国家|区域|省|市|宽带"")
  @TableField(value = "create_address", fill = FieldFill.INSERT)
  private String createAddress;
​
  @ApiModelProperty("最后更新时间")
  @TableField(value = "update_at", fill = FieldFill.INSERT_UPDATE)
  private LocalDateTime updateAt;
​
  @ApiModelProperty("最后更新人")
  @TableField(value = "update_by", fill = FieldFill.INSERT_UPDATE)
  private Long updateBy;
​
  @ApiModelProperty("最后更新ip")
  @TableField(value = "update_ip", fill = FieldFill.INSERT_UPDATE)
  private String updateIp;
​
  @ApiModelProperty("最后更新ip地址 "国家|区域|省|市|宽带"")
  @TableField(value = "update_address", fill = FieldFill.INSERT_UPDATE)
  private String updateAddress;
​
  @ApiModelProperty("备注")
  @TableField("remark")
  private String remark;
​
  @ApiModelProperty("乐观锁")
  @TableField("version")
  @Version
  private Integer version;
​
  @ApiModelProperty("逻辑删除 0 未删除 `null` 删除")
  @TableField("is_deleted")
  @TableLogic
  private Boolean isDeleted;
​
  @ApiModelProperty("是否启用 0 关闭  1 开启")
  @TableField("is_status")
  private Boolean isStatus;
}

OperateLogEntity(日志实体类)

package com.bluereba.br_pojo.entity;
​
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.bluereba.br_pojo.entity.type.BusinessType;
import com.bluereba.br_pojo.entity.type.OperatorType;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.*;
​
/**
 * <p>
 * 日志记录表
 * </p>
 *
 * @author tuuuuuun
 * @since 2022-10-19 02:09:15
 */@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@TableName("br_operate_log")
@EqualsAndHashCode(callSuper = true)
@ApiModel(value = "日志实体", description = "记录操作日志")
public class OperateLogEntity extends BaseEntity {
​
    private static final long serialVersionUID = 1L;
​
    /**
     * <span style="color: #33c481;font-size: 1.2em">操作参数<spans/>
     */
    @TableField("operate_type")
    @ApiModelProperty(value = "操作人类别", example = "", notes = "")
    private OperatorType operateType;
​
    @TableField("business_type")
    @ApiModelProperty(value = "操作功能", example = "", notes = "")
    private BusinessType businessType;
​
    @TableField("is_success")
    @ApiModelProperty(value = "成功处理", example = "", notes = "")
    private Boolean isSuccess;
​
    @TableField("topic")
    @ApiModelProperty(value = "操作模块", example = "", notes = "")
    private String topic;
​
    @TableField("summary")
    @ApiModelProperty(value = "操作概括", example = "", notes = "")
    private String summary;
​
    @TableField("notes")
    @ApiModelProperty(value = "操作说明", example = "", notes = "")
    private String notes;
​
    /**
     * <span style="color: #33c481;font-size: 1.2em">请求参数<spans/>
     */
    @TableField("url")
    @ApiModelProperty(value = "请求url", example = "", notes = "")
    private String url;
​
    @TableField("method")
    @ApiModelProperty(value = "请求方法", example = "", notes = "")
    private String method;
​
    @TableField("params")
    @ApiModelProperty(value = "请求参数", example = "", notes = "")
    private String params;
​
    /**
     * <span style="color: #33c481;font-size: 1.2em">响应参数<spans/>
     */
    @ApiModelProperty(value = "响应实体", example = "", notes = "")
    private String response;
}
​

MybatisPlus

Config

@MapperScan("com.bluereba.br_dao.mapper") 指定 mapper 包路径,插件无所谓。也可以直接放到Spring Boot 启动类上

package com.bluereba.br_stomp.config;
​
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
​
/**
 * MybatisPlus 配置类
 *
 * @author tuuuuuun
 * @since 2022/7/15 15:13
 */@Configuration
@MapperScan("com.bluereba.br_dao.mapper")
public class MyBatisPlusConfig {
​
  @Bean
  public MybatisPlusInterceptor mybatisPlusInterceptor() {
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    // TODO.1 分页器
    interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
    // TODO.2 乐观锁
    interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
    // TODO.3 阻断全表更新、删除的操作
    interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
    return interceptor;
  }
}
​

Mapper

BaseMapper 提供了许多单表操作的基础方法

package com.bluereba.br_dao.mapper;
​
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.bluereba.br_pojo.entity.OperateLogEntity;
​
/**
 * <p>
 * 日志记录表 Mapper 接口
 * </p>
 *
 * @author tuuuuuun
 * @since 2022-10-19 02:09:15
 */
public interface OperateLogMapper extends BaseMapper<OperateLogEntity> {}

Service

IService 定义了基础的业务方法,需要实现

package com.bluereba.br_service.service;
​
import com.baomidou.mybatisplus.extension.service.IService;
import com.bluereba.br_pojo.entity.OperateLogEntity;
​
/**
 * <p>
 * 日志记录表 服务类
 * </p>
 *
 * @author tuuuuuun
 * @since 2022-10-19 02:09:15
 */
public interface OperateLogService extends IService<OperateLogEntity> {}

ServiceImpl

ServiceImpl 实现了基础的业务方法

package com.bluereba.br_service.service.impl;
​
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bluereba.br_dao.mapper.OperateLogMapper;
import com.bluereba.br_pojo.entity.OperateLogEntity;
import com.bluereba.br_service.service.OperateLogService;
import org.springframework.stereotype.Service;
​
/**
 * <p>
 * 日志记录表 服务实现类
 * </p>
 *
 * @author tuuuuuun
 * @since 2022-10-19 02:09:15
 */@Service
public class OperateLogServiceImpl extends ServiceImpl<OperateLogMapper, OperateLogEntity> implements OperateLogService {}

Aop

OperatorType 枚举 (操作人类型)

@EnumValue 指定数据库存储的字段

@JsonValue 响应前端的字段

package com.bluereba.br_pojo.entity.type;
​
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor;
import lombok.Getter;
​
/**
 * @author tuuuuuun
 * @since 2022/10/18 6:36
 */
@Getter
@AllArgsConstructor
public enum OperatorType {
  /**
   * 操作人类型
   */
​
  USER(1, "user"),
  GROUP(2, "group"),
  SYSTEM(3, "system");
​
  @EnumValue
  private final Integer id;
​
  @JsonValue
  private final String type;
}

BusinessType (操作动作 类型)

@EnumValue 指定数据库存储的字段

@JsonValue 响应前端的字段

package com.bluereba.br_pojo.entity.type;
​
import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor;
import lombok.Getter;
​
/**
 * @author tuuuuuun
 * @since 2022/10/18 6:36
 */
@Getter
@AllArgsConstructor
public enum BusinessType {
  /**
   * 操作动作
   */
​
  INSERT(1, "新增操作"),
  LOGIN(2, "登录操作"),
  LOGOUT(3, "登出操作"),
  UPDATE(4, "编辑操作"),
  DELETE(5, "删除操作"),
  FORCE(6, "强退"),
  IMPORT(7, "导入"),
  OTHER(8, "其他"),
  GENERATE(9, "生成代码"),
  CLEAN(10, "清空数据"),
  GRANT(11, "授权");
​
  @EnumValue
  private final Integer id;
​
  @JsonValue
  private final String type;
}

操作日志注解

package com.bluereba.br_operate_log;
​
import com.bluereba.br_pojo.entity.type.BusinessType;
import com.bluereba.br_pojo.entity.type.OperatorType;
​
import java.lang.annotation.*;
​
/**
 * @author tuuuuuun
 * @since 2022/10/18 6:32
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperateLog {
​
  // 操作模块
  String topic() default "";
​
  // 操作概括
  String summary() default "";
​
  // 操作说明
  String notes() default "";
​
  // 操作功能(OTHER其他、INSERT新增、UPDATE修改、DELETE删除、GRANT授权、EXPORT导出、IMPORT导入、FORCE强退、GENCODE生成代码、CLEAN清空数据)
  BusinessType businessType() default BusinessType.OTHER;
​
  // 操作人类型(OTHER其他、MANAGE后台用户、MOBILE手机端用户)
  OperatorType operatorType() default OperatorType.USER;
​
  // 保存请求的参数
  boolean request() default true;
​
  // 保存响应的参数
  boolean response() default true;
​
  // 保存详情数据 {{@code ApiEntity<?> }}  save ? data
  boolean detail() default false;
​
}
​

OperateHelper(构建日志实体类辅助类)

  • 响应参数
  • 请求参数
  • 操作参数

ApiEntity(响应实体类)

package com.bluereba.br_common.api;
​
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
​
import java.time.LocalDateTime;
​
/**
 * @author tuuuuuun
 * @since 2022/7/15 16:05
 */
@Data
@ApiModel("响应对象")
@AllArgsConstructor
@NoArgsConstructor
public class ApiEntity<T> {
​
  @ApiModelProperty(value = "状态码")
  Integer status;
​
  @ApiModelProperty(value = "消息")
  String message;
​
  @ApiModelProperty(value = "数据")
  T data;
​
  @ApiModelProperty(value = "时间")
  LocalDateTime timestamp;
​
  public ApiEntity(Integer status, T data) {
    this.status = status;
    this.data = data;
  }
​
  public ApiEntity(Integer status, String message, T data) {
    this.status = status;
    this.message = message;
    this.data = data;
  }
​
  public ApiEntity(Integer status, String message) {
    this.status = status;
    this.message = message;
  }
}
package com.bluereba.br_operate_log.utils;
​
import com.bluereba.br_common.api.ApiEntity;
import com.bluereba.br_operate_log.OperateLog;
import com.bluereba.br_pojo.entity.OperateLogEntity;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
​
import javax.servlet.http.HttpServletRequest;
import java.util.Optional;
​
/**
 * 日志实体构建 辅助类
 *
 * @author tuuuuuun
 * @since 2022/10/18 20:15
 */
​
@AllArgsConstructor
@NoArgsConstructor
@Component
public class OperateHelper {
​
  private static ObjectMapper objectMapper;
​
  private OperateLogEntity.OperateLogEntityBuilder builder = null;
  private JoinPoint joinPoint = null;
  private ApiEntity<?> result = null;
​
  public static OperateHelper builder(@NonNull JoinPoint joinPoint,
                                      @Nullable ApiEntity<?> result
  ) {
    return new OperateHelper(OperateLogEntity.builder(), joinPoint, result);
  }
​
  /**
   * 获取 {{@code ApiOperation}} 注解
   *
   * @return ApiOperation annotation
   */
  public Optional<ApiOperation> getApiOperationAnnotation() {
    MethodSignature signature = (MethodSignature) joinPoint.getSignature();
    ApiOperation operation = signature.getMethod().getAnnotation(ApiOperation.class);
    return Optional.ofNullable(operation);
  }
​
  /**
   * 获取 {{@code Api}} 注解
   *
   * @return Api annotation
   */
  public Optional<Api> getApiAnnotation() {
    Api api = joinPoint.getTarget().getClass()
        .getAnnotation(Api.class);
    return Optional.ofNullable(api);
  }
​
  /**
   * 获取 {{@code OperateLog}} 注解
   *
   * @return OperateLog annotation
   */
  public Optional<OperateLog> getOperateLogAnnotation() {
    MethodSignature signature = (MethodSignature) joinPoint.getSignature();
    OperateLog operateLog = signature.getMethod().getAnnotation(OperateLog.class);
    return Optional.ofNullable(operateLog);
  }
​
  /**
   * 存储请求数据
   *
   * @return 结果
   */
  public boolean saveRequestData() {
    Optional<OperateLog> operateLog = getOperateLogAnnotation();
    if (operateLog.isPresent()) {
      OperateLog operate = operateLog.get();
​
      // 存储 请求参数
      return operate.response();
    }
    return false;
  }
​
​
  /**
   * 存储响应数据
   *
   * @return 结果
   */
  public boolean saveResponseData() {
    Optional<OperateLog> operateLog = getOperateLogAnnotation();
    if (operateLog.isPresent()) {
      OperateLog operate = operateLog.get();
​
      // 存储 响应参数
      return operate.response();
    }
    return false;
  }
​
  @Autowired
  public void setObjectMapper(ObjectMapper objectMapper) {
    OperateHelper.objectMapper = objectMapper;
  }
​
  /**
   * <span style="color: #33c481;font-size: 1.2em">操作参数<spans/>
   */
  public OperateHelper operate() {
    getOperateLogAnnotation().ifPresent(annotation -> {
      // 操作功能
      builder.businessType(annotation.businessType());
​
      // 操作人类型
      builder.operateType(annotation.operatorType());
​
      // 日志主题
      builder.topic(annotation.topic());
​
      // 日志概括
      builder.summary(annotation.summary());
​
      // 日志说明
      builder.notes(annotation.notes());
    });
​
    // 覆盖操作
    getApiAnnotation().ifPresent(annotation -> {
      String topic = StringUtils.join(annotation.tags(), "-");
      if (StringUtils.isNotBlank(topic)) {
        // 日志主题
        builder.topic(topic);
      }
    });
​
    // 覆盖操作
    getApiOperationAnnotation().ifPresent(annotation -> {
      // 日志概括
      if (StringUtils.isNotBlank(annotation.value())) {
        builder.summary(annotation.value());
      }
​
      // 日志说明
      if (StringUtils.isNotBlank(annotation.notes())) {
        builder.notes(annotation.notes());
      }
    });
    return this;
  }
​
  /**
   * <span style="color: #33c481;font-size: 1.2em">请求参数<spans/>
   */
  public OperateHelper request() throws Exception {
    if (!saveRequestData()) {
      return this;
    }
​
    ServletRequestAttributes requestAttributes = (ServletRequestAttributes)
        RequestContextHolder.getRequestAttributes();
​
    if (requestAttributes != null) {
      HttpServletRequest request = requestAttributes.getRequest();
      Object[] requestParam = joinPoint.getArgs();
      String method = request.getMethod();
      String url = request.getRequestURI();
​
      String requestParams = objectMapper.writeValueAsString(requestParam);
      builder
          .params(requestParams)
          .method(method)
          .url(url);
    }
    return this;
  }
​
  /**
   * <span style="color: #33c481;font-size: 1.2em">响应参数<spans/>
   */
  public OperateHelper response() throws JsonProcessingException {
    if (!saveResponseData()) {
      return this;
    }
​
    // 响应实体
    Boolean saveDetail = getOperateLogAnnotation()
        .map(OperateLog::detail)
        .orElse(false);
​
    String result;
    if (!saveDetail) {
      // copy result, 直接修改默认的 result,会影响返回给前端的数据
      ApiEntity<String> copyEntity = new ApiEntity<>();
      BeanUtils.copyProperties(this.result, copyEntity);
      copyEntity.setData("*");
​
      result = objectMapper.writeValueAsString(copyEntity);
    } else {
      result = objectMapper.writeValueAsString(this.result);
    }
​
    builder.response(result);
    return this;
  }
​
  public OperateHelper success() {
    builder.isSuccess(true);
    return this;
  }
​
  public OperateHelper fail() {
    builder.isSuccess(false);
    return this;
  }
​
  public OperateLogEntity build() {
    return builder.build();
  }
​
}

Mybatis 自动填充

  • 填充基础实体类字段(用户参数,创建、更新参数)

PrincipalContextHolder

获取 Spring Security 上下文用户配置

注: UserDetailsEntity(实现了 UserDetails)

package com.bluereba.br_service.utils;
​
import com.bluereba.br_pojo.entity.security.UserDetailsEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
​
import java.util.Optional;
​
/**
 * @author tuuuuuun
 * @since 2022/10/16 2:28
 */public class PrincipalContextHolder {
​
  /**
   * 获取 当前用户 ID
   *
   * @return 用户 id
   */
  public static Long getUserId() {
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    if (authentication != null) {
      Object principal = authentication.getPrincipal();
      if (principal instanceof UserDetailsEntity) {
        UserDetailsEntity userDetails = (UserDetailsEntity) authentication.getPrincipal();
        return userDetails.getUid();
      }
    }
    return null;
  }
​
  public static Optional<Long> getUserIdOpt() {
    return Optional.ofNullable(getUserId());
  }
}

MyMetaObjectHandler

当插入实体时,会调用 insertFill 方法

当更新实体时,会调用 updateFill 方法

注: 只有构建了实体对象才会调用方法

package com.bluereba.br_web.handler;
​
import cn.hutool.extra.servlet.ServletUtil;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.bluereba.br_common.utils.AddressTemplate;
import com.bluereba.br_service.utils.PrincipalContextHolder;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.ibatis.reflection.MetaObject;
import org.lionsoul.ip2region.xdb.Searcher;
import org.springframework.stereotype.Component;
​
import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
​
/**
 * mybatis 自动填充, 可能会有填充问题
 *
 * @author tuuuuuun
 * @since 2022/7/15 17:54
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class MyMetaObjectHandler implements MetaObjectHandler {
​
  private final HttpServletRequest httpServletRequest;
​
  private final Searcher searcher;
​
  private final AddressTemplate addressTemplate;
​
  @Override
  public void insertFill(MetaObject metaObject) {
    PrincipalContextHolder.getUserIdOpt().ifPresent(uid -> {
      // 填充创建人,更新人
      this.strictInsertFill(metaObject, "createBy", Long.class, uid);
​
      this.strictInsertFill(metaObject, "updateBy", Long.class, uid);
    });
​
    // 填充创建时间 和 更新时间
    this.strictInsertFill(metaObject, "createAt", LocalDateTime::now,
        LocalDateTime.class);
    this.strictInsertFill(metaObject, "updateAt", LocalDateTime::now,
        LocalDateTime.class);
​
    // 填充 ip 已经 ip address
    try {
      // 糊涂包 工具类, 获取实际请求ip (包括nginx代理)
      String ip = ServletUtil.getClientIP(httpServletRequest);
        
      // ip2region 包, 离线将 ip 转 地址("国家|区域|省|市|宽带")
      String address = searcher.search(ip);
​
      this.strictInsertFill(metaObject, "createIp", String.class, ip);
      this.strictInsertFill(metaObject, "createAddress", String.class, address);
​
      this.strictInsertFill(metaObject, "updateIp", String.class, ip);
      this.strictInsertFill(metaObject, "updateAddress", String.class, address);
    } catch (Exception e) {
      log.error(e.getMessage(), e);
    }
  }
​
  @Override
  public void updateFill(MetaObject metaObject) {
    PrincipalContextHolder.getUserIdOpt().ifPresent(uid -> {
      // 填充创建人,更新人
      this.strictUpdateFill(metaObject, "updateBy", Long.class, uid);
    });
​
    // 填充 ip 已经 ip address
    try {
      String ip = ServletUtil.getClientIP(httpServletRequest);
      String address = searcher.search(ip);
      this.strictUpdateFill(metaObject, "updateIp", String.class, ip);
      this.strictUpdateFill(metaObject, "updateAddress", String.class, address);
    } catch (Exception e) {
      log.error(e.getMessage(), e);
    }
​
    // 填充更新时间
    /*this.strictUpdateFill(metaObject, "updateAt", LocalDateTime::now,
        LocalDateTime.class);*/
    this.setFieldValByName("updateAt", LocalDateTime.now(), metaObject);
  }
​
}

Aspect

package com.bluereba.br_operate_log.aspect;
​
import com.bluereba.br_common.api.ApiEntity;
import com.bluereba.br_operate_log.utils.OperateHelper;
import com.bluereba.br_pojo.entity.OperateLogEntity;
import com.bluereba.br_service.service.OperateLogService;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
​
/**
 * @author tuuuuuun
 * @since 2022/3/2 20:43
 */@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class OperateLogAspect {
​
  private final OperateLogService operateLogService;
​
  /**
   * 通过注解类标识 控制操作 (OperateLog 注解标识)
   */
  @Pointcut("@annotation(com.bluereba.br_operate_log.OperateLog)")
  public void operateLog() {
  }
​
  /**
   * 方法成功执行
   */
  @Async
  @SneakyThrows
  @AfterReturning(value = "operateLog()", returning = "result")
  public void afterReturnOperate(JoinPoint joinPoint, ApiEntity<?> result) {
    OperateLogEntity operateEntity = OperateHelper.builder(joinPoint, result)
        .request()
        .response()
        .operate()
        .success()
        .build();
​
    // 由 mybatis plus 提供 向数据库中插入一条数据
    operateLogService.save(operateEntity);
  }
​
​
  /**
   * 方法执行中异常
   */
  @Async
  @SneakyThrows
  @AfterThrowing(pointcut = "operateLog()", throwing = "e")
  public void afterThrowingOperate(JoinPoint joinPoint, Throwable e) {
    OperateLogEntity operateEntity = OperateHelper.builder(joinPoint, null)
        .request()
        .operate()
        .fail()
        .build();
​
    // 由 mybatis plus 提供 向数据库中插入一条数据
    operateLogService.save(operateEntity);
  }
​
}
​

处理 RestController 返回的 ApiEntity 实体

  • 当消息为空 或 消息为默认消息 将替换为 swagger注解 {{@ApiOperation value}} 值
package com.bluereba.br_common.handler;
​
​
import com.bluereba.br_common.api.ApiEntity;
import com.bluereba.br_common.api.type.base.ApiEnum;
import io.swagger.annotations.ApiOperation;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
​
import java.time.LocalDateTime;
​
/**
 * @author tuuuuuun
 * @since 2022/10/17 21:38
 */
​
@RestControllerAdvice
public class ResponseBodyHandler implements ResponseBodyAdvice<ApiEntity<?>> {
​
  @Override
  public boolean supports(@NonNull MethodParameter returnType,
                          @NonNull Class<? extends HttpMessageConverter<?>> converterType) {
​
    // 只处理 {{@code ApiEntity }} 对象
    Class<?> c = returnType.getParameterType();
    return c == ApiEntity.class;
  }
​
  @Override
  public ApiEntity<?> beforeBodyWrite(@Nullable ApiEntity<?> body,
                                      @NonNull MethodParameter returnType,
                                      @NonNull MediaType selectedContentType,
                                      @NonNull Class<? extends HttpMessageConverter<?>> selectedConverterType,
                                      @NonNull ServerHttpRequest request,
                                      @NonNull ServerHttpResponse response) {
​
    if (body != null) {
      // 获取 {{@code ApiOperation}} 注解
      ApiOperation apiOperation = returnType.getAnnotatedElement()
          .getAnnotation(ApiOperation.class);
​
      if (apiOperation != null) {
        // {{@code ApiOperation}} 注解 @value
        String message = apiOperation.value();
​
        // 响应对象 消息
        String bodyMessage = body.getMessage();
​
        // 默认成功消息
        String successMessage = ApiEnum.OK.getMessage();
​
        // 当消息为空 或 消息为默认消息 将替换为 swagger注解{{@ApiOperation value}}
        if (StringUtils.isEmpty(bodyMessage) || successMessage.equals(bodyMessage)) {
          body.setMessage(message);
        }
      }
      body.setTimestamp(LocalDateTime.now());
    }
    return body;
  }
}
​

使用

UserController

success 静态方法是对 ApiEnity 进行的封装

package com.bluereba.br_stomp.controller;
​
import com.bluereba.br_common.api.ApiEntity;
import com.bluereba.br_operate_log.OperateLog;
import com.bluereba.br_pojo.dto.UserLoginDto;
import com.bluereba.br_pojo.entity.UserEntity;
import com.bluereba.br_pojo.entity.type.BusinessType;
import com.bluereba.br_pojo.vo.UserRegisterVo;
import com.bluereba.br_service.service.UserService;
import com.github.xiaoymin.knife4j.annotations.ApiSupport;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
​
import static com.bluereba.br_common.api.ApiRes.success;
​
/**
 * @author tuuuuuun
 * @since 2022/10/9 19:15
 */@Api(tags = "用户模块")  // Api.tags 将成为 操作日志的主题(topic)
@RestController
@RequestMapping("user")
@RequiredArgsConstructor
public class UserController {
​
  private final UserService userService;
​
  @PostMapping("/login")
  @ApiOperation(value = "用户登陆")  // ApiOperation.value 将成为 操作日志的操作概况(summary)
  @OperateLog(businessType = BusinessType.LOGIN)
  public ApiEntity<UserLoginDto> login(@RequestBody UserRegisterVo vo) {
    UserLoginDto dto = userService.signUp(vo);
    return success(dto);
  }
    
  @DeleteMapping("/logout")
  @ApiOperation(value = "用户退出")
  @OperateLog(businessType = BusinessType.LOGOUT)
  public ApiEntity<?> logout() {
    userService.logout();
    return success();
  }
​
  @GetMapping("/info")
  @ApiOperation(value = "用户信息")
  public ApiEntity<UserEntity> info() {
    UserEntity info = userService.info();
    return success(info);
  }
​
}

脱敏

UserRegisterVo

package com.bluereba.br_pojo.vo;
​
import com.bluereba.br_pojo.json.ChineseNameSerialize;
import com.bluereba.br_pojo.json.PasswordSerialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
​
import javax.validation.constraints.NotNull;
import java.io.Serializable;
​
/**
 * 登录视图
 *
 * @author tuuuuuun
 * @since 2022/10/10 0:00
 */
​
@Data
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(value = "登陆vo", description = "")
public class UserRegisterVo implements Serializable {
​
  @NotNull(message = "登陆用户名不能为空")
  @ApiModelProperty(value = "用户名", example = "", notes = "")
  @JsonSerialize(using = ChineseNameSerialize.class)
  private String username;
​
  @NotNull(message = "用户密码不能为空")
  @ApiModelProperty(value = "用户密码", example = "", notes = "")
  @JsonSerialize(using = PasswordSerialize.class)
  private String password;
​
  @ApiModelProperty(value = "记住我", example = "false",
      notes = "true, false, 1, 0, True, False")
  private Boolean readme = false;
​
}

ChineseNameSerialize(中文名序列化)

package com.bluereba.br_pojo.json;
​
import cn.hutool.core.util.DesensitizedUtil;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
​
import java.io.IOException;
​
/**
 * @author tuuuuuun
 * @since 2022/10/18 23:00
 */
public class ChineseNameSerialize extends JsonSerializer<String> {
​
  @Override
  public void serialize(String v, JsonGenerator g, SerializerProvider s)
      throws IOException {
​
    // DesensitizedUtil 糊涂包下的脱敏工具类, 
    // chineseName 方法是对首字符后的字符串串进行 * 替换 
    g.writeString(DesensitizedUtil.chineseName(v));
  }
}
​

PasswordSerialize(密码序列化)

package com.bluereba.br_pojo.json;
​
import cn.hutool.core.util.DesensitizedUtil;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
​
import java.io.IOException;
​
/**
 * @author tuuuuuun
 * @since 2022/10/18 23:00
 */
public class PasswordSerialize extends JsonSerializer<String> {
​
  @Override
  public void serialize(String v, JsonGenerator g, SerializerProvider s)
      throws IOException {
​
    // DesensitizedUtil 糊涂包下的脱敏工具类, 
    // password 方法是对全字符串进行 * 替换 
    g.writeString(DesensitizedUtil.password(v));
  }
}
​

效果

Success


接口

image-20221019191923290.png

请求登陆接口

image-20221019191512697.png

数据库存储

image-20221019191810470.png

Fail


接口

image-20221019192322103.png

数据存储

image-20221019192419600.png