在java中利用AOP 自动化整合 Redis 缓存与分布式锁,同时实现链路追踪与日志脱敏

 一、前情提要

在高并发系统中,“稳定运行” 需要解决三大核心痛点:数据库压力过载、日志定位困难、敏感信息泄露。前两者关乎 “系统可用性”,后者关乎 “数据安全性”,三者缺一不可。

1、缓存的意义:解决数据库 “慢 IO 与并发上限” 瓶颈​。

数据库(如 MySQL)本质是磁盘存储系统,磁盘 IO 速度远慢于内存(内存读写毫秒级,磁盘读写秒级,差距超 1000 倍),且默认并发连接数有限(如 MySQL 默认最大连接数 151)。一旦系统并发量上来(比如每秒 1000 次查询),数据库会因 “慢 IO 堆积” 和 “连接耗尽” 成为瓶颈,极端情况会触发 “锁表”“连接超时”,直接宕机。​

核心思路是:尽可能让请求不碰数据库。Redis 作为内存数据库,读写速度极快(每秒支持 10 万 + 操作),能将高频查询结果暂存在内存中,让后续请求直接从内存拿数据 —— 这就是缓存的核心价值:用 “内存的快” 替代 “磁盘的慢”,拦截 90%+ 的高频请求,从源头减少数据库负担,是系统的 “长期防护屏障”。

2、Redis分布式锁的意义:解决缓存异常时的 “瞬时高并发”​。

缓存虽能拦截绝大多数请求,但存在 “漏洞”:当缓存过期、缓存穿透(查询不存在的数据)时,大量请求会瞬间绕过缓存涌向数据库,即 “缓存击穿 / 穿透” 问题,仍会直接压垮数据库。​

这时候需要分布式锁来 “限流兜底”:让 1000 个并发请求 “排队”,只允许 1 个请求抢锁成功后查数据库,其他请求自旋等待(如每隔 100ms 重试);待抢锁请求将数据库结果回写缓存后,释放锁,剩余请求直接从缓存拿数据。最终数据库最多只处理 1 次请求,完美避免 “瞬时高并发压垮数据库”,是系统的 “应急防护屏障”。​

简言之,缓存 + 分布式锁是 “数据库防护组合拳”:缓存负责 “日常拦截”,分布式锁负责 “异常兜底”,二者结合确保数据库稳定。

3、日志追踪与敏感脱敏的意义:解决 “日志难找 + 信息泄露” 问题​。

当系统上线后,新的痛点随之出现:​

  • 日志定位难:高并发场景下,每秒有成百上千条日志输出,一条请求的 “入口→Controller→Service→响应” 日志分散在海量记录中,前端反馈 bug 时,后端需翻遍日志才能匹配对应请求,效率极低;​
  • 敏感信息泄露:请求参数(如手机号、身份证号)、返回值(如用户密码)若直接打印到日志,会违反数据安全规范,存在隐私泄露风险。​

这些 “日志治理” 需求,本质也是与业务无关的通用逻辑:如果每个接口都手动写 “生成唯一标识、过滤敏感参数、打印日志” 代码,会导致代码臃肿、易遗漏(比如忘记脱敏某字段)。​

解决方案:用 “全链路 traceId 串联日志 + 注解式敏感脱敏”—— 在请求入口生成唯一 traceId,贯穿整个请求生命周期(日志输出、返回值),前端反馈 bug 时只需提供 traceId,后端即可快速定位所有关联日志;

同时通过注解标记敏感参数 / 返回值,自动过滤不打印,既解决 “日志难找” 问题,又保障 “数据安全”,是系统的 “可观测性与安全防护屏障”。​

二、项目需求

高并发系统的 “稳定三角”​。

缓存、分布式锁、日志追踪与敏感脱敏,三者共同构成高并发系统的 “稳定三角”:​

  • 缓存:长期防护,减少数据库日常请求;​
  • 分布式锁:应急兜底,解决缓存异常时的瞬时高并发;​
  • 日志追踪 + 脱敏:可观测性 + 安全,解决 bug 定位难与敏感信息泄露;​

而这三者的实现,都面临同一个问题:通用逻辑与业务逻辑耦合—— 缓存需写 “查缓存→回写缓存”,锁需写 “抢锁→释放锁”,日志需写 “生成 traceId→脱敏”,这些代码与 “查数据库、返回结果” 等核心业务无关,却要在每个方法中重复编写。​

AOP(面向切面编程)正是解决这类问题的 “利器”:将缓存、锁、日志等通用逻辑抽离成独立 “切面”,通过注解灵活插入业务方法,让开发者专注于核心业务。本文将通过三个实战案例,完整覆盖这三大场景,实现 “注解即功能” 的简洁代码。

三、实现方法

AOP的核心逻辑:它本质是“代理模式”的延伸,通过在“目标方法执行前后”插入通用逻辑,实现“业务与非业务逻辑解耦”。

假如我们要实现 “查询用户信息” 的功能,核心逻辑是查数据库,附加逻辑是:“先查 Redis、没查到再查库、查到后回写 Redis”,如果不用 AOP,代码会是这样:

// 不用AOP的写法:附加逻辑与核心逻辑耦合
public User getUserById(Long id) {
    // 1. 查Redis(附加逻辑)
    String key = "user:" + id;
    User user = redisTemplate.opsForValue().get(key);
    if (user != null) return user;
    
    // 2. 查数据库(核心业务逻辑)
    user = userMapper.selectById(id);
    
    // 3. 回写Redis(附加逻辑)
    if (user != null) redisTemplate.opsForValue().set(key, user);
    return user;
}

如果有10个查询方法,就要写 10 遍 “查缓存 - 回写缓存” 的逻辑。而用 AOP 后,我们只需:

  1. 把 “查缓存 - 回写缓存” 抽成一个切面
  2. 定义一个注解(如@MyRedisCache);
  3. 给需要加缓存的方法贴注解,切面自动生效。

这就是AOP的核心价值:通用逻辑抽离成切面,通过注解按需切入,业务代码只保留核心逻辑

1、实战一:用AOP实现Redis缓存插件

第一个案例是 “基于 AOP 的 Redis 缓存插件”—— 目标是让任意查询方法贴个注解,就能自动实现 “先查 Redis、再查 DB、回写 Redis” 的逻辑。

要实现这个插件,需要三步:

  1. 定义注解:作为 “切面的触发标识”,让方法知道 “需要加缓存”,同时通过注解参数传递缓存键前缀等配置;
  2. 编写切面:实现 “查缓存 - 查 DB - 回写缓存” 的通用逻辑,通过 AOP 的环绕通知(@Around)在目标方法执行前后插入逻辑;
  3. 业务中使用:在 Service 层的查询方法上贴注解,测试效果。

1.1 步骤1:定义缓存注解@MyRedisCache

注解的作用是“标记方法需要缓存”,并传入2个关键参数:

· keyPrefix:缓存键的前缀,如“user”、“product”;

· matchValue:Spring EL表达式(如#id),用于动态获取方法参数拼接缓存键。

// 注解只能贴在方法上
@Target(ElementType.METHOD)

// 注解在运行时生效(AOP需要在运行时解析)
@Retention(RetentionPolicy.RUNTIME)

public @interface MyRedisCache {
    // 缓存键前缀(必填)
    String keyPrefix();
    // 动态参数的Spring EL表达式(必填,如#id、#username)
    String matchValue();
}

1.2 步骤2:编写缓存切面类MyRedisCacheAspect

切面是AOP的核心,这里用环绕通知(@Around),它能完整控制目标方法的执行(执行前、执行中、执行后),正好满足“查缓存 -> 缓存命中则返回,未命中则执行方法 -> 回写缓存”的逻辑。

// 标记为切面类
@Aspect
// 交给Spring管理
@Component
@Slf4j
public class MyRedisCacheAspect {

    // 注入Redis操作模板
    @Resource
    private RedisTemplate redisTemplate;

    // 1. 定义切点:所有贴了@MyRedisCache注解的方法,都会被切面拦截
    @Pointcut("@annotation(com.atguigu.study.annotations.MyRedisCache)")
    public void cachePointCut() {}

    // 2. 环绕通知:在目标方法执行前后插入缓存逻辑
    @Around("cachePointCut()")
    public Object doCache(ProceedingJoinPoint joinPoint) throws Throwable {
        Object result = null;
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();

        // 3. 解析方法上的@MyRedisCache注解,获取配置参数
        MyRedisCache cacheAnnotation = method.getAnnotation(MyRedisCache.class);
        String keyPrefix = cacheAnnotation.keyPrefix(); // 如"user"
        String matchValueEl = cacheAnnotation.matchValue(); // 如"#id"

        // 4. 用Spring EL解析matchValue,动态获取方法参数(如#id对应的值1001)
        SpelExpressionParser parser = new SpelExpressionParser();
        Expression expression = parser.parseExpression(matchValueEl);
        EvaluationContext context = new StandardEvaluationContext();

        // 把方法参数名和值存入EL上下文(让EL表达式能找到#id对应的参数值)
        Object[] args = joinPoint.getArgs();
        Parameter[] parameters = method.getParameters();
        for (int i = 0; i < parameters.length; i++) {
            context.setVariable(parameters[i].getName(), args[i]);
        }
        String dynamicValue = expression.getValue(context).toString(); // 如"1001"

        // 5. 拼接最终的Redis缓存键(如"user:1001")
        String cacheKey = keyPrefix + ":" + dynamicValue;
        log.info("生成Redis缓存键:{}", cacheKey);

        // 6. 先查Redis:命中则直接返回,不执行目标方法
        result = redisTemplate.opsForValue().get(cacheKey);
        if (result != null) {
            log.info("Redis缓存命中,直接返回结果:{}", result);
            return result;
        }

        // 7. Redis未命中:执行目标方法(即查数据库)
        log.info("Redis缓存未命中,执行数据库查询");
        result = joinPoint.proceed(); // 触发Service方法执行(如userMapper.selectById)

        // 8. 回写Redis:将数据库结果存入缓存,下次查询直接命中
        if (result != null) {
            log.info("数据库查询成功,回写Redis缓存:{}", result);
            redisTemplate.opsForValue().set(cacheKey, result);
        }

        return result;
    }
}

1.3 在业务中使用注解

以“根据ID查询用户”为例,只需在Service方法上贴@MyRedisCache,无需写任何缓存逻辑:

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

    @Resource
    private UserMapper userMapper;

    // 贴注解:keyPrefix是"user",matchValue是#id(动态获取方法参数id)
    @Override
    @MyRedisCache(keyPrefix = "user", matchValue = "#id")
    public User getById(Serializable id) {
        // 核心业务逻辑:只查数据库,缓存逻辑由切面自动处理
        return userMapper.selectById(id);
    }
}

1.4 测试效果

  1. 第一次调用getById(1)
    • Redis 中没有user:1,切面执行userMapper.selectById(1)
    • 数据库查询成功后,回写user:1到 Redis。
  2. 第二次调用getById(1)
    • Redis 中命中user:1,直接返回结果,不查数据库。

完美实现 “缓存自动生效”,业务代码极其简洁!

2、实战二:升级AOP,整合分布式锁防止缓存击穿

基础版缓存有个问题:当缓存key过期时,如果大量请求同时进来,会同时查数据库(即缓存击穿),压垮数据库。解决办法是加分布式锁—— 让只有一个请求能查数据库,其他请求等待锁释放后查缓存。

我们基于第一个案例升级,用 AOP 实现 “缓存 + 分布式锁” 的整合,核心思路是:在 “查缓存未命中” 后,先抢锁,抢到锁的查数据库,没抢到的自旋等待。

2.1 核心升级点

  1. 新增注解@LockCache(支持更多配置,如缓存过期时间);
  2. 切面中整合 Redisson 分布式锁(比原生 Redis 锁更安全,支持自动续期、可重入);
  3. 处理 “缓存穿透”:数据库查不到时,存入一个空值到缓存(避免每次都查库)。

2.2 代码实现

2.2.1 步骤1:定义增强注解@LockCache

新增缓存过期时间参数,让注解更灵活。

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LockCache {
    // 缓存键前缀(默认"cache")
    String prefix() default "cache";
    // 缓存过期时间(默认30分钟)
    long timeout() default 1800;
    // 时间单位(默认秒)
    TimeUnit timeUnit() default TimeUnit.SECONDS;
    // 空值缓存时间(防穿透,默认5分钟)
    long nullTimeout() default 300;
}

2.2.2 步骤2:编写“缓存+分布式锁”切面

整合Redisson,核心逻辑是“缓存未命中 -> 抢锁 -> 抢到锁查数据库,回写进缓存 -> 释放锁”。

@Aspect
@Component
public class LockCacheAspect {

    @Autowired
    private RedisTemplate redisTemplate;

    // 注入Redisson客户端(需提前配置Redisson)
    @Autowired
    private RedissonClient redissonClient;

    // 环绕通知:整合缓存+分布式锁
    @SneakyThrows
    @Around("@annotation(com.atguigu.tingshu.common.cache.LockCache)")
    public Object cacheWithLock(ProceedingJoinPoint joinPoint) {
        Object result = null;
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        LockCache cacheAnnotation = signature.getMethod().getAnnotation(LockCache.class);

        // 1. 解析注解参数
        String prefix = cacheAnnotation.prefix();
        long timeout = cacheAnnotation.timeout();
        TimeUnit timeUnit = cacheAnnotation.timeUnit();
        long nullTimeout = cacheAnnotation.nullTimeout();

        // 2. 拼接缓存键(用方法参数列表作为动态值,适配多参数场景)
        Object[] args = joinPoint.getArgs();
        String cacheKey = prefix + ":" + Arrays.asList(args).toString();
        String lockKey = cacheKey + ":lock"; // 分布式锁的key(与缓存键绑定)

        try {
            // 3. 先查缓存:命中则返回
            result = redisTemplate.opsForValue().get(cacheKey);
            if (result != null) {
                // 处理空值缓存(如果是之前存入的空对象,返回null)
                if (result instanceof JSONObject && ((JSONObject) result).isEmpty()) {
                    return null;
                }
                return result;
            }

            // 4. 缓存未命中(得查数据库了):抢分布式锁
            RLock lock = redissonClient.getLock(lockKey);
            // 尝试抢锁:最多等3秒,抢到后锁自动释放时间为10秒(防死锁)
            boolean isLockSuccess = lock.tryLock(3, 10, TimeUnit.SECONDS);

            if (isLockSuccess) {
                try {
                    // 5. 抢到锁:执行目标方法(查数据库)
                    result = joinPoint.proceed();

                    // 6. 处理查询结果:分“有值”和“空值”回写缓存
                    if (result != null) {
                        // 有值:按正常过期时间存缓存
                        redisTemplate.opsForValue().set(cacheKey, result, timeout, timeUnit);
                    } else {
                        // 空值:按短过期时间存缓存(防穿透)
                        redisTemplate.opsForValue().set(cacheKey, new JSONObject(), nullTimeout, timeUnit);
                    }
                } finally {
                    // 7. 释放锁(必须在finally中,防止方法异常导致锁不释放)
                    if (lock.isHeldByCurrentThread()) {
                        lock.unlock();
                    }
                }
            } else {
                // 8. 没抢到锁:自旋等待(递归调用自己,直到抢到锁或查到缓存)
                Thread.sleep(100); // 休眠100ms再试,减少CPU占用
                return cacheWithLock(joinPoint);
            }
        } catch (Throwable e) {
            log.error("缓存+分布式锁处理异常", e);
            // 异常时直接查数据库(降级策略,保证业务可用)
            return joinPoint.proceed();
        }

        return result;
    }
}

2.2.3 步骤3:业务中使用增强注解

贴注解即可自动实现“缓存+分布式锁”:

@Service
public class AlbumInfoServiceImpl implements AlbumInfoService {

    @Resource
    private AlbumInfoMapper albumInfoMapper;

    // 贴注解:前缀是"album:stat",缓存1小时,空值缓存5分钟
    //prefix和Timeout是我们自定义注解@LockCache的属性参数,如果不填会隐式设置为默认值
    @Override
    @LockCache(prefix = RedisConstant.ALBUM_STAT_PREFIX, timeout = 3600)
    public AlbumStatVo getAlbumStatVoByAlbumId(Long albumId) {
        // 核心业务逻辑:只查数据库,缓存+锁由切面处理
        return albumInfoMapper.selectAlbumStat(albumId);
    }
}

升级后的优势

· 防缓存击穿只有一个请求能查数据库,其他请求等待锁释放后查缓存;

· 防缓存穿透:数据库查不到时,存入空值到缓存(短过期),避免重复查库;

· 锁安全:用 Redisson 锁,支持自动续期、可重入。

3、实战三:AOP 整合链路追踪与日志脱敏,解决 “日志Bug难找 + 敏感信息泄露”

前两个案例用 AOP 解决了 “缓存重复逻辑”,但实际开发中还有个高频痛点:每次接口调试时,日志分散在大量请求中(比如 100 个并发请求,分不清哪条日志对应哪个用户的请求);

同时,请求参数 / 返回值中的密码、手机号等敏感信息,会直接打印到日志,存在安全风险。

这些 “日志追踪”和“敏感脱敏” 逻辑,同样是与业务无关的通用型附加代码—— 如果每个 Controller 方法都写 “生成 traceId→打印日志→脱敏参数”,会导致代码臃肿且易遗漏。

此时 AOP 再次派上用场:我们可以把 “traceId 生成、日志打印、敏感脱敏” 抽成切面,通过自定义注解控制脱敏范围,让业务代码只关注 “查数据 / 返回结果”,无需关心日志细节。

3.1 需求拆解

  · 全链路追踪:每个请求生成唯一traceId,串联 “请求入口→Controller→日志输出→返回值”,前端拿到traceId后,后端能快速定位对应日志;

  · 敏感信息脱敏:通过注解标记 “不打印的参数 / 返回值”(如密码参数不记录、用户信息返回值不打印);

  · 自动增强返回值:所有接口返回值自动附加traceId,无需手动 set;

  · 线程安全:traceId需绑定当前请求线程,避免多线程串号。

3.2 实现步骤:注解 + AOP+ThreadLocal 三配合

  3.2.1 步骤 1:定义脱敏注解 @NoLogAnnotation(标记 “无需日志的目标”)

    和前两个案例的注解逻辑一致:用注解作为 “切面触发标识”,控制 “哪些参数 / 方法不打印日志”,代码如下:

// 可标记在方法(不打印返回值)或参数(不打印该参数)上​

@Target({ElementType.METHOD, ElementType.PARAMETER})​

// 运行时生效(AOP需动态解析)​

@Retention(RetentionPolicy.RUNTIME)​

public @interface NoLogAnnotation {​

// 无额外参数,仅作为“标记符”​

}

  3.2.2 步骤 2:编写核心切面(日志处理 + 返回值增强)

    需两个切面分工:ControllerLogAspect负责 “日志打印 + 参数脱敏”,ResultTraceIdAspect负责 “返回值注入 traceId”,确保职责单一。

  ① 切面 1:ControllerLogAspect(日志打印 + 敏感脱敏)​

    拦截所有 Controller 方法,实现 “打印类名方法名→过滤敏感参数→打印返回值” 逻辑:

@Order(Ordered.HIGHEST_PRECEDENCE) // 优先级最高,确保日志先打印

@Aspect

@Component

@Slf4j

public class ControllerLogAspect {

// 切点:所有Controller的方法

@Pointcut("execution(* com.zzyy..*Controller.*(..))")

public void logPointCut() {}

// 环绕通知:控制日志打印全流程

@Around("logPointCut()")

public Object doLog(ProceedingJoinPoint joinPoint) throws Throwable {

Object result = null;

MethodSignature signature = (MethodSignature) joinPoint.getSignature();

try {

// 1. 打印“类名+方法名”(定位请求归属)

log.info("【traceId:{}】接口方法:{}.{}",

TraceThreadLocal.getTraceId(), // 从ThreadLocal拿当前请求的traceId

signature.getDeclaringTypeName(),

signature.getName());

// 2. 过滤敏感参数后打印(带@NoLogAnnotation的参数不打印)

Map<String, Object> safeParams = this.getSafeParams(signature, joinPoint.getArgs());

log.info("【traceId:{}】方法参数:{}",

TraceThreadLocal.getTraceId(),

JSONUtil.toJsonStr(safeParams));

// 3. 执行目标方法(如Controller的getById)

result = joinPoint.proceed();

return result;

} finally {

// 4. 过滤敏感返回值后打印(方法带@NoLogAnnotation则不打印)

if (this.needPrintResult(signature)) {

log.info("【traceId:{}】方法返回值:{}",

TraceThreadLocal.getTraceId(),

JSONUtil.toJsonStr(result));

}

}

}

// 辅助方法1:过滤敏感参数(带@NoLogAnnotation的参数不加入日志)

private Map<String, Object> getSafeParams(MethodSignature signature, Object[] args) {

Map<String, Object> params = new LinkedHashMap<>();

String[] paramNames = signature.getParameterNames();

for (int i = 0; i < args.length; i++) {

// 跳过带@NoLogAnnotation的参数

if (signature.getMethod().getParameterAnnotations()[i].length > 0

&& signature.getMethod().getParameterAnnotations()[i][0] instanceof NoLogAnnotation) {

params.put(paramNames[i], "***(敏感参数,已脱敏)");

continue;

}

// 跳过ServletRequest/ServletResponse等无意义参数

if (args[i] instanceof ServletRequest || args[i] instanceof ServletResponse) {

continue;

}

params.put(paramNames[i], args[i]);

}

return params;

}

// 辅助方法2:判断返回值是否需要打印(方法带@NoLogAnnotation则不打印)

private boolean needPrintResult(MethodSignature signature) {

return signature.getMethod().getAnnotation(NoLogAnnotation.class) == null;

}

}

  ① 切面 2:ResultTraceIdAspect(返回值注入 traceId)

    所有返回值(包括异常场景)自动附加traceId,前端直接获取:

@Aspect

@Component

public class ResultTraceIdAspect {

// 切点:Controller方法 + 全局异常处理器(确保异常返回也带traceId)

@Pointcut("execution(* com.zzyy..*Controller.*(..)) || " +

"execution(* com.zzyy..*GlobalExceptionHandler.*(..))")

public void resultPointCut() {}

@Around("resultPointCut()")

public Object fillTraceId(ProceedingJoinPoint joinPoint) throws Throwable {

Object result = joinPoint.proceed();

// 若返回值是统一封装的ResultData,注入traceId

if (result instanceof ResultData) {

((ResultData<?>) result).setTraceId(TraceThreadLocal.getTraceId());

}

return result;

}

}

3.2.3 步骤 3:ThreadLocal+Filter 保障 traceId 线程安全

  traceId需要 “请求入口生成→线程内传递→请求结束清理”,用ThreadLocal存储(避免多线程串号),Filter控制生命周期:

  ① TraceThreadLocal(线程级存储 traceId)

public class TraceThreadLocal {

private static final ThreadLocal<String> TRACE_ID_HOLDER = new ThreadLocal<>();

public static final String TRACE_ID_KEY = "traceId";

// 设置traceId,并同步到MDC(日志模板可直接引用)

public static void setTraceId(String traceId) {

TRACE_ID_HOLDER.set(traceId);

MDC.put(TRACE_ID_KEY, traceId); // 配合logback.xml,日志自动带traceId

}

// 获取当前线程的traceId

public static String getTraceId() {

return TRACE_ID_HOLDER.get();

}

// 请求结束清理,避免内存泄漏

public static void removeTraceId() {

TRACE_ID_HOLDER.remove();

MDC.remove(TRACE_ID_KEY);

}

}

  ② TraceFilter(请求入口生成 traceId)

@Order(Ordered.HIGHEST_PRECEDENCE) // 比AOP更早执行,确保traceId先生成

@WebFilter(urlPatterns = "/**", filterName = "TraceFilter")

@Slf4j

public class TraceFilter extends OncePerRequestFilter {

@Override

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {

try {

// 1. 生成唯一traceId(UUID)

String traceId = IdUtil.fastSimpleUUID();

// 2. 绑定到当前线程

TraceThreadLocal.setTraceId(traceId);

log.info("【traceId:{}】请求开始:{}", traceId, request.getRequestURL());

long startTime = System.currentTimeMillis();

chain.doFilter(request, response); // 放行请求

// 3. 打印请求耗时

log.info("【traceId:{}】请求结束,耗时:{}ms", traceId, System.currentTimeMillis() - startTime);

} finally {

// 4. 清理traceId(必须在finally,避免异常导致内存泄漏)

TraceThreadLocal.removeTraceId();

}

}

}

3.2.4 步骤 4:日志模板配置(logback.xml)

  让所有日志自动带上traceId,无需手动拼接:

<configuration>

<!-- 日志格式:强制包含traceId(从MDC中取) -->

<property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss} [traceId:%X{traceId}] - %msg%n"/>

<!-- 控制台输出 -->

<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">

<encoder><pattern>${LOG_PATTERN}</pattern></encoder>

</appender>

<!-- 文件输出(按时间滚动) -->

<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">

<file>D:/logs/app.log</file>

<encoder><pattern>${LOG_PATTERN}</pattern></encoder>

<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">

<fileNamePattern>D:/logs/app-%d{yyyy-MM-dd}.log</fileNamePattern>

<maxHistory>30</maxHistory> <!-- 保留30天日志 -->

</rollingPolicy>

</appender>

<root level="info">

<appender-ref ref="CONSOLE"/>

<appender-ref ref="FILE"/>

</root>

</configuration>

3.3 业务中使用:注解贴一贴,功能全生效

  和前两个案例一样,业务代码只需贴注解,无需关心日志逻辑:

@RestController

public class UserController {

@Resource

private UserService userService;

// 1. 正常场景:打印参数+返回值,返回值带traceId

@GetMapping("/user/get/{id}")

public ResultData<User> getById(@PathVariable Integer id) {

return ResultData.success(userService.getById(id));

}

// 2. 脱敏返回值:方法带@NoLogAnnotation,不打印返回值(避免User的password泄露)

@GetMapping("/user/getv2/{id}")

@NoLogAnnotation

public ResultData<User> getByIdV2(@PathVariable Integer id) {

return ResultData.success(userService.getById(id));

}

// 3. 脱敏参数:参数带@NoLogAnnotation,不打印id(假设id是敏感信息)

@GetMapping("/user/getv3/{id}")

public ResultData<User> getByIdV3(@NoLogAnnotation @PathVariable Integer id) {

return ResultData.success(userService.getById(id));

}

}

3.4 测试效果:日志可追踪,敏感信息安全

  3.4.1 请求/user/get/1

    日志自动带traceId,参数和返回值完整打印:

2025-10-12 10:00:00 [traceId:1a2b3c] - 【traceId:1a2b3c】请求开始:http://localhost:8080/user/get/1

2025-10-12 10:00:00 [traceId:1a2b3c] - 【traceId:1a2b3c】接口方法:com.zzyy.study.controller.UserController.getById

2025-10-12 10:00:00 [traceId:1a2b3c] - 【traceId:1a2b3c】方法参数:{"id":1}

2025-10-12 10:00:00 [traceId:1a2b3c] - 【traceId:1a2b3c】方法返回值:{"code":200,"msg":"success","data":{"id":1,"username":"zhangsan","password":"123456"...},"traceId":"1a2b3c"}

2025-10-12 10:00:00 [traceId:1a2b3c] - 【traceId:1a2b3c】请求结束,耗时:50ms

3.4.2 请求/user/getv3/2

  参数id带@NoLogAnnotation,日志脱敏显示:

  2025-10-12 10:01:00 [traceId:4d5e6f] - 【traceId:4d5e6f】方法参数:{"id":"***(敏感参数,已脱敏)"}

四、知识点总结

1、使用Redis和Redis分布式锁的意义:都在于保护脆弱的数据库;Redis 缓存负责 “日常拦截”,减少 90%+ 的数据库请求,是 “长期防护”;Redis 分布式锁负责 “特殊情况兜底”,解决缓存过期 / 穿透时的瞬时高并发,是 “应急防护”。

2、AOP的核心思想:通用内容抽离成独立切面,通过注解形式有选择性地插入业务方法里。

3、AOP的核心价值:通用逻辑 “一次编写,到处使用”。无论是 “缓存查询”“分布式锁” 还是 “日志追踪”,只要是 “与业务无关、重复出现” 的逻辑,都可以用 AOP 抽成切面。比如:​

  • 缓存逻辑:切面控制 “查缓存→查 DB→回写缓存”;​
  • 锁逻辑:切面控制 “抢锁→执行方法→释放锁”;​
  • 日志逻辑:切面控制 “生成 traceId→打印日志→脱敏敏感信息”;​

业务代码只需贴注解,实现 “注解即功能” 的简洁性。

4、AOP的使用步骤:定义注解 -> 定义切面类 -> 在需要插入该切面类的业务方法上添加注解。

5、AOP+ThreadLocal:解决 “线程安全的上下文传递”。新插件中,traceId通过ThreadLocal绑定当前请求线程,避免多线程串号;同时结合Filter控制traceId的 “生成→清理” 生命周期,防止内存泄漏。这种 “Filter 初始化上下文→AOP 使用上下文→finally 清理” 的模式,可复用在 “用户登录态传递”“请求耗时统计” 等场景。

6、MDC:日志上下文传递的 “利器”。SLF4J 的 MDC(Mapped Diagnostic Context)可将traceId等上下文信息 “隐式” 传入日志模板,无需在每个log.info()中手动拼接traceId,只需在logback.xml配置%X{traceId},所有日志自动带上上下文,极大简化日志编写。

7、注解精准控制切面范围:自定义注解不仅是 “切面触发标识”,还能实现 “精细化控制”:​

  • @MyRedisCache:通过keyPrefix控制缓存键前缀;​
  • @LockCache:通过timeout控制缓存过期时间;​
  • @NoLogAnnotation:通过 “标记方法 / 参数” 控制脱敏范围;​

这种 “注解参数 + 切面解析” 的模式,让 AOP 逻辑更灵活,适配不同业务场景。​

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值