目录
- 一、需求背景
- [二、定义注解 @MethodExporter](#二、定义注解 @MethodExporter)
- 三、控制层使用注解
- [四、AOP 切面实现](#四、AOP 切面实现)
- 五、效果展示
- 六、总结与扩展
在日常的业务开发中,我们经常需要对某些关键方法进行统一的日志记录、调用耗时统计以及输入输出参数的追踪。如果每个方法都手动加日志代码,不仅冗余,还容易出错。
本文将通过 自定义注解 + Spring AOP 切面 的方式,实现一种轻量化的"方法导出器"(MethodExporter),可以对标记的方法自动进行日志上报。
一、需求背景
在很多系统中,尤其是微服务架构下,运维和排查问题时,经常需要知道:
- 哪个接口被调用了?
- 输入参数是什么?
- 返回结果如何?
- 耗时多少?
传统的做法是在方法里手写 log.info(...)
,但这样代码会变得非常冗余。
于是,我们可以自定义一个注解 @MethodExporter
,只要在方法上标注它,就能自动捕捉调用信息,统一输出日志。
二、定义注解 @MethodExporter
首先我们需要一个自定义注解,作用在方法上,运行时生效:
java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义方法导出器注解
*/
@Target({ElementType.METHOD}) // 仅作用在方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时可被反射获取
public @interface MethodExporter {
}
这个注解本身不包含任何业务逻辑,只是一个"标签",方便 AOP 进行拦截。
三、控制层使用注解
在 Controller
中,我们只要在需要上报的方法上加上 @MethodExporter
:
java
@RestController
@Slf4j
public class MethodExporterController {
// http://localhost:24618/method/list?page=1&rows=7
@GetMapping("/method/list")
@MethodExporter
public Map list(@RequestParam(value = "page", defaultValue = "1") int page,
@RequestParam(value = "rows", defaultValue = "5") int rows) {
Map<String, String> result = new LinkedHashMap<>();
result.put("code", "200");
result.put("message", "success");
// 模拟随机耗时
try {
TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
return result;
}
@GetMapping("/method/get")
@MethodExporter
public Map get() {
Map<String, String> result = new LinkedHashMap<>();
result.put("code", "404");
result.put("message", "not-found");
try {
TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
return result;
}
@GetMapping("/method/update")
public String update() {
System.out.println("update method without @MethodExporter");
return "ok update";
}
}
这里的 list
和 get
方法会触发 AOP,而 update
方法不会。
四、AOP 切面实现
切面是核心,它会拦截所有带有 @MethodExporter
注解的方法,记录输入参数、返回结果、耗时。
java
@Aspect
@Component
@Slf4j
public class MethodExporterAspect {
@Around("@annotation(com.donglin.interview2.annotations.MethodExporter)")
public Object methodExporter(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
Object retValue;
long startTime = System.currentTimeMillis();
System.out.println("-----@Around环绕通知AAA-methodExporter");
// 执行目标方法
retValue = proceedingJoinPoint.proceed();
long endTime = System.currentTimeMillis();
long costTime = endTime - startTime;
// 获取方法签名
MethodSignature signature = (MethodSignature) proceedingJoinPoint.getSignature();
Method method = signature.getMethod();
// 判断是否有 MethodExporter 注解
MethodExporter methodExporterAnnotation = method.getAnnotation(MethodExporter.class);
if (methodExporterAnnotation != null) {
// 获取输入参数
StringBuilder jsonInputParam = new StringBuilder();
Object[] args = proceedingJoinPoint.getArgs();
DefaultParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer();
String[] parameterNames = discoverer.getParameterNames(method);
if (parameterNames != null) {
for (int i = 0; i < parameterNames.length; i++) {
jsonInputParam.append(parameterNames[i])
.append("=")
.append(args[i].toString())
.append(";");
}
}
// 序列化返回结果
String jsonResult = retValue != null ?
new ObjectMapper().writeValueAsString(retValue) : "null";
log.info("\n方法分析上报中 " +
"\n类名方法名: {}.{}()" +
"\n执行耗时: {} 毫秒" +
"\n输入参数: {}" +
"\n返回结果: {}" +
"\nover",
proceedingJoinPoint.getTarget().getClass().getName(),
proceedingJoinPoint.getSignature().getName(),
costTime,
jsonInputParam,
jsonResult
);
System.out.println("-----@Around环绕通知BBB-methodExporter");
}
return retValue;
}
}
五、效果展示
调用 list
接口
访问:
http://localhost:24618/method/list?page=1&rows=7
日志输出:

调用 update
接口
访问:
http://localhost:24618/method/update
输出:

说明 AOP 没有生效。
六、总结与扩展
通过 @MethodExporter
注解 + AOP,我们实现了对方法的统一拦截和日志上报。这样可以大大减少样板代码,同时让日志收集更可控。
未来可以扩展的功能:
- 将日志结果存入数据库 / Kafka / ELK 进行分析。
- 配合
@MyRedisCache
等注解,做缓存层封装。 - 结合
Spring Boot Actuator
,进一步做监控指标上报。
这是一种通用的设计模式:用注解声明,用 AOP 实现横切逻辑。