Lombok + 自定义注解 + AOP:构建轻量级日志增强方案

在编写业务代码时,我们常常需要在方法入口打印入参、计算耗时、捕获异常。如果每个方法都手动编写 log.info(...),既枯燥又容易出错。借助 Lombok自定义注解 和 Spring AOP,我们可以将日志逻辑统一抽离,实现零侵入、易维护的横切关注点管理。本文将从核心概念到实战示例,带你一步步搭建一套轻量级日志增强组件


一、Lombok:消灭样板代码

Lombok 是一颗 Java 语法糖,通过注解在编译期自动生成构造器、getter/setter、日志对象等。本节只关注它对日志的支持。

1.1 传统日志声明 vs Lombok

// 传统写法
public class UserService {
    private static final Logger log = LoggerFactory.getLogger(UserService.class);
  
    public void doSomething() {
        log.info("doing something");
    }
}

// Lombok 写法
@Slf4j
public class UserService {
    public void doSomething() {
        log.info("doing something");  // 直接使用自动生成的 log 对象
    }
}

@Slf4j 会在编译时注入名为 log 的静态常量,相当于一行注解取代了样板声明。同理还有 @Log@Log4j2 等变体,可按日志框架选用。

除了日志,Lombok 常用的注解还有:

注解功能
@Data自动生成 getter/setter、toStringequalshashCode
@Builder生成建造者模式代码
@AllArgsConstructor / @NoArgsConstructor生成全参/无参构造器
@Accessors(chain = true)使 setter 返回 this,支持链式调用

这些注解能够大幅减少 POJO 和 Service 中的冗余代码,让类更专注于业务逻辑。


二、自定义注解:给方法贴上“描述标签”

Java 注解(Annotation)是一种元数据,本身不包含业务逻辑,但可以在运行时通过反射读取。为了标记需要记录日志的方法并携带业务描述,我们定义一个自定义注解 @LogRecord

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)  // 运行时保留,反射可读
@Target({ElementType.METHOD})        // 只能用于方法
@Documented
public @interface LogRecord {
    /**
     * 业务操作描述,例如:"查询用户列表"
     */
    String value() default "";

    /**
     * 是否打印方法返回值(默认 false,避免大对象导致性能问题)
     */
    boolean printResult() default false;
}
  • @Retention(RetentionPolicy.RUNTIME):保证注解在程序运行期间依然存在,AOP 切面才能通过反射获取它。

  • @Target(ElementType.METHOD):限定只能方法使用,避免误贴到类上。

  • 两个属性 value 和 printResult 为后续切面提供了灵活的配置能力。

使用方法非常简单,直接在 Controller 或 Service 的方法上添加即可:

@LogRecord(value = "分页查询商品", printResult = true)
public List<Product> listProducts(ProductQuery query) {
    // 业务逻辑...
}

三、Spring AOP 核心概念速览

AOP(Aspect-Oriented Programming,面向切面编程)允许我们将散落在各处的横切关注点(日志、事务、权限等)抽离成独立的切面。关键是理解以下几个角色:

  • 切面(Aspect)
    封装了横切逻辑的类,通常使用 @Aspect 和 @Component 注解。

  • 切点(Pointcut)
    表达式,用来匹配哪些方法需要被增强。例如:@annotation(com.example.LogRecord) 表示所有标注了该注解的方法。

  • 通知(Advice)
    在切点匹配的连接点处执行的代码。常用类型:

    • @Before:前置通知

    • @AfterReturning:后置返回通知

    • @AfterThrowing:异常通知

    • @Around:环绕通知(最强大,可控制方法执行前后及异常)

  • 织入(Weaving)
    将切面应用到目标对象并生成代理的过程。Spring AOP 默认基于动态代理实现。

下图展示了 AOP 的工作流程:业务方法调用时,代理对象会先匹配切点,然后按配置的顺序执行相应的通知,最后才到达真正的目标方法。


四、实战:用环绕通知打造日志切面

下面我们实现一个切面,对标记了 @LogRecord 的方法自动输出:操作描述、入参、执行耗时,并按需打印返回值。

4.1 依赖准备

Spring Boot 项目通常已包含 AOP 启动器,如果没有,手动添加:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

4.2 切面代码

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Aspect
@Component
@Slf4j
public class LogRecordAspect {

    private static final ObjectMapper MAPPER = new ObjectMapper();

    // 切点:所有标注 @LogRecord 的方法
    @Pointcut("@annotation(com.example.annotation.LogRecord)")
    public void logPointcut() {}

    @Around("logPointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();

        // ----- 获取方法签名和注解信息 -----
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        LogRecord logRecord = method.getAnnotation(LogRecord.class);

        String desc = logRecord.value();
        String className = joinPoint.getTarget().getClass().getSimpleName();
        String methodName = method.getName();
        Object[] args = joinPoint.getArgs();

        // 入参转 JSON(实际可加脱敏处理)
        String argsJson = toJson(args);

        log.info("【{}】开始执行 -> 类:{}, 方法:{}, 参数:{}", desc, className, methodName, argsJson);

        Object result;
        try {
            result = joinPoint.proceed();  // 执行目标方法
        } catch (Throwable e) {
            log.error("【{}】执行异常,异常信息:{}", desc, e.getMessage(), e);
            throw e; // 异常继续抛出,不破坏原有流程
        }

        long cost = System.currentTimeMillis() - start;
        log.info("【{}】执行结束,耗时:{}ms", desc, cost);

        if (logRecord.printResult()) {
            log.info("【{}】返回结果:{}", desc, toJson(result));
        }
        return result;
    }

    private String toJson(Object obj) {
        try {
            return MAPPER.writeValueAsString(obj);
        } catch (Exception e) {
            return obj == null ? "null" : obj.toString();
        }
    }
}

4.3 代码讲解

  • @Pointcut("@annotation(...)")
    通过注解匹配切点,任何打上 @LogRecord 的方法都会被拦截。

  • @Around 通知
    我们选择环绕通知,因为它可以同时控制方法执行前、后以及异常时的行为。ProceedingJoinPoint 提供了执行目标方法的能力(proceed()),并可以获取方法签名、参数、目标对象等信息。

  • 获取注解
    通过 MethodSignature 拿到反射的 Method,再调用 getAnnotation(LogRecord.class) 读取注解属性,从而得到业务描述和打印配置。

  • 异常处理
    捕获异常后记录日志,然后重新抛出,保证上层调用者仍能感知异常,不影响原有事务机制。

  • JSON 序列化
    使用 Jackson 将对象转为字符串,便于阅读;同时增加容错,避免因序列化失败导致日志记录中断。

4.4 使用效果

在项目中的方法上添加注解:

@RestController
@RequestMapping("/product")
@Slf4j
public class ProductController {

    @LogRecord(value = "查询商品列表", printResult = true)
    @GetMapping("/list")
    public List<Product> list(ProductQuery query) {
        // 业务查询...
        return productService.list(query);
    }

    @LogRecord(value = "创建商品")
    @PostMapping("/create")
    public Product create(@RequestBody Product product) {
        return productService.save(product);
    }
}

调用接口后,控制台输出类似:

【查询商品列表】开始执行 -> 类:ProductController, 方法:list, 参数:{"keyword":"手机"}
【查询商品列表】执行结束,耗时:42ms
【查询商品列表】返回结果:[{...}, {...}]

业务代码无需任何日志逻辑,完美解耦

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值