【Spring AOP】操作日志的完整实现与原理剖析

文章目录

    • [0. AOP 操作日志设计思路](#0. AOP 操作日志设计思路)
    • [1. AOP原理](#1. AOP原理)
      • [1.1 核心概念](#1.1 核心概念)
      • [1.2 切点表达式](#1.2 切点表达式)
      • [1.3 通知类型](#1.3 通知类型)
      • [1.4 代理机制](#1.4 代理机制)
      • [1.5 织入时机](#1.5 织入时机)
    • [2. AOP操作日志实现](#2. AOP操作日志实现)
      • [2.0 实现关键考量:异步写入与安全上下文传递](#2.0 实现关键考量:异步写入与安全上下文传递)
      • [2.1 MySQL建表语句](#2.1 MySQL建表语句)
      • [2.2 AOP依赖](#2.2 AOP依赖)
      • [2.3 操作日志注解](#2.3 操作日志注解)
      • [2.4 操作日志实体类](#2.4 操作日志实体类)
      • [2.5 操作日志Mapper(持久层)](#2.5 操作日志Mapper(持久层))
      • [2.6 操作日志Service接口(业务层)](#2.6 操作日志Service接口(业务层))
      • [2.7 操作日志Service实现类(业务层)](#2.7 操作日志Service实现类(业务层))
      • [2.8 操作日志切面类](#2.8 操作日志切面类)
    • [3. AOP操作日志注解使用](#3. AOP操作日志注解使用)
      • [3.1 操作类型常量类](#3.1 操作类型常量类)
      • [3.2 在业务层实现类中使用](#3.2 在业务层实现类中使用)
    • [4. AOP在Spring中的其他典型应用](#4. AOP在Spring中的其他典型应用)

这篇写了我一整天,终于写完了。和 AI 边讨论边做的,除了操作日志框架的完整代码,正文还内附了很多个注意事项和为什么要这么做。今天学到了很多新东西,谢谢 Qwen。

2025/12/24 0:29


0. AOP 操作日志设计思路

通过 AOP 自动记录用户在系统中的关键业务操作(如增删改),包括操作人、操作内容、时间、结果等,用于后续审计与排查。

1. AOP原理

AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,用于将横切关注点 (如日志、事务、安全等)与核心业务逻辑解耦。Spring AOP 是其在 Spring 框架中的实现。

1.1 核心概念

术语 说明
Aspect(切面) 横切关注点的模块化,通常是一个带有 @Aspect 注解的类,包含通知和切点。
Join Point(连接点) 程序执行过程中的某个特定点,如方法调用、异常抛出。Spring AOP 中仅支持方法级别的连接点
Pointcut(切点) 匹配连接点的表达式,用于指定哪些方法需要被增强
Advice(通知) 在切点处执行的增强逻辑,按执行时机分为多种类型(见下文)。
Weaving(织入) 将切面应用到目标对象并创建代理对象的过程。Spring AOP 在运行时通过代理完成织入

1.2 切点表达式

切点表达式用于指定哪些方法要被增强。Spring AOP 支持多种写法,比较常用的是:

  • 推荐方式:基于注解 (解耦、显式、安全)

    java 复制代码
    @Around("@annotation(org.example.framework.logging.annotation.OperationLog)")
    • 匹配所有标注了 @OperationLog 的方法
    • 不依赖包路径或方法名,重构友好。
  • 备选方式:基于方法签名(execution

    java 复制代码
    // execution(返回类型 包.类.方法名(参数))
    // 匹配 service 包下所有以 create 开头、任意参数、任意返回值的方法
    execution(* org.example.service.*.create*(..))
  • 何时使用 @Pointcut
    @Pointcut 不是必须的,仅在以下情况使用:

    • 表达式需要被多个通知复用
    • 需要组合多个条件 (如"带注解 + 在 Service 层")。
    java 复制代码
    @Pointcut("@annotation(OperationLog)")
    private void logAnnotated() {}
    
    @Pointcut("within(org.example.service..*)")
    private void inServiceLayer() {}
    
    @Around("logAnnotated() && inServiceLayer()")
    public Object around(ProceedingJoinPoint jp) { ... }

1.3 通知类型

类型 注解 执行时机
前置通知 @Before 目标方法执行前
后置通知 @After 目标方法执行后(无论是否异常)
返回通知 @AfterReturning 目标方法成功返回后
异常通知 @AfterThrowing 目标方法抛出异常后
环绕通知 @Around 包围整个方法,可控制是否执行、修改参数/返回值(最强大)

实际项目中,@Around 最常用(如操作日志、性能监控),因为它能统一处理成功/异常,并获取方法元信息。

1.4 代理机制

Spring AOP 基于代理实现,分两种情况:

目标对象 代理方式 特点
实现了接口 JDK 动态代理 生成 $Proxy 接口代理类
未实现接口(或强制使用) CGLIB 代理 生成目标类的子类作为代理

这里先不管这两种代理是什么。

注意:同一个类内部方法调用不会触发 AOP(因为绕过了代理对象),这是常见陷阱。即外部调用时 AOP 生效,内部调用时 AOP 失效

Spring 为了让我(业务层接口,IBrandService专注核心业务逻辑解耦,非侵入 ),自动给我配了一个"助手"(代理对象)。

.

别人调用我 时,Spring 先把请求交给"助手"------助手先做 AOP 的事情(比如记日志、开事务),然后再通知真正的我去执行业务逻辑

.

但当我自己调用自己的方法 (比如 this.methodB())时,我根本不知道有"助手"存在(因为 this 就是我自己),所以直接执行了,完全绕过了助手

  • 这个"助手"是 Spring 在启动时偷偷创建的,我们写的类本身没有任何变化;
  • 注入的 Bean(比如 Controller@Autowired BrandService)其实拿到的是助手,不是我们自己;
  • 但在类内部用 this,永远指向原始的自己,不是助手。

1.5 织入时机

  • Spring AOP 是运行时织入(Runtime Weaving);
  • 在 Bean 初始化时,由 BeanPostProcessor 创建代理对象;
  • 与 AspectJ 的编译时/加载时织入不同,功能较弱但更轻量。

小结:Spring AOP 通过动态代理 + 切点表达式 + 通知,在运行时将横切逻辑织入目标方法。

2. AOP操作日志实现

2.0 实现关键考量:异步写入与安全上下文传递

为了不影响主线程的业务响应速度 ,使用 AOP 切面类 OperationLogAspect.java 后调用操作日志服务 IOperationService,用 @Async 独立线程异步实现记录操作日志。

在这个独立线程中,就没有办法拿到 userId了,因为 Spring Security 的用户上下文 SecurityContext 是绑定在主线程的 ThreadLocal 中的,不会自动传递到异步线程 ,故 MP 的自动填充处理器也无法正确填充 createdBy(和 createdAt)字段。

所以,为了正确写入操作日志记录,只能在触发独立线程前,在切面类里就把这俩字段给填充了。这样,异步线程只需"傻瓜式"地把已填充好的日志写入数据库,不依赖任何上下文

原则:敏感上下文(用户、时间点)在主线程捕获,异步线程只做无状态写入。

2.1 MySQL建表语句

sql 复制代码
CREATE TABLE `operation_log`  (
  `id` bigint(0) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `operator_id` bigint(0) NOT NULL COMMENT '操作人ID',
  `description` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '操作描述,如"删除品牌"',
  `operation_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '操作类型,如 BRAND_DELETE, DRINK_RECORD',
  `request_method` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '请求方法(GET/POST/PUT/DELETE)',
  `request_url` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '请求路径',
  `request_params` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '请求参数(JSON,已脱敏)',
  `client_ip` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '客户端IP(支持IPv6)',
  `success` tinyint(1) NOT NULL COMMENT '是否成功(1=成功,0=失败)',
  `error_message` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '异常信息(失败时记录,建议前端截断)',
  `cost_time` bigint(0) NOT NULL COMMENT '耗时(毫秒)',
  `created_at` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '操作时间',
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `idx_create_time`(`created_at`) USING BTREE,
  INDEX `idx_operation_type`(`operation_type`) USING BTREE,
  INDEX `idx_success`(`success`) USING BTREE,
  INDEX `idx_url`(`request_url`(100)) USING BTREE,
  INDEX `idx_operator_id`(`operator_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '系统操作日志表(审计日志)' ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

2.2 AOP依赖

xml 复制代码
		<!-- AOP -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

2.3 操作日志注解

java 复制代码
package org.example.framework.logging.annotation;

import java.lang.annotation.*;

@Target(ElementType.METHOD) // 该注解只能用在"方法"上
@Retention(RetentionPolicy.RUNTIME) // 让 Spring 在运行时能通过反射读取到该注解,从而触发日志记录等逻辑
public @interface OperationLog {

    /**
     * 操作描述,例如 "删除用户"
     */
    String value() default "";

    /**
     * 操作类型(可选,如:CREATE, UPDATE, DELETE, QUERY)
     */
    String type() default "OPERATE";
}
  • 当注解只有一个 value 字段时,使用时可省略字段名,直接写值。如:

    java 复制代码
    @Operation("删除商品")	// 可省略
    @Operation(value = "删除商品")
  • 当注解只有一个字段且字段名不为 value ,或有多个字段时,使用时必须显式写出字段名。如:

    java 复制代码
    @Operation(description = "删除商品")
    @Operation(value = "删除商品", type = "DELETE")

2.4 操作日志实体类

java 复制代码
package org.example.framework.logging.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Builder;
import lombok.Data;

import java.time.LocalDateTime;

@Data
@Builder
@TableName("operation_log")
public class OperationLogEntity {

    @TableId(type = IdType.AUTO)
    private Long id;

    /**
     * 操作人ID
     */
    private Long operatorId;
    /**
     * 操作描述,如"删除用户"
     */
    private String description;
    /**
     * 操作类型
     */
    private String operationType;
    /**
     * GET/POST/PUT/DELETE
     */
    private String requestMethod;
    /**
     * 请求路径
     */
    private String requestUrl;
    /**
     * 请求参数(JSON),在AOP切面中需脱敏处理
     */
    private String requestParams;
    /**
     * 客户端IP
     */
    private String clientIp;
    /**
     * 是否成功
     */
    private Boolean success;
    /**
     * 异常信息(失败时)
     */
    private String errorMessage;
    /**
     * 耗时(毫秒),用于性能监控
     */
    private Long costTime;
    /**
     * 操作时间:
     * @Async 异步线程中 SecurityUtils.getCurrentUserId() 会失效!
     * 故此处无法触发 mp 的自动填充处理器,需要手动填充。
     */
    private LocalDateTime createdAt;
}

我还有一个 BaseEntity.java 的通用基类(idcreatedBycreatedAtupdatedByupdatedAtdeleted),但这里不直接继承它:操作日志是只写不改的审计记录,不需要更新审计字段和逻辑删除字段

2.5 操作日志Mapper(持久层)

java 复制代码
package org.example.framework.logging.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.example.framework.logging.entity.OperationLogEntity;

@Mapper
public interface OperationLogMapper extends BaseMapper<OperationLogEntity> {
}

这里要注意一下启动类里指定的 mapper 扫描路径:原来的路径 @MapperScan("org.example.*.mapper") 改成 @MapperScan("org.example.**.mapper")匹配任意层级子包。

2.6 操作日志Service接口(业务层)

java 复制代码
package org.example.framework.logging.service;

import com.baomidou.mybatisplus.extension.service.IService;
import org.example.framework.logging.entity.OperationLogEntity;
import org.springframework.stereotype.Service;

@Service
public interface IOperationLogService extends IService<OperationLogEntity> {

    /**
     * 异步保存操作日志
     * @param entity
     */
    void createOperationLog(OperationLogEntity entity);
}

2.7 操作日志Service实现类(业务层)

java 复制代码
package org.example.framework.logging.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.example.framework.logging.entity.OperationLogEntity;
import org.example.framework.logging.mapper.OperationLogMapper;
import org.example.framework.logging.service.IOperationLogService;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
public class OperationLogServiceImpl extends ServiceImpl<OperationLogMapper, OperationLogEntity> implements IOperationLogService {

    /**
     * 异步保存操作日志
     * @param entity
     */
    @Async  // 独立线程,不影响主线程业务
    @Override
    public void createOperationLog(OperationLogEntity entity) {
        // @Async 异步线程无法获取 SecurityContext(绑定在主线程 ThreadLocal),
        // 导致 MP 自动填充拿不到 userId,故需提前手动设置。
        this.save(entity);
    }
}

2.8 操作日志切面类

java 复制代码
package org.example.framework.logging.aspect;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.example.framework.logging.annotation.OperationLog;
import org.example.framework.logging.entity.OperationLogEntity;
import org.example.framework.logging.service.IOperationLogService;
import org.example.framework.security.util.SecurityUtils;
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.lang.reflect.Method;
import java.time.LocalDateTime;

/**
 * 日志 AOP 切面类
 */
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class OperationLogAspect {

    private final IOperationLogService operationLogService;
    private final ObjectMapper objectMapper;    // 把方法参数(Java 对象)转成 JSON 字符串,存入 requestParams 字段

    /**
     * 环绕通知:记录带 @OperationLog 注解的方法操作日志
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Around("@annotation(org.example.framework.logging.annotation.OperationLog)")   // 切点表达式
    public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable {
        // 1. 获取方法签名和注解
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        OperationLog anno = method.getAnnotation(OperationLog.class);

        // 2. 初始化日志实体
        OperationLogEntity entity = OperationLogEntity.builder()
                .description(anno.value())
                .operationType(anno.type())
                .success(true)  // 默认成功,异常时改为 false
                .build();

        // 3. 设置请求基本信息
        // 获取当前 HTTP 请求
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes != null) {
            HttpServletRequest request = attributes.getRequest();
            entity.setRequestMethod(request.getMethod());   // 请求方法
            entity.setRequestUrl(request.getRequestURI());  // 请求路径
            entity.setClientIp(getClientIp(request));       // 客户端 IP
        }

        // 4. 设置操作人(雪花ID)
        Long userId = SecurityUtils.getCurrentUserId();
        entity.setOperatorId(userId);

        // 5. 记录请求参数(安全脱敏)
        try {
            String params = objectMapper.writeValueAsString(joinPoint.getArgs());
            // 简单脱敏:移除常见敏感字段
            params = params.replaceAll("\"password\":\"[^\"]*\"", "\"password\":\"******\"");   // 目前只有管理员会使用到 password 字段,为此脱敏
            entity.setRequestParams(params);
        } catch (JsonProcessingException e) {
            entity.setRequestParams("[参数序列化失败]");
            log.warn("参数序列化失败: ", e);
        }

        // 6. 执行方法并记录结果
        long start = System.currentTimeMillis();
        Object result;
        try {
            result = joinPoint.proceed();   // 执行原方法
        } catch (Exception e) {
            entity.setSuccess(false);   // 标记失败
            entity.setErrorMessage(StringUtils.abbreviate(e.toString(), 1000)); // 截断错误信息
            throw e; // 重新抛出,不影响业务。@Async + 异常隔离,独立线程。此处如果抛业务异常会导致业务终止
        } finally {
            // 记录耗时和创建时间
            long cost = System.currentTimeMillis() - start;
            entity.setCostTime(cost);
            entity.setCreatedAt(LocalDateTime.now());

            // 异步保存日志
            operationLogService.createOperationLog(entity);
        }

        return result;
    }

    /**
     * 获取客户端真实 IP(支持代理)
     * @param request
     * @return
     */
    private String getClientIp(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Real-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        // 处理多IP情况(如 X-Forwarded-For: ip1, ip2, ip3)
        if (ip != null && ip.contains(",")) {
            ip = ip.split(",")[0].trim();
        }
        return ip;
    }
}

这里为了避免 @Autowired 字段注入的隐患,推荐使用 @RequiredArgsConstructor +
final 构造器注入
,原因如下:

  • 字段注入在切面中不可靠@Aspect 由 Spring AOP 和代理机制特殊处理,@Autowired 字段可能因初始化顺序问题为 null,导致空指针异常。
  • 构造器注入保证依赖非空 :Spring 在创建 Bean 时通过构造器注入依赖,确保切面实例化后所有 final 字段已就绪,避免运行时错误。
  • 符合 Spring 官方最佳实践:构造器注入更利于不可变性、单元测试和依赖显式化,官方推荐优先使用。

3. AOP操作日志注解使用

3.1 操作类型常量类

这里为了避免硬编码,定义一个操作类型的常量类。我有 common.constants 包,但现在还是放 framework.logging.constants 下吧。

java 复制代码
package org.example.framework.logging.constants;

/**
 * 操作类型常量类
 */
public class OperationLogType {
    public static final String CREATE = "CREATE";
    public static final String UPDATE = "UPDATE";
    public static final String DELETE = "DELETE";
    public static final String QUERY  = "QUERY";
    public static final String OPERATE = "OPERATE"; // 默认值
}

3.2 在业务层实现类中使用

java 复制代码
package org.example.business.service.impl;

...
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.example.business.dto.BrandDTO;
...
import org.example.business.entity.Brand;
import org.example.business.mapper.BrandMapper;
import org.example.business.service.IBrandService;
...
import org.example.business.vo.BrandAdminVO;
...
import org.example.framework.logging.annotation.OperationLog;
import org.example.framework.logging.constants.OperationLogType;
...
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Slf4j
@Service
public class BrandServiceImpl extends ServiceImpl<BrandMapper, Brand> implements IBrandService {

    ...

    /**
     * 新增一个品牌
     * @param dto
     * @return
     */
    @OperationLog(value = "新增品牌", type = OperationLogType.CREATE)
    @Transactional(rollbackFor = Exception.class)   // 先查后插,涉及多步操作,应开启事务管理
    @Override
    public BrandAdminVO createBrand(BrandDTO dto) {
        ...
    }

 	...
}

关于操作日志应该打在 Service 层,而不是 Controller

  • 日志要反映真实业务操作,而业务逻辑在 Service
  • 可保证与事务一致(失败不记日志);
  • 能被所有调用方(Web、定时任务等)统一覆盖。

其他注意事项:

  • 避免冗余控制台日志:
    • 控制器层不应打印请求参数或用户信息到控制台 (如 log.info() / log.debug()),因为操作日志已通过 AOP 统一记录操作人、操作内容及方法参数。
    • 业务层仅保留必要的 warn 级别日志info 级日志应改为 debug 并注释掉(或删除),保持生产环境日志干净。
  • 防止级联操作重复记录
    • 操作日志应仅标注在顶层业务入口方法 (即 Controller 直接调用的 Service 方法);
    • 内部调用的方法(如级联删除商品)不得添加 @OperationLog,避免因 Spring AOP 代理机制触发多次日志记录,导致审计信息冗余或失真。

4. AOP在Spring中的其他典型应用

Spring AOP(面向切面编程)是实现横切关注点(如日志、安全、事务等)的核心机制。以下是一些常见且重要的应用:

  1. 声明式事务管理(@Transactional
    • 最经典的应用:通过 AOP 自动开启/提交/回滚数据库事务;
    • 开发者无需手动写 beginTransaction() / commit()
  2. MyBatis-Plus 自动填充(MetaObjectHandler
    • 虽然 MP 的自动填充主要靠 MyBatis 插件,但部分扩展逻辑 (如结合用户上下文填充 createBy)常配合 AOP 使用;
    • 例如:在 Service 方法执行前,从 SecurityContext 获取当前用户 ID 并注入实体。
  3. Spring Security 方法级安全控制
    • 注解如 @PreAuthorize("hasRole('ADMIN')")@PostAuthorize@Secured 等;
    • 底层通过 AOP 拦截方法调用,在执行前校验权限,拒绝非法访问。
  4. Spring Boot Actuator 健康检查 & 指标收集
    • 部分指标(如方法调用次数、耗时)通过 AOP 切面自动采集;
    • 例如 @Timed(Micrometer)注解可记录方法性能。
  5. 缓存管理(@Cacheable, @CacheEvict, @CachePut
    • Spring Cache 抽象基于 AOP 实现;
    • 方法调用时自动查缓存、更新缓存或清除缓存,业务代码无感知。
  6. 异步方法执行(@Async
    • 标记方法为异步执行,底层通过 AOP 创建代理,将方法体提交到线程池;
    • 注意:SecurityContextTransactionThreadLocal 上下文默认不会传递(需额外处理)。
  7. 重试机制(Spring Retry)
    • 使用 @Retryable 注解的方法,失败后自动重试;
    • 由 AOP 切面拦截异常并控制重试逻辑。

总结:AOP 是 Spring 实现"非侵入式增强"的关键技术。

凡是需要在不修改业务代码的前提下,统一添加通用逻辑的地方,几乎都用到了 AOP。

相关推荐
狗头大军之江苏分军2 小时前
年底科技大考:2025 中国前端工程师的 AI 辅助工具实战盘点
java·前端·后端
一 乐2 小时前
酒店客房预订|基于springboot + vue酒店客房预订系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端
计算机毕设指导62 小时前
基于Spring Boot的防诈骗管理系统【源码文末联系】
java·spring boot·后端·spring·tomcat·maven·intellij-idea
a程序小傲2 小时前
饿了吗Java面试被问:Redis的持久化策略对比(RDBVS AOF)
java·redis·面试
我家领养了个白胖胖3 小时前
MCP模型上下文协议 Model Context Protocol & 百度地图MCP开发
java·后端·ai编程
Coder_Boy_3 小时前
基于DDD+Spring Boot 3.2+LangChain4j构建企业级智能客服系统
java·人工智能·spring boot·后端
黄俊懿3 小时前
【深入理解SpringCloud微服务】Spring-Security作用与原理解析
java·后端·安全·spring·spring cloud·微服务·架构师
塔能物联运维3 小时前
设备自适应采样率忽视能耗致续航降 后来结合功耗模型动态调优
java·后端·struts
rchmin3 小时前
Spring Boot自动装配原理解析
java·spring boot·后端