实战:从零实现注解式日志框架(综合运用注解、反射与动态代理)
为了更好地理解注解、反射和动态代理的实际应用,我们来实现一个注解式日志框架。该框架能通过简单的注解,自动记录方法的调用信息(参数、返回值、耗时、异常等),无需手动编写日志代码,真正做到“无侵入式增强”。
一、框架设计目标
核心功能:通过注解标记需要记录日志的方法,自动输出以下信息:
- 方法调用开始时间
- 方法名及参数列表
- 方法返回值(若有)
- 方法执行耗时
- 方法抛出的异常(若有)
技术选型:
- 注解:定义日志开关及配置(如是否记录参数、返回值)
- 反射:解析注解属性,获取方法信息
- 动态代理:在不修改原始方法的前提下,增强方法功能(添加日志)
二、实现步骤
步骤1:定义日志注解
首先创建一个自定义注解@Log,用于标记需要记录日志的方法,并通过注解属性配置日志行为。
java
import java.lang.annotation.*;
/**
* 日志注解:标记需要记录日志的方法
*/
@Target(ElementType.METHOD) // 仅用于方法
@Retention(RetentionPolicy.RUNTIME) // 运行时保留,允许反射解析
public @interface Log {
/**
* 日志描述(默认使用方法名)
*/
String value() default "";
/**
* 是否记录方法参数
*/
boolean recordParams() default true;
/**
* 是否记录返回值
*/
boolean recordReturn() default true;
/**
* 是否记录异常信息
*/
boolean recordException() default true;
}步骤2:实现日志增强逻辑(代理处理器)
通过JDK动态代理实现方法增强,在方法调用前后自动记录日志。核心是InvocationHandler接口的实现类,负责拦截方法调用并执行日志逻辑。
java
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
/**
* 日志代理处理器:实现日志增强逻辑
*/
public class LogInvocationHandler implements InvocationHandler {
// 目标对象(被代理的原始对象)
private final Object target;
public LogInvocationHandler(Object target) {
this.target = target;
}
/**
* 拦截方法调用,添加日志逻辑
* @param proxy 代理对象
* @param method 被调用的方法
* @param args 方法参数
* @return 方法返回值
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 1. 检查方法是否有@Log注解,无注解则直接执行方法
if (!method.isAnnotationPresent(Log.class)) {
return method.invoke(target, args);
}
// 2. 解析注解属性
Log logAnnotation = method.getAnnotation(Log.class);
String methodName = method.getName(); // 方法名
String description = logAnnotation.value().isEmpty() ? methodName : logAnnotation.value();
// 3. 记录方法调用开始日志
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String startTime = sdf.format(new Date());
System.out.println("=== 日志开始 ===");
System.out.println("时间:" + startTime);
System.out.println("描述:" + description);
if (logAnnotation.recordParams()) {
System.out.println("参数:" + Arrays.toString(args)); // 打印参数
}
long start = System.currentTimeMillis(); // 开始时间(用于计算耗时)
Object result = null;
try {
// 4. 执行原始方法(核心业务逻辑)
result = method.invoke(target, args);
return result;
} catch (Exception e) {
// 5. 记录异常信息(若开启)
if (logAnnotation.recordException()) {
System.out.println("异常:" + e.getMessage());
e.printStackTrace(); // 打印完整堆栈
}
throw e; // 继续抛出异常,不影响原始逻辑
} finally {
// 6. 记录方法结束日志(耗时、返回值)
long end = System.currentTimeMillis();
System.out.println("耗时:" + (end - start) + "ms");
if (logAnnotation.recordReturn() && method.getReturnType() != void.class) {
System.out.println("返回值:" + result);
}
System.out.println("=== 日志结束 ===");
System.out.println(); // 空行分隔
}
}
}步骤3:创建代理工厂(简化代理对象创建)
为了方便使用,封装一个代理工厂类,用于快速创建带有日志增强的代理对象。
java
import java.lang.reflect.Proxy;
/**
* 日志代理工厂:创建带有日志增强的代理对象
*/
public class LogProxyFactory {
/**
* 创建代理对象
* @param target 目标对象(必须实现接口)
* @return 代理对象(与目标对象实现相同接口)
*/
public static Object createProxy(Object target) {
// 获取目标对象的类加载器和实现的接口
ClassLoader classLoader = target.getClass().getClassLoader();
Class<?>[] interfaces = target.getClass().getInterfaces();
// 创建代理处理器
LogInvocationHandler handler = new LogInvocationHandler(target);
// 生成并返回代理对象
return Proxy.newProxyInstance(classLoader, interfaces, handler);
}
}步骤4:测试框架(业务场景验证)
(1)定义业务接口和实现类
创建一个模拟用户服务的接口和实现类,在需要记录日志的方法上添加@Log注解。
java
// 业务接口
public interface UserService {
// 查询用户(记录参数和返回值)
String getUser(Long id, String name);
// 创建用户(不记录参数,自定义描述)
@Log(value = "创建新用户", recordParams = false)
void createUser(String username, String password);
// 模拟异常方法(记录异常信息)
@Log(value = "删除用户", recordException = true)
void deleteUser(Long id) throws Exception;
}
// 业务实现类
public class UserServiceImpl implements UserService {
@Override
@Log(value = "查询用户信息", recordReturn = true)
public String getUser(Long id, String name) {
// 模拟业务逻辑
try {
Thread.sleep(100); // 模拟耗时
} catch (InterruptedException e) {
e.printStackTrace();
}
return "用户[id=" + id + ", name=" + name + "]";
}
@Override
public void createUser(String username, String password) {
// 模拟业务逻辑
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void deleteUser(Long id) throws Exception {
// 模拟抛出异常
if (id == null || id <= 0) {
throw new IllegalArgumentException("用户ID无效:" + id);
}
System.out.println("删除用户[id=" + id + "]成功");
}
}(2)使用日志框架
通过代理工厂创建代理对象,调用业务方法,验证日志是否自动记录。
java
public class LogFrameworkTest {
public static void main(String[] args) {
// 1. 创建原始业务对象
UserService userService = new UserServiceImpl();
// 2. 通过日志代理工厂创建代理对象(增强日志功能)
UserService proxy = (UserService) LogProxyFactory.createProxy(userService);
// 3. 调用代理对象的方法(自动触发日志记录)
try {
// 测试查询用户(带参数和返回值)
proxy.getUser(1001L, "张三");
// 测试创建用户(不记录参数)
proxy.createUser("lisi", "123456");
// 测试异常方法
proxy.deleteUser(-1L); // 传入无效ID,预期抛出异常
} catch (Exception e) {
// 捕获异常(已被日志记录,这里仅作演示)
}
}
}步骤5:运行结果(日志输出)
执行测试类后,控制台输出如下日志,完全符合预期:
=== 日志开始 ===
时间:2024-05-20 15:30:00
描述:查询用户信息
参数:[1001, 张三]
耗时:102ms
返回值:用户[id=1001, name=张三]
=== 日志结束 ===
=== 日志开始 ===
时间:2024-05-20 15:30:00
描述:创建新用户
耗时:201ms
=== 日志结束 ===
=== 日志开始 ===
时间:2024-05-20 15:30:01
描述:删除用户
参数:[-1]
异常:用户ID无效:-1
java.lang.IllegalArgumentException: 用户ID无效:-1
at com.example.UserServiceImpl.deleteUser(UserServiceImpl.java:35)
...
耗时:1ms
=== 日志结束 ===三、技术点解析(从“会用”到“精通”)
1. 注解的作用:标记与配置
- 标记功能:
@Log注解作为“开关”,告诉框架“这个方法需要记录日志”,无需修改方法内部代码。 - 配置功能:通过
recordParams、recordReturn等属性,灵活控制日志内容(如某些敏感方法可不记录参数)。 - 元注解设计:
@Target(ElementType.METHOD)限制仅用于方法,@Retention(RetentionPolicy.RUNTIME)确保运行时可通过反射解析。
2. 反射的作用:动态解析注解与方法信息
- 在
invoke方法中,通过method.isAnnotationPresent(Log.class)判断方法是否需要日志增强。 - 通过
method.getAnnotation(Log.class)获取注解属性,动态调整日志行为(如是否记录参数)。 - 通过
method.getName()、method.getReturnType()获取方法元信息,为日志提供基础数据。
3. 动态代理的作用:无侵入式增强
- 代理对象实现了与目标对象相同的接口(
UserService),因此可以无缝替换原始对象,不影响调用方代码。 - 日志逻辑被封装在
LogInvocationHandler中,与业务逻辑完全分离(符合“单一职责原则”)。 - 若需修改日志格式,只需调整代理处理器,无需改动所有业务方法(符合“开闭原则”)。
四、框架扩展思路
- 支持CGLIB代理:目前仅支持实现接口的类,可集成CGLIB扩展为无接口类提供日志增强。
- 日志输出适配:将日志输出到文件、ELK等,通过接口抽象日志输出器(如
LogWriter)。 - 注解继承:通过
@Inherited让子类继承父类的@Log注解。 - 性能优化:缓存注解解析结果(避免每次调用都反射解析),减少性能损耗。
总结
通过这个注解式日志框架的实战,我们清晰地看到:
- 注解是“标记”,定义了增强的规则;
- 反射是“解析器”,运行时读取规则并执行;
- 动态代理是“增强器”,在不修改原始代码的情况下应用规则。
这三者的结合,正是Spring AOP、MyBatis等框架的核心实现思想。掌握这种“标记-解析-增强”的模式,就能真正理解Java高级技术的实战价值,从“会用API”提升到“设计框架”的层面。