再说 Spring AOP

什么是 AOP

AOP(Aspect-OrientedProgramming,面向方面编程),可以说是 OOP(Object-Oriented Programing,面向对象编程)的补充和完善。OOP 引入封装、继承和多态性等概念来建立一种对象层次结构,用以模拟公共行为的一个集合。当我们需要为分散的对象引入公共行为的时候,OOP 则显得无能为力。也就是说,OOP 允许你定义从上到下的关系,但并不适合定义从左到右的关系。例如日志功能。日志代码往往水平地散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系。对于其他类型的代码,如安全性、异常处理和透明的持续性也是如此。这种散布在各处的无关的代码被称为横切(cross-cutting)代码,在 OOP 设计中,它导致了大量代码的重复,而不利于各个模块的重用。

AOP 实现的关键在于 AOP 框架自动创建的 AOP 代理,AOP 代理主要分为静态代理和动态代理,静态代理的代表为 AspectJ;而动态代理则以 Spring AOP 为代表。静态代理是编译期实现,动态代理是运行期实现,可想而知前者拥有更好的性能。

静态代理是编译阶段生成 AOP 代理类,也就是说生成的字节码就织入了增强后的 AOP 对象;动态代理则不会修改字节码,而是在内存中临时生成一个 AOP 对象,这个 AOP 对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。

Spring AOP 中的动态代理主要有两种方式,JDK 动态代理和 CGLIB 动态代理。JDK 动态代理通过反射来接收被代理的类,并且要求被代理的类必须实现一个接口。JDK 动态代理的核心是 InvocationHandler 接口和 Proxy 类。

如果目标类没有实现接口,那么 Spring AOP 会选择使用 CGLIB 来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成某个类的子类,注意,CGLIB 是通过继承的方式做的动态代理,因此如果某个类被标记为 final,那么它是无法使用 CGLIB 做动态代理的,诸如 private 的方法也是不可以作为切面的。

我们分别通过实例来研究 AOP 的具体实现。

直接使用 Spring AOP

首先需要引入相关依赖,我这里是用了 SpringBoot 的相关 starter

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

然后定义需要切入的接口和实现。为了简单起见,定义一个接口Speakable和一个具体的实现类PersonSpring,只有两个方法sayHi()sayBye()

1
2
3
4
public interface Speakable {
void sayHi();
void sayBye();
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Service
public class PersonSpring implements Speakable {

@Override
public void sayHi() {
try {
Thread.currentThread().sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Hi!!");
}

@Override
public void sayBye() {
try {
Thread.currentThread().sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Bye!!");
}

}

现在我们需要实现一个功能,记录sayHi()sayBye()的执行时间。
我们定义了一个MethodMonitor类用来记录 Method 执行时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MethodMonitor {

private long start;
private String methodName;

public MethodMonitor(String methodName) {
this.methodName = methodName;
this.start = System.currentTimeMillis();
System.out.println(this.methodName + "monitor begin...");
}

public void log() {
long elapsedTime = System.currentTimeMillis() - start;
System.out.println("Method:" + this.methodName + ", elapsedTime:" + elapsedTime + "millis");
System.out.println(this.methodName + "monitor end.");
}

}

光有这个类还是不够的,希望有个静态方法用起来更顺手,像这样

1
2
3
MonitorSession.start(methodName);
doWork();
MonitorSession.end();

说干就干,定义一个MonitorSession

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MonitorSession {

private static ThreadLocal<MethodMonitor> threadLocal = new ThreadLocal<>();

public static void start(String methodName) {
threadLocal.set(new MethodMonitor(methodName));
}

public static void end() {
threadLocal.get().log();
}

}

准备工作都做完了, 接下来只需要我们做好切面的编码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Aspect
@Component
public class MonitorAdvice {

@Pointcut("execution (* com.windmt.springaop.service.Speakable.*(..))")
public void pointcut() {

}

@Around("pointcut()")
public void around(ProceedingJoinPoint pjp) throws Throwable {
MonitorSession.start(pjp.getSignature().getName());
pjp.proceed();
MonitorSession.end();
}

}

如何使用?写一个启动函数吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@SpringBootApplication
public class SpringAopApplication {

@Autowired
private Speakable personSpring;

public static void main(String[] args) {
SpringApplication.run(SpringAopApplication.class, args);
}

@Bean
public CommandLineRunner commandLineRunner(ApplicationContext ctx) {
return args -> {
System.out.println("********** spring aop **********");
personSpring.sayHi();
personSpring.sayBye();
System.exit(0);
};
}

}

运行后输出:

1
2
3
4
5
6
7
8
9
********** spring aop **********
sayHi monitor begin...
Hi!!
Method:sayHi, elapsedTime:48 millis
sayHi monitor end.
sayBye monitor begin...
Bye!!
Method:sayBye, elapsedTime:34 millis
sayBye monitor end.

JDK 动态代理

刚刚的例子其实内部实现机制就是 JDK 动态代理,因为PersonSpring实现了一个接口。
为了不和第一个例子冲突,我们再定义一个PersonIndie来实现Speakable,实现和之前的完全一样,只是注意这个实现是不带 Spring Annotation 的,所以他不会被 Spring 托管。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class PersonIndie implements Speakable {

@Override
public void sayHi() {
try {
Thread.currentThread().sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Hi!!");
}

@Override
public void sayBye() {
try {
Thread.currentThread().sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Bye!!");
}

}

重头戏来了,我们需要利用InvocationHandler实现一个代理,让它去包含Person这个对象。那么在运行时实际上执行的是这个代理的方法,然后代理再去执行真正的方法。所以我们得以在执行真正方法的前后做一些手脚。JDK 动态代理是利用反射实现,直接看代码。

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
public class DynamicProxy implements InvocationHandler {

private Object target;

public DynamicProxy(Object obj) {
this.target = obj;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
MonitorSession.start(method.getName());
Object obj = method.invoke(this.target, args);
MonitorSession.end();
return obj;
}


public <T> T getProxy() {
return (T) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
this);
}

}

通过getProxy可以得到这个代理对象,invoke就是具体的执行方法,可以看到我们在执行每个真正的方法前后都加了 Monitor。

再来一个工厂类来获取 Person 代理对象

1
2
3
4
5
6
7
8
public class PersonProxyFactory {

public static Speakable newJdkProxy() {
DynamicProxy proxy = new DynamicProxy(new PersonIndie());
return proxy.getProxy();
}

}

具体使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@SpringBootApplication
public class SpringAopApplication {

public static void main(String[] args) {
SpringApplication.run(SpringAopApplication.class, args);
}

@Bean
public CommandLineRunner commandLineRunner(ApplicationContext ctx) {
return args -> {
System.out.println("********** spring jdk proxy **********");

Speakable person = PersonProxyFactory.newJdkProxy();
person.sayHi();
person.sayBye();

System.exit(0);
};
}

}

运行并输出:

1
2
3
4
5
6
7
8
9
********** spring jdk proxy **********
sayHi monitor begin...
Hi!!
Method:sayHi, elapsedTime:35 millis
sayHi monitor end.
sayBye monitor begin...
Bye!!
Method:sayBye, elapsedTime:32 millis
sayBye monitor end.

CGLib 动态代理

我们再新建一个Person来,这次不实现任何接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Person {

public void sayHi() {
try {
Thread.currentThread().sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Hi!!");
}

public void sayBye() {
try {
Thread.currentThread().sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Bye!!");
}

}

如果 Spring 识别到所代理的类没有实现 Interface,那么就会使用 CGLib 来创建动态代理,原理实际上成为所代理类的子类。

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
public class CGLibProxy implements MethodInterceptor {

private static volatile CGLibProxy instance;
private CGLibProxy() {}
public static CGLibProxy getInstance() {
if (instance == null) {
synchronized (CGLibProxy.class) {
if (instance == null) {
instance = new CGLibProxy();
}
}
}
return instance;
}

@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
MonitorSession.start(method.getName());
Object obj = methodProxy.invokeSuper(o, objects);
MonitorSession.end();
return obj;
}

private Enhancer enhancer = new Enhancer();

public <T> T getProxy(Class<T> clazz) {
enhancer.setSuperclass(clazz);
enhancer.setCallback(this);
return (T) enhancer.create();
}

}

类似的通过getProxy可以得到这个代理对象,intercep就是具体的执行方法,可以看到我们在执行每个真正的方法前后都加了 Monitor。
在工厂类中增加获得 Person 代理类的方法

1
2
3
public static Person newCGLibProxy() {
return CGLibProxy.getInstance().getProxy(Person.class);
}

具体使用

1
2
3
Person person = PersonProxyFactory.newCGLibProxy();
person.sayHi();
person.sayBye();

输出结果

1
2
3
4
5
6
7
8
9
********** CGLib proxy **********
sayHi monitor begin...
Hi!!
Method:sayHi, elapsedTime:38 millis
sayHi monitor end.
sayBye monitor begin...
Bye!!
Method:sayBye, elapsedTime:35 millis
sayBye monitor end.

以上 code 都可以通过 Github 中获取。