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)确保字段完整性的关键组件。

相关推荐
爱笑的源码基地1 小时前
高并发 Redis 缓存门诊HIS系统源码,含财务统计药房进销存
java·程序·门诊系统·诊所系统·云诊所源码
憧憬成为web高手1 小时前
皮卡丘靶场速通--sql 2
数据库·sql·mybatis
HLC++1 小时前
Linux的进程间通信
android·linux·服务器
段一凡-华北理工大学1 小时前
向量数据库实战:选型、调优与落地~系列文章12:文本分块策略实战:chunk_size 怎么选?重叠多少?
开发语言·数据库·后端·oracle·rust·工业智能体·高炉智能化
oradh3 小时前
Oracle XTTS实现跨版本迁移和升级(Oracle 11g单库升级至19C RAC集群)
数据库·oracle·11g升级19c·xtts跨版本迁移和升级
莫逸风3 小时前
【AgentScope 2.0】 0. 学习指南
java·llm·agent·agentscope
z123456789864 小时前
2026最新两款AI编程工具深度对比实测
java·数据库·ai编程
程序猿DD4 小时前
一个 API Key,统一调用大模型、生图和联网搜索
数据库·网关
yaoxin5211235 小时前
470. Java 反射 - Member 接口与 AccessFlag
java·开发语言·python
groundhappy5 小时前
idalib安装和codex ida-mcp配置
linux·开发语言·python