Skip to content

实战:从零实现注解式日志框架(综合运用注解、反射与动态代理)

为了更好地理解注解、反射和动态代理的实际应用,我们来实现一个注解式日志框架。该框架能通过简单的注解,自动记录方法的调用信息(参数、返回值、耗时、异常等),无需手动编写日志代码,真正做到“无侵入式增强”。

一、框架设计目标

  1. 核心功能:通过注解标记需要记录日志的方法,自动输出以下信息:

    • 方法调用开始时间
    • 方法名及参数列表
    • 方法返回值(若有)
    • 方法执行耗时
    • 方法抛出的异常(若有)
  2. 技术选型

    • 注解:定义日志开关及配置(如是否记录参数、返回值)
    • 反射:解析注解属性,获取方法信息
    • 动态代理:在不修改原始方法的前提下,增强方法功能(添加日志)

二、实现步骤

步骤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注解作为“开关”,告诉框架“这个方法需要记录日志”,无需修改方法内部代码。
  • 配置功能:通过recordParamsrecordReturn等属性,灵活控制日志内容(如某些敏感方法可不记录参数)。
  • 元注解设计@Target(ElementType.METHOD)限制仅用于方法,@Retention(RetentionPolicy.RUNTIME)确保运行时可通过反射解析。

2. 反射的作用:动态解析注解与方法信息

  • invoke方法中,通过method.isAnnotationPresent(Log.class)判断方法是否需要日志增强。
  • 通过method.getAnnotation(Log.class)获取注解属性,动态调整日志行为(如是否记录参数)。
  • 通过method.getName()method.getReturnType()获取方法元信息,为日志提供基础数据。

3. 动态代理的作用:无侵入式增强

  • 代理对象实现了与目标对象相同的接口(UserService),因此可以无缝替换原始对象,不影响调用方代码。
  • 日志逻辑被封装在LogInvocationHandler中,与业务逻辑完全分离(符合“单一职责原则”)。
  • 若需修改日志格式,只需调整代理处理器,无需改动所有业务方法(符合“开闭原则”)。

四、框架扩展思路

  1. 支持CGLIB代理:目前仅支持实现接口的类,可集成CGLIB扩展为无接口类提供日志增强。
  2. 日志输出适配:将日志输出到文件、ELK等,通过接口抽象日志输出器(如LogWriter)。
  3. 注解继承:通过@Inherited让子类继承父类的@Log注解。
  4. 性能优化:缓存注解解析结果(避免每次调用都反射解析),减少性能损耗。

总结

通过这个注解式日志框架的实战,我们清晰地看到:

  • 注解是“标记”,定义了增强的规则;
  • 反射是“解析器”,运行时读取规则并执行;
  • 动态代理是“增强器”,在不修改原始代码的情况下应用规则。

这三者的结合,正是Spring AOP、MyBatis等框架的核心实现思想。掌握这种“标记-解析-增强”的模式,就能真正理解Java高级技术的实战价值,从“会用API”提升到“设计框架”的层面。

Released under the MIT License.