java每日精进 4.29【框架之自动记录日志并插入如数据库流程分析】
1.日志记录注解(LogRecord)
@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层使用注解,并给出字段的值
@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 前缀开头的请求进行处理
/*** 过滤 /admin-api、/app-api 等 API 请求的过滤器** @author 芋道源码*/
@RequiredArgsConstructor
public abstract class ApiRequestFilter extends OncePerRequestFilter {protected final WebProperties webProperties;@Overrideprotected 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
/*** 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访问日志的APIprivate 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)) { // 默认记录,所以判断 !falseMap<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)) { // 默认不记录,默认强制要求 trueaccessLog.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);}}
}
- 继承 ApiRequestFilter:
- ApiAccessLogFilter 继承 ApiRequestFilter,因此只处理以 /admin-api 或 /app-api 开头的请求。
- 其他请求(如 /swagger-ui/*)被 shouldNotFilter 跳过。
- 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 记录成功日志。
- 异常执行:捕获异常,记录失败日志(包含异常信息),然后重新抛出异常。
- createApiAccessLog 方法:
- 创建 ApiAccessLogCreateReqDTO 对象,用于封装日志信息。
- 调用 buildApiAccessLog 构建日志详情。
- 如果 buildApiAccessLog 返回 false(例如,方法标注了 @ApiAccessLog(enable = false)),则跳过日志记录。
- 通过 apiAccessLogApi.createApiAccessLogAsync 异步保存日志,捕获并记录任何异常。
- 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)。
- 检查 @ApiAccessLog Annotation:
- 敏感字段脱敏:
- sanitizeMap 和 sanitizeJson:
- 移除敏感字段(如 password、token),支持自定义 sanitizeKeys(来自 @ApiAccessLog)和默认 SANITIZE_KEYS。
- 对于 JSON 数据,递归处理对象和数组,确保嵌套字段也被脱敏。
- 示例:
- 输入:{"username":"user","password":"123456"}
- 输出:{"username":"user"}
- sanitizeMap 和 sanitizeJson:
示例:
- 请求: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 包路径的配置,支持动态调整过滤规则
- ApiAccessLogCreateReqDTO:
@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@Validpublic 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@Validpublic static class Ui {/*** 访问地址*/private String url;}}
4.API 访问日志 Service 实现类
/*** API 访问日志 Service 实现类*/
@Slf4j
@Service
@Validated
public class ApiAccessLogServiceImpl implements ApiAccessLogService {@Resourceprivate ApiAccessLogMapper apiAccessLogMapper;@Overridepublic 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));}}@Overridepublic 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就会自动填充字段;
/*** 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;/*** 编号*/@TableIdprivate 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;/*** 是否删除*/@TableLogicprivate Boolean deleted;
}
YudaoMybatisAutoConfiguration配置类
/*** 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)));}@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor()); // 分页插件return mybatisPlusInterceptor;}@Beanpublic 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
/*** 通用参数填充实现类** 如果没有显式的对通用参数进行赋值,这里会对通用参数进行填充、赋值** creatTime、updateTime、creator、updater等信息** @author hexiaowu*/
public class DefaultDBFieldHandler implements MetaObjectHandler {@Overridepublic 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());}}}@Overridepublic 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)确保字段完整性的关键组件。
相关文章:
java每日精进 4.29【框架之自动记录日志并插入如数据库流程分析】
1.日志记录注解(LogRecord) Repeatable(LogRecords.class) Target({ElementType.METHOD, ElementType.TYPE}) Retention(RetentionPolicy.RUNTIME) Inherited Documented public interface LogRecord {String success();String fail() default "&q…...
HarmonyOS NEXT 诗词元服务项目开发上架全流程实战(二、元服务与应用APP签名打包步骤详解)
在HarmonyOS应用开发过程中,发布应用到应用市场是一个重要的环节。没经历过的童鞋,首次对HarmonyOS的应用签名打包上架可能感觉繁琐。需要各种秘钥证书生成和申请,混在一起分不清。其实搞清楚后也就那会事,各个文件都有它存在的作…...
java的多线程
文章目录 创建线程什么是线程?什么是多线程?如何在程序中创建出多条线程?方式一:继承Thread类方式二:实现Runnable接口方式三:实现Callable接口 三种创建方式的对比 线程的常用方法Thread提供的常用方法Thr…...
CSS--图片链接水平居中展示的方法
原文网址:CSS--图片链接居中展示的方法-CSDN博客 简介 本文介绍CSS图片链接水平居中展示的方法。 图片链接 问题复现 源码 <html xml:lang"cn" lang"cn"><head><meta http-equiv"Content-Type" content"te…...
【计算机视觉】目标检测:深度解析YOLOv5:下一代实时目标检测框架实战指南
深度解析YOLOv5:下一代实时目标检测框架实战指南 技术演进与架构设计YOLO系列发展脉络YOLOv5核心架构1. 骨干网络(Backbone)2. 特征融合(Neck)3. 检测头(Head) 环境配置与快速开始硬件要求建议详…...
CentOS NFS共享目录
最近遇到一个问题,一台CentOS7应用服务器上的服务需要访问另外一台CentOS7应用服务器上的文件,然后传输文件给第三方。想到windows系统之间有文件共享的功能,Linux系统之间是否也有类似的文件共享功能呢? NFS NFS代表Network Fil…...
「国产嵌入式仿真平台:高精度虚实融合如何终结Proteus时代?」——从教学实验到低空经济,揭秘新一代AI赋能的产业级教学工具
引言:从Proteus到国产平台的范式革新 在高校嵌入式实验教学中,仿真工具的选择直接影响学生的工程能力培养与创新思维发展。长期以来,Proteus作为经典工具占据主导地位,但其设计理念已难以满足现代复杂系统教学与国产化技术需求。…...
[随笔] 升级uniapp旧项目的vue、pinia、vite、dcloudio依赖包等
汇总 # 升级uniapp项目dcloudio整体依赖,建议执行多次 # 会顺带自动更新/升级vue的版本 npx dcloudio/uvmlatest alpha# 检查 pinia 的最新版本 npm view pinia version# 更新项目 pinia 到最新版本 npm update pinia# 更新项目 pinia 到特定的版本 # 首先…...
C++学习:六个月从基础到就业——异常处理:机制与最佳实践
C学习:六个月从基础到就业——异常处理:机制与最佳实践 本文是我C学习之旅系列的第三十八篇技术文章,也是第二阶段"C进阶特性"的最后一篇,主要介绍C中的异常处理机制及其最佳实践。查看完整系列目录了解更多内容。 引言…...
【MongoDB篇】MongoDB的数据库操作!
目录 引言第一节:数据库的“诞生”——如何创建数据库?🤔第二节:数据库的“查阅”——看看我的数据库们!🕵️♀️第三节:数据库的“切换”——我在哪个房间干活?➡️🚪…...
react-新建项目复用node_modules
每次新建定制时,前端都需要npm i来安装依赖,耗时长 失败多。 可以把这个bat文件放到新建分支的前端目录下,修改后双击bat文件运行,如果不需要添加修改依赖,无需运行npm i node_modules.bat里面的内容如下:…...
unity Orbbec Femto Bolt接入unity流程记录 AzureKinectExamples 插件 使用记录
奥比中光的深度相机Orbbec Femto Bolt是Microsoft的Azure Kinect DK的升级版,根据官网的文档配置环境遇到了一些问题,记录一下。 注意: 官网文档链接:Femto Bolt文档 1、首先连接相机到电脑USB3.0,接通电源…...
信息科技伦理与道德3-4:面临挑战
1 人机结合 1.1 人机结合的挑战 如何处理好人与机器的决策的关系?智能决策的不透明、不可解释性…出了问题该谁负责? 案例1:设想救护车调度系统造成混乱 某城市使用一个机器学习平台来进行城市里医院的救护车调度工作。起初,这个…...
对比测评:为什么AI编程工具需要 Rules 能力?
通义灵码 Project Rules 在开始体验通义灵码 Project Rules 之前,我们先来简单了解一下什么是通义灵码 Project Rules? 大家都知道,在使用 AI 代码助手的时候,有时候生成的代码不是自己想要的,或者说生成的代码采纳后…...
git学习之git常用命令
1. 初始化仓库 git init初始化一个新的 Git 仓库。 2. 克隆远程仓库 git clone <repository-url>从远程服务器克隆一个已有仓库到本地。 3. 配置用户名和邮箱 git config --global user.name "Your Name" git config --global user.email "youexampl…...
The Open Group 参加雷丁博物馆的数字革命展览
The Open Group 参加了雷丁博物馆的数字革命展览,庆祝雷丁市转型为数字中心60周年。 展览于3月18日(星期二)向公众开放,将持续至2025年12月24日。展览旨在纪念雷丁市令人惊叹的科技之旅,从1964年数字设备公司ÿ…...
Linux[配置vim]
Linux[配置vim] 我这里的环境是xshell8的虚拟机,Ubuntu 配置好了以后功能嘎嘎多 以下是为 Ubuntu 配置功能增强版 Vim 的详细步骤,包含代码高亮、插件管理、自动补全、文件导航等常用功能: 1. 安装最新版 Vim sudo apt update sudo apt install vim-g…...
【数据结构】图论存储结构深度解析:邻接多重表如何实现无向图O(1)删边?邻接矩阵/链表/十字链对比
邻接多重表 导读一、有向图的存储结构二、邻接多重表三、存储结构四、算法评价4.1 时间复杂度4.2 空间复杂度 五、四种存储方式的总结5.1 空间复杂度5.2 找相邻边5.3 删除边或结点5.4 适用于5.5 表示方式 六、图的基本操作结语 导读 大家好,很高兴又和大家见面啦&a…...
【AlphaFold2】Feature extraction:提取特征,为模型输入做准备|Datapipeline讲解
博主简介:努力学习的22级计算机科学与技术本科生一枚🌸博主主页: Yaoyao2024往期回顾:【深度学习】多头注意力机制的实现|pytorch每日一言🌼: 学习成绩只是表象,而学习能力才是伴随一身的结果🌺…...
Android 实现一个隐私弹窗
效果图如下: 1. 设置同意、退出、点击用户协议、点击隐私协议的函数参数 2. 《用户协议》、《隐私政策》设置成可点击的,且颜色要区分出来 res/layout/dialog_privacy_policy.xml 文件 <?xml version"1.0" encoding"utf-8"?&…...
第三方软件测试报告如何凭借独立公正与专业权威发挥关键作用?
在软件项目里,第三方软件测试报告起着极为关键的作用。第三方有着中立客观的立场。第三方具备专业能力。凭借这些,第三方能为软件质量评估提供可靠依据。下面要从不同方面介绍第三方软件测试报告。 独立公正性 第三方测试机构与软件开发方、使用方不存…...
QT控件 参考Qt的PIMPL设计模式实现使用QWidget控件绘制3D饼状图表和3D柱状图表,使用QChartView绘制圆柱体图表
整体绘制效果就是:Qt 实现3维饼状图 中的内容, 只不过我借鉴了Qt的PIMPL模式重新封装了整个实现过程 实现效果展示 目录导读 实现效果展示前言绘制3D饼状图表PIMPL模式设计类具体实现计算圆弧中心判断点是否在某个扇区中在私有类中绘制绘制3D柱状图表PIMPL模式设计类具体实现绘…...
Android Q允许低内存启用系统弹窗
如果SYSTEM_ALERT_WINDOW权限可用,则返回true。 *从Q开始,在低ram手机上禁用SYSTEM_ALERT_WINDOW。 vendor/mediatek/proprietary/packages/apps/MtkSettings/src/com/android/settings/Utils.java public static boolean isSystemAlertWindowEnabled(Co…...
Leetcode 3532. Path Existence Queries in a Graph I
Leetcode 3532. Path Existence Queries in a Graph I 1. 解题思路2. 代码实现 题目链接:3532. Path Existence Queries in a Graph I 1. 解题思路 这一题算是一个比较典型的DSU的题目,我们就是不断地根据前后节点的距离将其进行聚类,然后…...
AI Agent Protocols:现状、挑战与未来展望
一、引言 在当今人工智能飞速发展的时代,大语言模型(LLMs)的进步使得LLM智能体在各个行业得到了广泛的应用,如客户服务、内容生成、数据分析和医疗保健等领域。 然而,随着越来越多的LLM智能体被部署,一个…...
自动化立库/AGV物流仿真详细步骤
以下是一种可以在预算和周期内实现自动化立库及AGV 方案仿真分析的方法: 一、工具选择 软件工具FlexSim:这是一款流行的离散事件仿真软件。它具有直观的图形用户界面,通过简单的拖拽操作就可以构建自动化立库和 AGV 的模型。其内置的丰富的…...
【题解-Acwing】872. 最大公约数
题目:872. 最大公约数 题目描述 给定 n 对正整数 ai,bi,请你求出每对数的最大公约数。 输入 第一行包含整数 n。 接下来 n 行,每行包含一个整数对 ai,bi。 输出 输出共 n 行,每行输出一个整数对的最大公约数。 数据范围 1 ≤ n ≤ 105, 1 ≤ai, bi ≤ 2109 时空限…...
62.微服务保姆教程 (五) Seata--微服务分布式事务组件
Seata–微服务分布式事务组件 一、什么是分布式事务 1.什么是事务 事务指的是一个操作单元,在这个操作单元中的所有操作最终要保持一致的行为,要么所有操作都成功,要么所有的操作都被撤销。 2.本地事务 本地事务是指基于关系型数据库的事务,也称为传统事务。大多数场景…...
【算法练习】归并排序和归并分治
文章目录 1.归并排序1.1 递归版本1.2 非递归版本 2.归并分治2.1 计算数组的小和2.2 计算翻转对 1.归并排序 归并排序的核心步骤是: 拆分:将无序数组不断对半拆分成小块,直到每个小块只剩一个元素(自然有序)。 合并&a…...
从SOA到微服务:架构演进之路与实践示例
一、架构演进背景 在软件开发领域,架构风格随着业务需求和技术发展不断演进。从早期的单体架构,到面向服务架构(SOA),再到如今的微服务架构,每一次变革都是为了解决当时面临的核心问题。 二、SOA架构解析 2.1 SOA核心概念 SOA&…...
vue+cesium线流动纹理
index.vue页面 <!--线流动纹理实现--> <template><div id"mapContainerFirst"></div> </template> <script lang"ts" setup> import { init as initPolylineTrailLinkMaterialProperty } from ./PolylineTrailLinkM…...
深度学习·经典模型·SwinTransformer
SwinTransformer 主要创新点:移动窗口,基于窗口的注意力计算 Patch Embedding 下采样打包为Pacth:可以直接使用Conv2d 也可以先打包后使用embedding映射。 Patch Merging 类似池化的操作,压缩图片大小,同时通道数增多ÿ…...
在开发板上如何处理curl: (60) SSL certificate problem
目录 引言 问题解析 解决方法 跳过证书验证 采用证书认证 结语 引言 最近一直推荐学生们在课程实验中使用curl及其libcurl。curl 是一个强大的命令行工具,用于在命令行中进行数据传输。它支持多种协议,如 HTTP、HTTPS、FTP、FTPS、SCP、SFTP 等。…...
Ansible 铸就 Linux 安全之盾(Ansible Builds Linux Security Shield)
Ansible 铸就 Linux 安全之盾:自动化基线检查与防护 在当今网络安全形势日益严峻的背景下,Linux 系统作为服务器和关键基础设施的核心,其安全防护显得尤为重要。Ansible 作为一款强大的自动化运维工具,能够帮助我们高效、可靠地实…...
字符串(格式化字符串字面值)进行输出
在 Python 中,print(fnew_obs:{new_obs}) 这种形式是使用 f 字符串(格式化字符串字面值) 进行输出,它可以打印 任何可转换为字符串的数据类型,并且支持在字符串中嵌入表达式。以下是详细说明: 1. 基本功能…...
微服务架构详解:从概念到实践
目录 前言1. 微服务架构概述1.1 什么是微服务?1.2 微服务的核心思想 2. 微服务的优势2.1 可扩展性2.2 高灵活性2.3 容错性和可靠性2.4 高效开发与部署 3. 微服务的挑战3.1 系统复杂性增加3.2 分布式事务和数据一致性3.3 部署和运维的复杂性 4. 微服务的实施与实践4.…...
激光驱鸟:以科技重构生态防护边界
技术原理 激光驱鸟装置的核心机制基于鸟类视觉系统特性。其发射的绿色激光束(波长通常为532纳米)处于鸟类视网膜敏感光谱范围内,当激光束在特定角度扫描时,会形成动态光斑干扰。鸟类视网膜中视锥细胞对绿色光的高敏感度使其产生应…...
【Python魔法方法(特殊方法)】
在 Python 中,许多运算符都可以进行重载,以下是一些常见运算符及其对应的魔法方法(特殊方法): 算术运算符 加法 :__add__ 用于定义对象相加的行为。例如,当你对两个自定义类的实例使用 运算符…...
centos上安装python的3.13版本
在 CentOS 上安装 Python 3.13(或其它自定义版本)最推荐的方法是通过源码编译安装,不会影响系统自带的 Python2/Python3 环境,也更灵活可控。 以下步骤适用于: ✅ CentOS 7 / 8 / 9 ✅ 安装 Python 3.13(…...
实习技能记录【4】-----消息分发中的观察者模型
观察者 观察者模式(Observer Pattern)是一种行为型设计模式,主要用于定义对象之间的一对多依赖关系,让多个观察者对象能够同时监听某个主题对象的状态变化,并在主题对象状态改变时自动通知所有观察者对象。 参考b站博…...
Linux 下编译BusyBox
一、linux下编译 1.拉取busybox源码 git clone https://github.com/mirror/busybox.git 内容如下 2.配置make,建议在linux下单独开一个终端执行 进入busybox源码目录,使用如下命令 make menuconfig 3.报错 解决办法: 安装ncurses sud…...
Linux《进程概念(中)》
在之前的Linux《进程概念(上)》当中我们已经了解了进程的基本概念以及如何去创建对应的子进程,那么接下来在本篇当中我们就继续来进程的学习,在本篇当中我们要学习到进程的状态、进程的优先级、进程切换、Linux真实的调度算法——…...
Linux Vim 使用 显示行号、替换、查找、多文件打开等骚操作
目录 简述 vim的三种模式 概述 转换方式 文本编辑 命令模式 插入(编辑)模式 底行模式 搜索关键字 显示行号 替换 多文件打开 简述 vi编辑器是Linux系统下标准的编辑器。 那么简单的理解,就像是Windows下的记事本。 补充&a…...
AimRT 从零到一:官方示例精讲 —— 三、Executor示例.md
Executor示例 官方仓库:executor 配置文件(configuration_executor.yaml) 依据官方示例项目结构自行编写YAML配置文件: # 基础信息 base_info:project_name: Logger # 项目名称build_mode_tags: ["EXAMPLE", &quo…...
只把夜莺监控当作告警来使用:一种轻量化的运维实践
只把夜莺监控当作告警来使用:一种轻量化的运维实践 在现代的 IT 运维体系中,监控和告警是两个经常被一同提及的概念。然而,在实际工作中,很多团队对监控系统的需求并不一定全面覆盖指标采集、可视化展示、告警触发等功能…...
按键精灵安卓ios辅助工具脚本:实用的文件插件(lua开源)
亮点:此lua插件可再android和ios上通用 1、获取文件的属性 2、改变当前的工作路径为dirpath 3、获取当前的工作路径 4、创建文件夹,支持多级创建 5、删除文件夹 6、递归遍历文件夹 7、设置文件的访问时间和修改时间 函数原型:lfs.Attribute(…...
水库现代化建设指南-水库运管矩阵管理系统建设方案
政策背景 2023年8月24日,水利部发布的水利部关于加快构建现代化水库运行管理矩阵的指导意见中指出,在全面推进水库工程标准化管理的基础上,强化数字赋能,加快构建以推进全覆盖、全要素、全天候、全周期“四全”管理,完…...
若依后台管理系统-v3.8.8-登录模块--个人笔记
各位编程爱好者们,你们好!今天让我们来聊聊若依系统在登录模块的一些业务逻辑,以及本人的一些简介和心得,那么废话不多说,让我们现在开始吧。 以下展示的这段代码,正是若依在业务层对应的登录代码…...
Flip PDF Plus Corp7.7.22电子书制作软件
flip pdf plus corporate7.7.22中文版由FlipBuilder官方出品的一款企业级的翻页电子书制作软件,拥有丰富的模板,主题和动画场景,每本书最大页数1000页,每本书的最大大小1GB,即可以帮助企业用户制作好丰富的电子书籍。 …...
公路安全知识竞赛主持稿串词
合 :尊敬的各位领导、各位来宾 、各位选手 : 大家上午 好! 男 :安全就是生命,安全就是效益,安全是一切工作的重中之重!安全生产只有满分,没有及格。只有安全生产这个环节不出差错,我…...