面试官:抛开Spring来说,如何自己实现Spring AOP?-20231127

哈喽,大家好,我是了不起。

作为一名Java程序员,面向切面编程这种编程思想,应该是我们日常编码中常应用的编程思想。

这种编程范式,旨在提高代码的模块化程度。在AOP中,特定类型的问题被定义为“切面”,例如日志、事务管理或安全性等,这些切面可以在不改变核心业务逻辑的情况下,被插入程序的不同部分。对于提高代码的优雅,减少冗余度特别有用。

虽然Spring框架中的Spring AOP是Java社区中最著名的AOP实现,但为了完全理解这种思想,我们可以不依赖Spring来实现AOP功能。

1、AOP 核心概念

1.1 切面(Aspects)

切面是AOP的核心,它将横切关注点(如日志、事务处理等)与主业务逻辑分离。一个切面定义了何时(何处)和如何执行这些横切关注点。

1.2 连接点(Join Points)

连接点是应用执行过程中能够插入切面的点。在Java中,这通常是方法的调用。

1.3 通知(Advice)

通知定义了切面具体要执行的操作。主要类型包括前置通知(before)、后置通知(after)、环绕通知(around)、抛出异常时通知(after throwing)和返回时通知(after returning)。

1.4 切点(Pointcuts)

切点定义了在哪些连接点执行切面代码。它是一组表达式,用于匹配特定的连接点。

2、使用Java动态代理

Java动态代理是一种在运行时创建代理对象的方法,代理对象可以在调用实际对象的方法前后执行额外的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

// 简单的AOP实现
public class SimpleAOP {
    // 获取代理对象
    public static Object getProxy(Object target, Advice advice) {
        return Proxy.newProxyInstance(
            target.getClass().getClassLoader(),
            target.getClass().getInterfaces(),
            new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    advice.beforeMethod(method);
                    Object result = method.invoke(target, args);
                    advice.afterMethod(method);
                    return result;
                }
            }
        );
    }

    // 通知接口
    public interface Advice {
        void beforeMethod(Method method);
        void afterMethod(Method method);
    }
}

在上述代码中,getProxy 方法创建了一个代理对象,该对象在每次方法调用前后执行定义在 Advice 接口中的操作。

3、字节码操作

字节码操作是更高级但复杂的AOP实现方式。这涉及在类加载到JVM时修改其字节码,插入额外的代码。

3.1 使用ASM或ByteBuddy

  • ASM:一种低级字节码操作库,提供了对字节码的细粒度控制。
  • ByteBuddy:相比ASM,ByteBuddy提供了更简洁的API,适合那些不需要深入字节码细节的场景。

下面我以 ByteBuddy 为例,展示一下如何使用ByteBuddy来实现一个基本的AOP功能:在方法执行前后添加日志。

①、添加ByteBuddy依赖到你的项目中。如果你使用Maven,可以在pom.xml文件中加入以下依赖:

1
2
3
4
5
<dependency>
    <groupId>net.bytebuddy</groupId>
    <artifactId>byte-buddy</artifactId>
    <version>1.11.22</version>
</dependency>

②、使用ByteBuddy来创建一个代理类,这个类在方法执行前后打印日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.implementation.FixedValue;
import net.bytebuddy.matcher.ElementMatchers;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;

import java.lang.reflect.Modifier;

public class AOPExample {

    public static void main(String[] args) throws Exception {
        DynamicType.Unloaded<Object> dynamicType = new ByteBuddy()
            .subclass(Object.class)
            .method(ElementMatchers.named("toString"))
            .intercept(MethodDelegation.to(LoggerInterceptor.class))
            .make();

        Class<?> dynamicTypeLoaded = dynamicType
            .load(AOPExample.class.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
            .getLoaded();

        Object dynamicObject = dynamicTypeLoaded.newInstance();
        System.out.println(dynamicObject.toString());
    }

    public static class LoggerInterceptor {
        public static String intercept() {
            System.out.println("Method intercepted before execution");
            String result = "Hello from intercepted method";
            System.out.println("Method intercepted after execution");
            return result;
        }
    }
}

在上述代码中,我们创建了一个代理类,它覆盖了toString方法。方法被调用时,我们的LoggerInterceptor类将被调用。在LoggerInterceptor类中,我们在方法执行前后添加了日志。

Java Geek Tech wechat
欢迎订阅 Java 技术指北,这里分享关于 Java 的一切。