如何通过自定义注解实现零代码侵入的方法日志记录
- 方案一:动态代理 + 反射
- 利用 Java 原生动态代理和反射机制,对标记自定义注解的方法做代理增强:通过代理类拦截目标方法执行,在方法执行前后 / 异常时解析注解信息并输出日志,全程仅依赖 Java 原生 API,无第三方框架依赖。
- 先创建自定义注解
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 MyLog {
// 日志描述
String value() default "";
}
- 定义需要实现的测试接口
java
复制代码
public interface UserService {
Integer testMyLog(int a1, int a2);
}
- 对该接口的具体实现并加上自定义注解
java
复制代码
import com.example.annotation.MyLog;
import com.example.user.UserService;
public class UserServiceImpl implements UserService {
@Override
@MyLog("测试自定义注解")
public Integer testMyLog(int a1, int a2) {
return a1+a2;
}
}
- 接下来实现方法的动态代理,以及在实现具体日志的输出
java
复制代码
import com.example.annotation.MyLog;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
@Slf4j
public class LogProxyInterceptor implements InvocationHandler {
private final Object target;
public LogProxyInterceptor(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 步骤1:从实现类方法上获取@MyLog注解
Method implMethod = target.getClass().getMethod(method.getName(), method.getParameterTypes());
MyLog myLog = implMethod.getAnnotation(MyLog.class);
//判断是否有自定义注解,若有则实现相应的日志输出
boolean hasLog = myLog != null;
String methodName = method.getName();
Object result = null;
try {
// 步骤2:方法执行前记录日志
if (hasLog) {
logBefore(methodName, myLog.value(), args);
}
// 步骤3:执行目标方法
result = method.invoke(target, args);
// 步骤4:方法执行成功后记录日志
if (hasLog) {
logAfter(methodName, result);
}
return result;
} catch (Exception e) {
// 步骤5:方法执行异常时记录日志
if (hasLog) {
logException(methodName, e);
}
throw e;
}
}
/**
* 记录方法执行前的日志
* @param methodName 方法名
* @param description 注解描述
* @param args 方法参数
*/
private void logBefore(String methodName, String description, Object[] args) {
if (args == null || args.length == 0) {
// 无参数的情况
log.info("========== 方法调用开始 ==========\n" +
"操作描述: {}\n" +
"方法名称: {}\n" +
"参数信息: 无参数\n" +
"==================================",
description, methodName);
} else {
// 有参数的情况
log.info("========== 方法调用开始 ==========\n" +
"操作描述: {}\n" +
"方法名称: {}\n" +
"参数个数: {}\n" +
"参数详情: {}\n" +
"==================================",
description, methodName, args.length, Arrays.toString(args));
}
}
/**
* 记录方法执行成功后的日志
* @param methodName 方法名
* @param result 方法返回值
*/
private void logAfter(String methodName, Object result) {
log.info("========== 方法调用成功 ==========\n" +
"方法名称: {}\n" +
"返回结果: {}\n" +
"==================================",
methodName, result);
}
/**
* 记录方法执行异常的日志
* @param methodName 方法名
* @param e 异常对象
*/
private void logException(String methodName, Exception e) {
log.error("========== 方法调用异常 ==========\n" +
"方法名称: {}\n" +
"异常类型: {}\n" +
"异常信息: {}\n" +
"==================================",
methodName, e.getClass().getSimpleName(), e.getMessage());
}
/**
* 创建代理对象
* @param target 目标对象
* @return 代理对象
*/
public static Object getProxy(Object target) {
return Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new LogProxyInterceptor(target)
);
}
}
- 方案二:基于Spring AOP
- 利用 Spring AOP 的 "面向切面" 特性,将日志记录逻辑封装为独立切面,通过@annotation(myLog)切入点匹配所有标记@MyLog的方法,实现日志统一增强,依托 Spring 容器自动管理代理对象。
- 引入AOP依赖
java
复制代码
<!-- Spring Boot AOP Starter(核心依赖) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
- 创建自定义注解
java
复制代码
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyLog {
//方法描述
String description() default "";
}
- 创建测试方法
java
复制代码
import com.example.annotation.MyLog;
import org.springframework.stereotype.Service;
@Service
public class TestService {
@MyLog(description = "测试方法日志")
public void testMyLog() {
System.out.println("testMyLog");
}
@MyLog(description = "带参数的方法")
public String testWithParams(String name, int age) {
System.out.println("testWithParams: " + name + ", " + age);
return "result: " + name;
}
@MyLog(description = "有返回值的方法")
public String testWithReturn() {
System.out.println("testWithReturn");
return "Hello, AOP!";
}
@MyLog(description = "会抛出异常的方法")
public void testWithException() {
System.out.println("testWithException");
throw new RuntimeException("测试异常");
}
}
- 创建切面类
java
复制代码
import com.example.annotation.MyLog;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import java.util.Arrays;
@Aspect
@Component
@Slf4j
public class MethodLogAspect {
/**
* 环绕通知:拦截带有@MyLog注解的方法
* 在方法执行前后记录日志信息
*
* @param joinPoint 连接点,包含方法执行的上下文信息
* @param myLog 自定义日志注解实例
* @return 方法执行结果
* @throws Throwable 方法执行异常时抛出
*/
@Around("@annotation(myLog)")
public Object around(ProceedingJoinPoint joinPoint, MyLog myLog) throws Throwable {
// 获取方法名
String methodName = joinPoint.getSignature().getName();
Object result = null;
try {
// 步骤1:方法执行前记录日志
logBefore(methodName, myLog.description(), joinPoint.getArgs());
// 步骤2:执行目标方法
result = joinPoint.proceed();
// 步骤3:方法执行成功后记录日志
logAfter(methodName, result);
return result;
} catch (Throwable throwable) {
// 步骤4:方法执行异常时记录日志
logException(methodName, throwable);
throw throwable;
}
}
/**
* 记录方法执行前的日志
*
* @param methodName 方法名
* @param description 注解描述
* @param args 方法参数
*/
private void logBefore(String methodName, String description, Object[] args) {
if (args == null || args.length == 0) {
// 无参数的情况
log.info("========== 方法调用开始 ==========\n" +
"操作描述: {}\n" +
"方法名称: {}\n" +
"参数信息: 无参数\n" +
"==================================",
description, methodName);
} else {
// 有参数的情况
log.info("========== 方法调用开始 ==========\n" +
"操作描述: {}\n" +
"方法名称: {}\n" +
"参数个数: {}\n" +
"参数详情: {}\n" +
"==================================",
description, methodName, args.length, Arrays.toString(args));
}
}
/**
* 记录方法执行成功后的日志
*
* @param methodName 方法名
* @param result 方法返回值
*/
private void logAfter(String methodName, Object result) {
log.info("========== 方法调用成功 ==========\n" +
"方法名称: {}\n" +
"返回结果: {}\n" +
"==================================",
methodName, result);
}
/**
* 记录方法执行异常的日志
*
* @param methodName 方法名
* @param throwable 异常对象
*/
private void logException(String methodName, Throwable throwable) {
log.error("========== 方法调用异常 ==========\n" +
"方法名称: {}\n" +
"异常类型: {}\n" +
"异常信息: {}\n" +
"==================================",
methodName, throwable.getClass().getSimpleName(), throwable.getMessage());
}
}
| 对比维度 |
动态代理 + 反射 |
Spring AOP |
| 框架依赖 |
无(纯 Java 原生) |
强依赖 Spring |
| 代理方式 |
JDK 动态代理(仅支持接口) |
默认 JDK 代理,可配置 CGLIB(支持类) |
| 对象管理 |
手动创建代理对象 |
Spring 容器自动管理 |
| 侵入性 |
零侵入,但需手动替换业务对象 |
完全零侵入(Spring 自动代理) |
| 扩展性 |
扩展需修改代理类,灵活性低 |
切面解耦,扩展灵活 |
| 适用场景 |
非 Spring 项目、轻量级 Java 项目 |
Spring Boot/Spring MVC 项目、企业级应用 |
- 两种方案均通过 "自定义注解 + 代理增强" 实现零侵入日志记录
- 若项目无 Spring 依赖、追求轻量,选「动态代理 + 反射」;
- 若项目基于 Spring 生态、需灵活扩展,选「Spring AOP」。