java每日精进 4.29【框架之自动记录日志并插入如数据库流程分析】

1.日志记录注解(LogRecord)

java 复制代码
@Repeatable(LogRecords.class)
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface LogRecord {
    String success();

    String fail() default "";

    String operator() default "";

    String type();

    String subType() default "";

    String bizNo();

    String extra() default "";

    String condition() default "";

    String successCondition() default "";
}

1.无默认值的字段必须显式指定;

2.@Repeatable(让被注解的注解可重复使用。当一个注解被 @Repeatable 注解修饰时,就意味着在同一个元素上能够多次使用该注解)

3.@Target

  • 参数解释
    • ElementType.METHOD:表明该注解可用于方法。
    • ElementType.TYPE:表明该注解可用于类、接口、枚举等类型。

4.@Retention(表示该注解在运行时可见,这样就能通过反射机制在运行时获取注解信息)

5.@Inherited(若一个注解被 @Inherited 修饰,那么该注解会被子类继承。也就是说,若一个类被该注解标注,其所有子类也会自动拥有这个注解

2.Services层使用注解,并给出字段的值

java 复制代码
@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService{
    @Override
    @Transactional
    @LogRecord(type = SYSTEM_USER_TYPE, subType = SYSTEM_USER_CREATE_SUB_TYPE,bizNo ="{{#user.id}}",
    success = SYSTEM_USER_CREATE_SUCCESS, fail = "{{#user.username}}创建失败!")
    public boolean creatUser(User user) {
        User userIfExist = this.getOne(new QueryWrapper<User>().eq("username", user.getUsername()));
        if (userIfExist != null) {
            log.error("用户名已存在");
            return false;
        }
        return this.save(user);
    }
}

常量配置

3.过滤器进行请求过滤

1.ApiRequestFilter

作为抽象基类,提供基本的请求过滤逻辑,决定哪些请求需要被子类处理

对 HTTP 请求进行过滤,仅对以管理员 API 或应用 API 前缀开头的请求进行处理

java 复制代码
/**
 * 过滤 /admin-api、/app-api 等 API 请求的过滤器
 *
 * @author 芋道源码
 */
@RequiredArgsConstructor
public abstract class ApiRequestFilter extends OncePerRequestFilter {

    protected final WebProperties webProperties;

    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) {
        // 只过滤 API 请求的地址
        String apiUri = request.getRequestURI().substring(request.getContextPath().length());
        return !StrUtil.startWithAny(apiUri, webProperties.getAdminApi().getPrefix(), webProperties.getAppApi().getPrefix());
    }

}

2.ApiAccessLogFilter

java 复制代码
/**
 * API 访问日志 Filter
 *
 * 目的:记录 API 访问日志到数据库中
 *
 * @author 芋道源码
 */
@Slf4j
public class ApiAccessLogFilter extends ApiRequestFilter {

    //静态常量数组,包含了需要在请求和响应中脱敏的敏感字段名,如密码、令牌等
    private static final String[] SANITIZE_KEYS = new String[]{"password", "token", "accessToken", "refreshToken"};
   //表示当前应用的名称,用于日志记录
    private final String applicationName;
    //表示API访问日志的API
    private final ApiAccessLogApi apiAccessLogApi;

    public ApiAccessLogFilter(WebProperties webProperties, String applicationName, ApiAccessLogApi apiAccessLogApi) {
        super(webProperties);
        this.applicationName = applicationName;
        this.apiAccessLogApi = apiAccessLogApi;
    }

    @Override
    @SuppressWarnings("NullableProblems")
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        // 获得开始时间
        LocalDateTime beginTime = LocalDateTime.now();
        // 提前获得参数,避免 XssFilter 过滤处理
        Map<String, String> queryString = ServletUtils.getParamMap(request);
        // 缓存请求体,
        String requestBody = null;
        if (ServletUtils.isJsonRequest(request)) {
            requestBody = ServletUtils.getBody(request);
            request.setAttribute(REQUEST_BODY_ATTRIBUTE, requestBody);
        }

        try {
            // 继续过滤器
            filterChain.doFilter(request, response);
            // 正常执行,记录日志
            createApiAccessLog(request, beginTime, queryString, requestBody, null);
        } catch (Exception ex) {
            // 异常执行,记录日志
            createApiAccessLog(request, beginTime, queryString, requestBody, ex);
            throw ex;
        }
    }

    private void createApiAccessLog(HttpServletRequest request, LocalDateTime beginTime,
                                    Map<String, String> queryString, String requestBody, Exception ex) {
        ApiAccessLogCreateReqDTO accessLog = new ApiAccessLogCreateReqDTO();
        try {
            boolean enable = buildApiAccessLog(accessLog, request, beginTime, queryString, requestBody, ex);
            if (!enable) {
                return;
            }
            apiAccessLogApi.createApiAccessLogAsync(accessLog);
        } catch (Throwable th) {
            log.error("[createApiAccessLog][url({}) log({}) 发生异常]", request.getRequestURI(), toJsonString(accessLog), th);
        }
    }

    private boolean buildApiAccessLog(ApiAccessLogCreateReqDTO accessLog, HttpServletRequest request, LocalDateTime beginTime,
                                      Map<String, String> queryString, String requestBody, Exception ex) {
        // 判断:是否要记录操作日志
        HandlerMethod handlerMethod = (HandlerMethod) request.getAttribute(ATTRIBUTE_HANDLER_METHOD);
        ApiAccessLog accessLogAnnotation = null;
        if (handlerMethod != null) {
            accessLogAnnotation = handlerMethod.getMethodAnnotation(ApiAccessLog.class);
            if (accessLogAnnotation != null && BooleanUtil.isFalse(accessLogAnnotation.enable())) {
                return false;
            }
        }

        // 处理用户信息
        accessLog.setUserId(WebFrameworkUtils.getLoginUserId(request))
                .setUserType(WebFrameworkUtils.getLoginUserType(request));
        // 设置访问结果
        CommonResult<?> result = WebFrameworkUtils.getCommonResult(request);
        if (result != null) {
            accessLog.setResultCode(result.getCode())
                    .setResultMsg(result.getMsg());
        } else if (ex != null) {
            accessLog.setResultCode(GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR
                    .getCode())
                    .setResultMsg(ExceptionUtil.getRootCauseMessage(ex));
        } else {
            accessLog.setResultCode(GlobalErrorCodeConstants.SUCCESS.getCode())
                    .setResultMsg("");
        }
        // 设置请求字段
        accessLog.setTraceId(TracerUtils.getTraceId())
                .setApplicationName(applicationName)
                .setRequestUrl(request.getRequestURI()).setRequestMethod(request.getMethod())
                .setUserAgent(ServletUtils.getUserAgent(request)).setUserIp(ServletUtils.getClientIP(request));
        String[] sanitizeKeys = accessLogAnnotation != null ? accessLogAnnotation.sanitizeKeys() : null;
        Boolean requestEnable = accessLogAnnotation != null ? accessLogAnnotation.requestEnable() : Boolean.TRUE;
        if (!BooleanUtil.isFalse(requestEnable)) { // 默认记录,所以判断 !false
            Map<String, Object> requestParams = MapUtil.<String, Object>builder()
                    .put("query", sanitizeMap(queryString, sanitizeKeys))
                    .put("body", sanitizeJson(requestBody, sanitizeKeys)).build();
            accessLog.setRequestParams(toJsonString(requestParams));
        }
        Boolean responseEnable = accessLogAnnotation != null ? accessLogAnnotation.responseEnable() : Boolean.FALSE;
        if (BooleanUtil.isTrue(responseEnable)) { // 默认不记录,默认强制要求 true
            accessLog.setResponseBody(sanitizeJson(result, sanitizeKeys));
        }
        // 持续时间
        accessLog.setBeginTime(beginTime)
                .setEndTime(LocalDateTime.now())
                .setDuration((int) LocalDateTimeUtil.between(accessLog.getBeginTime(), accessLog.getEndTime(), ChronoUnit.MILLIS));

        // 操作模块
        if (handlerMethod != null) {
            Tag tagAnnotation = handlerMethod.getBeanType().getAnnotation(Tag.class);
            Operation operationAnnotation = handlerMethod.getMethodAnnotation(Operation.class);
            String operateModule = accessLogAnnotation != null && StrUtil.isNotBlank(accessLogAnnotation.operateModule()) ?
                    accessLogAnnotation.operateModule() :
                    tagAnnotation != null ? StrUtil.nullToDefault(tagAnnotation.name(), tagAnnotation.description()) : null;
            String operateName = accessLogAnnotation != null && StrUtil.isNotBlank(accessLogAnnotation.operateName()) ?
                    accessLogAnnotation.operateName() :
                    operationAnnotation != null ? operationAnnotation.summary() : null;
            OperateTypeEnum operateType = accessLogAnnotation != null && accessLogAnnotation.operateType().length > 0 ?
                    accessLogAnnotation.operateType()[0] : parseOperateLogType(request);
            accessLog.setOperateModule(operateModule)
                    .setOperateName(operateName).setOperateType(operateType.getType());
        }
        return true;
    }

    // ========== 解析 @ApiAccessLog、@Swagger 注解  ==========

    private static OperateTypeEnum parseOperateLogType(HttpServletRequest request) {
        RequestMethod requestMethod = ArrayUtil.firstMatch(method ->
                StrUtil.equalsAnyIgnoreCase(method.name(), request.getMethod()), RequestMethod.values());
        if (requestMethod == null) {
            return OperateTypeEnum.OTHER;
        }
        switch (requestMethod) {
            case GET:
                return OperateTypeEnum.GET;
            case POST:
                return OperateTypeEnum.CREATE;
            case PUT:
                return OperateTypeEnum.UPDATE;
            case DELETE:
                return OperateTypeEnum.DELETE;
            default:
                return OperateTypeEnum.OTHER;
        }
    }

    // ========== 请求和响应的脱敏逻辑,移除类似 password、token 等敏感字段 ==========

    private static String sanitizeMap(Map<String, ?> map, String[] sanitizeKeys) {
        if (CollUtil.isEmpty(map)) {
            return null;
        }
        if (sanitizeKeys != null) {
            MapUtil.removeAny(map, sanitizeKeys);
        }
        MapUtil.removeAny(map, SANITIZE_KEYS);
        return JsonUtils.toJsonString(map);
    }

    private static String sanitizeJson(String jsonString, String[] sanitizeKeys) {
        if (StrUtil.isEmpty(jsonString)) {
            return null;
        }
        try {
            JsonNode rootNode = JsonUtils.parseTree(jsonString);
            sanitizeJson(rootNode, sanitizeKeys);
            return JsonUtils.toJsonString(rootNode);
        } catch (Exception e) {
            // 脱敏失败的情况下,直接忽略异常,避免影响用户请求
            log.error("[sanitizeJson][脱敏({}) 发生异常]", jsonString, e);
            return jsonString;
        }
    }

    private static String sanitizeJson(CommonResult<?> commonResult, String[] sanitizeKeys) {
        if (commonResult == null) {
            return null;
        }
        String jsonString = toJsonString(commonResult);
        try {
            JsonNode rootNode = JsonUtils.parseTree(jsonString);
            sanitizeJson(rootNode.get("data"), sanitizeKeys); // 只处理 data 字段,不处理 code、msg 字段,避免错误被脱敏掉
            return JsonUtils.toJsonString(rootNode);
        } catch (Exception e) {
            // 脱敏失败的情况下,直接忽略异常,避免影响用户请求
            log.error("[sanitizeJson][脱敏({}) 发生异常]", jsonString, e);
            return jsonString;
        }
    }

    private static void sanitizeJson(JsonNode node, String[] sanitizeKeys) {
        // 情况一:数组,遍历处理
        if (node.isArray()) {
            for (JsonNode childNode : node) {
                sanitizeJson(childNode, sanitizeKeys);
            }
            return;
        }
        // 情况二:非 Object,只是某个值,直接返回
        if (!node.isObject()) {
            return;
        }
        //  情况三:Object,遍历处理
        Iterator<Map.Entry<String, JsonNode>> iterator = node.fields();
        while (iterator.hasNext()) {
            Map.Entry<String, JsonNode> entry = iterator.next();
            if (ArrayUtil.contains(sanitizeKeys, entry.getKey())
                || ArrayUtil.contains(SANITIZE_KEYS, entry.getKey())) {
                iterator.remove();
                continue;
            }
            sanitizeJson(entry.getValue(), sanitizeKeys);
        }
    }
}
  1. 继承 ApiRequestFilter
    • ApiAccessLogFilter 继承 ApiRequestFilter,因此只处理以 /admin-api 或 /app-api 开头的请求。
    • 其他请求(如 /swagger-ui/*)被 shouldNotFilter 跳过。
  2. doFilterInternal 方法
    • 记录开始时间:LocalDateTime beginTime = LocalDateTime.now(),用于计算请求耗时。
    • 获取查询参数:通过 ServletUtils.getParamMap(request) 获取 URL 查询参数(如 ?key=value)。
    • 缓存请求体:对于 JSON 请求(Content-Type: application/json),通过 ServletUtils.getBody(request) 获取请求体,并缓存到 request 的属性中。
    • 执行过滤器链:调用 filterChain.doFilter(request, response),继续处理请求。
    • 记录日志
      • 正常执行:调用 createApiAccessLog 记录成功日志。
      • 异常执行:捕获异常,记录失败日志(包含异常信息),然后重新抛出异常。
  3. createApiAccessLog 方法
    • 创建 ApiAccessLogCreateReqDTO 对象,用于封装日志信息。
    • 调用 buildApiAccessLog 构建日志详情。
    • 如果 buildApiAccessLog 返回 false(例如,方法标注了 @ApiAccessLog(enable = false)),则跳过日志记录。
    • 通过 apiAccessLogApi.createApiAccessLogAsync 异步保存日志,捕获并记录任何异常。
  4. buildApiAccessLog 方法
    • 检查 @ApiAccessLog Annotation
      • 获取请求对应的 HandlerMethod(Controller 方法)。
      • 检查方法是否标注了 @ApiAccessLog 注解,若 enable = false,返回 false,跳过日志记录。
    • 设置用户信息
      • userId:通过 WebFrameworkUtils.getLoginUserId(request) 获取当前登录用户 ID。
      • userType:通过 WebFrameworkUtils.getLoginUserType(request) 获取用户类型(如管理员或普通用户)。
    • 设置访问结果
      • 如果请求成功,获取 CommonResult(芋道源码的统一响应格式),设置 resultCode 和 resultMsg。
      • 如果发生异常,设置错误码(INTERNAL_SERVER_ERROR)和异常消息。
      • 如果无 CommonResult 和异常,设置为成功状态(SUCCESS)。
    • 设置请求字段
      • traceId:链路追踪 ID(如 SkyWalking)。
      • applicationName:应用名称(通过构造函数注入)。
      • requestUrl、requestMethod、userAgent、userIp:从 request 获取。
    • 处理请求参数
      • 如果 @ApiAccessLog.requestEnable = true(默认),记录查询参数和请求体。
      • 使用 sanitizeMap 和 sanitizeJson 脱敏敏感字段(如 password、token)。
    • 处理响应数据
      • 如果 @ApiAccessLog.responseEnable = true(默认 false),记录响应体的 data 字段(脱敏后)。
    • 设置时间和耗时
      • beginTime:请求开始时间。
      • endTime:请求结束时间。
      • duration:计算耗时(毫秒)。
    • 设置操作模块
      • 从 @ApiAccessLog 或 Swagger 注解(@Tag、@Operation)获取模块(operateModule)、操作名称(operateName)和操作类型(operateType)。
      • 如果无注解,根据 HTTP 方法推断 operateType(如 POST 对应 CREATE)。
  5. 敏感字段脱敏
    • sanitizeMap 和 sanitizeJson
      • 移除敏感字段(如 password、token),支持自定义 sanitizeKeys(来自 @ApiAccessLog)和默认 SANITIZE_KEYS。
      • 对于 JSON 数据,递归处理对象和数组,确保嵌套字段也被脱敏。
    • 示例
      • 输入:{"username":"user","password":"123456"}
      • 输出:{"username":"user"}

示例:

  • 请求:POST /admin-api/users/createUser
  • 请求体:{"username":"newUser","password":"123456"}
  • 响应:{"code":200,"msg":"success","data":108}
  • 日志记录:
    • ApiAccessLogCreateReqDTO: ApiAccessLogCreateReqDTO( userId=1, userType=1, traceId="trace-123", applicationName="ruoyi-vue-pro", requestUrl="/admin-api/users/createUser", requestMethod="POST", userAgent="Apifox/1.0.0", userIp="0:0:0:0:0:0:0:1", requestParams="{\"query\":{},\"body\":{\"username\":\"newUser\"}}", responseBody=null, // 默认不记录 resultCode=200, resultMsg="success", beginTime="2025-04-29T11:12:00", endTime="2025-04-29T11:12:01", duration=1000, operateModule="用户管理", operateName="创建用户", operateType="CREATE" )

      复制代码
      WebProperties配置类,提供 API 前缀和 Controller 包路径的配置,支持动态调整过滤规则
java 复制代码
@ConfigurationProperties(prefix = "moyun.web")
@Validated
@Data
@Component
public class WebProperties {

    @NotNull(message = "APP API 不能为空")
    private Api appApi = new Api("/app-api", "**.controller.app.**");
    @NotNull(message = "Admin API 不能为空")
    private Api adminApi = new Api("/admin-api", "**.controller.admin.**");

    @NotNull(message = "Admin UI 不能为空")
    private Ui adminUi;

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @Valid
    public static class Api {

        /**
         * API 前缀,实现所有 Controller 提供的 RESTFul API 的统一前缀
         *
         *
         * 意义:通过该前缀,避免 Swagger、Actuator 意外通过 Nginx 暴露出来给外部,带来安全性问题
         *      这样,Nginx 只需要配置转发到 /api/* 的所有接口即可。
         *
         * @see YudaoWebAutoConfiguration#configurePathMatch(PathMatchConfigurer)
         */
        @NotEmpty(message = "API 前缀不能为空")
        private String prefix;

        /**
         * Controller 所在包的 Ant 路径规则
         *
         * 主要目的是,给该 Controller 设置指定的 {@link #prefix}
         */
        @NotEmpty(message = "Controller 所在包不能为空")
        private String controller;

    }

    @Data
    @Valid
    public static class Ui {

        /**
         * 访问地址
         */
        private String url;

    }

}

4.API 访问日志 Service 实现类

java 复制代码
/**
 * API 访问日志 Service 实现类
 */
@Slf4j
@Service
@Validated
public class ApiAccessLogServiceImpl implements ApiAccessLogService {

    @Resource
    private ApiAccessLogMapper apiAccessLogMapper;

    @Override
    public void createApiAccessLog(ApiAccessLogCreateReqDTO createDTO) {
        ApiAccessLogDO apiAccessLog = BeanUtils.toBean(createDTO, ApiAccessLogDO.class);
        apiAccessLog.setRequestParams(StrUtils.maxLength(apiAccessLog.getRequestParams(), ApiAccessLogDO.REQUEST_PARAMS_MAX_LENGTH));
        apiAccessLog.setResultMsg(StrUtils.maxLength(apiAccessLog.getResultMsg(), ApiAccessLogDO.RESULT_MSG_MAX_LENGTH));
        if (TenantContextHolder.getTenantId() != null) {
            apiAccessLogMapper.insert(apiAccessLog);
        } else {
            // 极端情况下,上下文中没有租户时,此时忽略租户上下文,避免插入失败!
            TenantUtils.executeIgnore(() -> apiAccessLogMapper.insert(apiAccessLog));
        }
    }

    @Override
    public PageResult<ApiAccessLogDO> getApiAccessLogPage(ApiAccessLogPageReqVO pageReqVO) {
        return apiAccessLogMapper.selectPage(pageReqVO);
    }

    @Override
    @SuppressWarnings("DuplicatedCode")
    public Integer cleanAccessLog(Integer exceedDay, Integer deleteLimit) {
        int count = 0;
        LocalDateTime expireDate = LocalDateTime.now().minusDays(exceedDay);
        // 循环删除,直到没有满足条件的数据
        for (int i = 0; i < Short.MAX_VALUE; i++) {
            int deleteCount = apiAccessLogMapper.deleteByCreateTimeLt(expireDate, deleteLimit);
            count += deleteCount;
            // 达到删除预期条数,说明到底了
            if (deleteCount < deleteLimit) {
                break;
            }
        }
        return count;
    }

}

插入的时候新建的ApiAccessLogDO就会自动填充字段;

java 复制代码
/**
 * API 访问日志
 *
 * @author 芋道源码
 */
@TableName("infra_api_access_log")
@KeySequence(value = "infra_api_access_log_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ApiAccessLogDO extends BaseDO {

    /**
     * {@link #requestParams} 的最大长度
     */
    public static final Integer REQUEST_PARAMS_MAX_LENGTH = 8000;

    /**
     * {@link #resultMsg} 的最大长度
     */
    public static final Integer RESULT_MSG_MAX_LENGTH = 512;

    /**
     * 编号
     */
    @TableId
    private Long id;
    /**
     * 链路追踪编号
     *
     * 一般来说,通过链路追踪编号,可以将访问日志,错误日志,链路追踪日志,logger 打印日志等,结合在一起,从而进行排错。
     */
    private String traceId;
    /**
     * 用户编号
     */
    private Long userId;
    /**
     * 用户类型
     *
     * 枚举 {@link UserTypeEnum}
     */
    private Integer userType;
    /**
     * 应用名
     *
     * 目前读取 `spring.application.name` 配置项
     */
    private String applicationName;

    // ========== 请求相关字段 ==========

    /**
     * 请求方法名
     */
    private String requestMethod;
    /**
     * 访问地址
     */
    private String requestUrl;
    /**
     * 请求参数
     *
     * query: Query String
     * body: Quest Body
     */
    private String requestParams;
    /**
     * 响应结果
     */
    private String responseBody;
    /**
     * 用户 IP
     */
    private String userIp;
    /**
     * 浏览器 UA
     */
    private String userAgent;

    // ========== 执行相关字段 ==========

    /**
     * 操作模块
     */
    private String operateModule;
    /**
     * 操作名
     */
    private String operateName;
    /**
     * 操作分类
     *
     * 枚举 {@link OperateTypeEnum}
     */
    private Integer operateType;

    /**
     * 开始请求时间
     */
    private LocalDateTime beginTime;
    /**
     * 结束请求时间
     */
    private LocalDateTime endTime;
    /**
     * 执行时长,单位:毫秒
     */
    private Integer duration;

    /**
     * 结果码
     *
     * 目前使用的 {@link CommonResult#getCode()} 属性
     */
    private Integer resultCode;
    /**
     * 结果提示
     *
     * 目前使用的 {@link CommonResult#getMsg()} 属性
     */
    private String resultMsg;

}
/**
 * 基础实体对象
 */
@Data
public abstract class BaseDO implements Serializable,TransPojo {

    /**
     * 创建时间
     */
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
    /**
     * 最后更新时间
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    /**
     * 创建者,目前使用 SysUser 的 id 编号
     *
     * 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。
     */
    @TableField(fill = FieldFill.INSERT, jdbcType = JdbcType.VARCHAR)
    private String creator;
    /**
     * 更新者,目前使用 SysUser 的 id 编号
     *
     * 使用 String 类型的原因是,未来可能会存在非数值的情况,留好拓展性。
     */
    @TableField(fill = FieldFill.INSERT_UPDATE, jdbcType = JdbcType.VARCHAR)
    private String updater;
    /**
     * 是否删除
     */
    @TableLogic
    private Boolean deleted;
}

YudaoMybatisAutoConfiguration配置类

java 复制代码
/**
 * MyBaits 配置类
 * 避免 @MapperScan 警告	@AutoConfiguration(before = MybatisPlusAutoConfiguration.class)	确保 Mapper 先被扫描
 * SQL 解析缓存	JsqlParserGlobal.setJsqlParseCache(...)	提高动态 SQL 解析性能
 * 分页插件	PaginationInnerInterceptor	自动分页,优化 LIMIT 查询
 * 自动填充字段	MetaObjectHandler	插入/更新时自动填充 create_time 等
 * 主键生成策略	IKeyGenerator	根据数据库类型自动选择主键生成方式
 */
@AutoConfiguration(before = MybatisPlusAutoConfiguration.class) // 目的:先于 MyBatis Plus 自动配置,避免 @MapperScan 可能扫描不到 Mapper 打印 warn 日志
@MapperScan(value = "${moyun.info.base-package}", annotationClass = Mapper.class,
        lazyInitialization = "${mybatis.lazy-initialization:false}") // Mapper 懒加载,目前仅用于单元测试
public class YudaoMybatisAutoConfiguration {

    static {
        // 动态 SQL 智能优化支持本地缓存加速解析,更完善的租户复杂 XML 动态 SQL 支持,静态注入缓存
        JsqlParserGlobal.setJsqlParseCache(new JdkSerialCaffeineJsqlParseCache(
                (cache) -> cache.maximumSize(1024)
                        .expireAfterWrite(5, TimeUnit.SECONDS))
        );
    }

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor()); // 分页插件
        return mybatisPlusInterceptor;
    }

    @Bean
    public MetaObjectHandler defaultMetaObjectHandler() {
        return new DefaultDBFieldHandler(); // 自动填充参数类
    }

    @Bean
    @ConditionalOnProperty(prefix = "mybatis-plus.global-config.db-config", name = "id-type", havingValue = "INPUT")
    public IKeyGenerator keyGenerator(ConfigurableEnvironment environment) {
        DbType dbType = IdTypeEnvironmentPostProcessor.getDbType(environment);
        if (dbType != null) {
            switch (dbType) {
                case POSTGRE_SQL:
                    return new PostgreKeyGenerator();
                case ORACLE:
                case ORACLE_12C:
                    return new OracleKeyGenerator();
                case H2:
                    return new H2KeyGenerator();
                case KINGBASE_ES:
                    return new KingbaseKeyGenerator();
                case DM:
                    return new DmKeyGenerator();
            }
        }
        // 找不到合适的 IKeyGenerator 实现类
        throw new IllegalArgumentException(StrUtil.format("DbType{} 找不到合适的 IKeyGenerator 实现类", dbType));
    }
}
  • 核心基础设施:为整个系统提供 MyBatis-Plus 的配置,保障数据库操作的正确性和性能。
  • 日志支持
    • 扫描 OperateLogMapper 和 ApiAccessLogMapper,支持日志插入和查询。
    • 通过 DefaultDBFieldHandler,确保日志实体的 createTime 等字段自动填充。
  • 灵活性:支持多数据库(MySQL、PostgreSQL 等)和动态包扫描,适应不同项目需求。
  • 性能优化:SQL 解析缓存和分页插件提高日志模块的高并发性能。

DefaultDBFieldHandler

java 复制代码
/**
 * 通用参数填充实现类
 *
 * 如果没有显式的对通用参数进行赋值,这里会对通用参数进行填充、赋值
 *
 * creatTime、updateTime、creator、updater等信息
 *
 * @author hexiaowu
 */
public class DefaultDBFieldHandler implements MetaObjectHandler {

    @Override
    public void insertFill(MetaObject metaObject) {
        if (Objects.nonNull(metaObject) && metaObject.getOriginalObject() instanceof BaseDO) {
            BaseDO baseDO = (BaseDO) metaObject.getOriginalObject();

            LocalDateTime current = LocalDateTime.now();
            // 创建时间为空,则以当前时间为插入时间
            if (Objects.isNull(baseDO.getCreateTime())) {
                baseDO.setCreateTime(current);
            }
            // 更新时间为空,则以当前时间为更新时间
            if (Objects.isNull(baseDO.getUpdateTime())) {
                baseDO.setUpdateTime(current);
            }

            Long userId = WebFrameworkUtils.getLoginUserId();
            // 当前登录用户不为空,创建人为空,则当前登录用户为创建人
            if (Objects.nonNull(userId) && Objects.isNull(baseDO.getCreator())) {
                baseDO.setCreator(userId.toString());
            }
            // 当前登录用户不为空,更新人为空,则当前登录用户为更新人
            if (Objects.nonNull(userId) && Objects.isNull(baseDO.getUpdater())) {
                baseDO.setUpdater(userId.toString());
            }
        }
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        // 更新时间为空,则以当前时间为更新时间
        Object modifyTime = getFieldValByName("updateTime", metaObject);
        if (Objects.isNull(modifyTime)) {
            setFieldValByName("updateTime", LocalDateTime.now(), metaObject);
        }

        // 当前登录用户不为空,更新人为空,则当前登录用户为更新人
        Object modifier = getFieldValByName("updater", metaObject);
        Long userId = WebFrameworkUtils.getLoginUserId();
        if (Objects.nonNull(userId) && Objects.isNull(modifier)) {
            setFieldValByName("updater", userId.toString(), metaObject);
        }
    }
}

DefaultDBFieldHandler 实现 MyBatis-Plus 的 MetaObjectHandler 接口,负责在插入和更新实体时自动填充通用字段(如 createTime、updateTime、creator、updater)。它是日志模块(如 OperateLogDO 和 ApiAccessLogDO)确保字段完整性的关键组件。

相关推荐
chilling heart6 分钟前
JAVA---集合ArrayList
java·开发语言
ss2736 分钟前
基于Springboot + vue实现的中医院问诊系统
java·spring boot·后端
wuqingshun31415922 分钟前
经典算法 最长单调递增子序列
java·c++·算法·蓝桥杯·机器人
IT技术员25 分钟前
【Java学习】动态代理有哪些形式?
java·python·学习
2401_8979300627 分钟前
Maven 依赖范围(Scope)详解
java·maven
@t.t.33 分钟前
Docker容器资源控制--CGroup
linux·运维·docker·容器·云计算
豆沙沙包?42 分钟前
2025年- H13-Lc120-189.轮转数组(普通数组)---java版
java·算法·排序算法
剑哥在胡说1 小时前
Python三大Web框架对比:Django、Flask、Tornado的异步实现方式详解
数据库·python·django·flask·tornado
不想当程序猿_1 小时前
Centos 7系统 宝塔部署Tomcat项目(保姆级教程)
linux·redis·centos·tomcat·minio·宝塔
吴声子夜歌1 小时前
Linux运维——Vim基础
linux·运维·vim