抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

SpringAOP-概述

前言

​ 之前用十几篇文章介绍了 Spring IoC 的源码,作为与 IoC 齐名的 AOP 自然也不能错过。同样的,接下去将会通过几篇文章来解析 Spring AOP 的源码。

注:本文的内容以 AspectJ 来进行介绍。

关于 AOP

​ 百度百科:AOP 即 Aspect Oriented Programming,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP 是 OOP 的延续,是软件开发中的一个热点,也是 Spring 框架中的一个重要内容,是函数式编程的一种衍生范型。利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

例子

​ 我们有两个接口,一个用于进行加法计算,一个用于进行减法计算,为了避免计算出现问题,我们需要对每次接口调用的入参进行日志记录,于是我们有了以下的第一版实现。

​ 看起来还不错,简单明了。但是这个方案有个问题,就是后续每次新增一个接口,就需要拷贝一次 “记录入参” 的代码。对于一个 “懒人”,这是不可容忍的。好,提出一个公共方法,每个接口都来调用这个方法,于是我们有了以下第二版实现。这里有点切面的味道了。

​ 这个方案看起来更好了,但是同还是存在问题,虽然不用每次都拷贝代码了,但是,每个接口总得要调用这个方法吧,有办法让 “调用” 也省掉吗。我们设想一下,我们可以通过策略识别出所有要加入日志记录的接口,然后在接口调用时,将日志记录注入到接口调用的地方(切点),这就是 AOP 的核心思想。按这个思想,我们有了第三版的实现。

​ 这样接口只需要关心具体的业务,而不需要关注其他非该接口关注的逻辑或处理。 红框处,就是面向切面编程的思想。

AOP 的常见概念

Aspect

​ 切面是一个关注点的模块化,这个关注点可能是横切多个对象。在Spring AOP中切面是常规的类(基于xml配置)或者是带有@Aspect注解的类实现的。

Join point

​ 连接点是程序执行的一个方法或者一个可处理的异常。在Spring AOP中,连接点总是代表执行方法。

Advice

​ 通知是切面拦截到连接点之后所要采取的行动。通知类型分为 “环绕”, “前置” 和 “后置” 通知。Spring框架的一个通知作为一个拦截器,并且维护了一个拦截器链环绕连接点。

Pointcut

​ 指匹配连接点的断言。通知与一个切入点表达式关联,并在满足这个切入的连接点上运行,例如:当执行某个特定的名称的方法。连接点和切点表达式相匹配是Spring AOP的核心概念,Spring默认使用AspectJ切点表达式语言。

Introduction

​ 声明额外的方法或者某个类型的字段。Spring AOP允许你可以引入新的接口(以及相应的实现)到被通知的对象。

Target object

​ 目标对象是被一个或者多个切面所通知的对象。也称为“被通知对象”。因为Spring AOP实现通过使用运行时代理,这个对象总是一个代理对象。

AOP proxy

​ AOP代理是指AOP框架创建的对对象,用来实现切面约定(通知方法等功能)。在Spring中一个AOP代理是一个JDK动态代理或一个CGLIB代理。

Weaving

​ 指把切面连接到其他应用出程序类型或者对象上,并创建一个被通知的对象。这个过程可以在编译期(例如使用AspectJ 编译期)、加载期或者运行期完成。 Spring AOP像其他纯Java的AOP框架一样在运行时执行织入。

总结

  • “加法接口” 或 “减法接口” 每次被调用时所处的程序执行点都是一个 Jointpoint
  • Pointcut 就是用于指定 “加法接口” 和 “减法接口” 的一个 “表达式”,当然这个表达式还可以指定很多其他的接口,表达式常见的格式为:“execution(* com.joonwhee.open.demo.service...(..))”
  • Aspect 是定义 Advice、Pointcut 的地方
  • Advice 就是我们要在 “加法接口” 和 “减法接口” 织入的日志记录逻辑
  • Weaving 就是指将日记记录逻辑加到 “加法接口” 和 “减法接口” 的过程
  • Target 就是定义了 “加法接口” 和 “减法接口” 的对象实例

Advice通知

​ Spring AOP通过这个接口,为AOP切面增强的织入功能做了更多的细化和扩展,比如提供了更具体的通知类型,如BeforeAdvice、AfterAdvice、ThrowsAdvice等。作为SpringAOP定义的接口类,具体的切面增强可以通过这些接口集成到AOP框架中去发挥作用。

在BeforeAdvice的继承关系中,定义了为待增强的目标方法设置的前置增强接口MethodBeforeAdvice,使用这个前置接口需要实现一个回调函数。

1
2
3
public interface MethodBeforeAdvice extends BeforeAdvice {
void before(Method method, Object[] args, @Nullable Object target) throws Throwable;
}

作为回调函数,before方法的实现在Advice中被配置到目标方法后,会在调用目标方法时被回调。具体的调用参数有:Method对象,这个参数是目标方法的反射对象;Object[]对象数组,这个对象数组中包含目标方法的输人参数。

在Advice的实现体系中,Spnng还提供了AfterAdvice这种通知类型,可以看到AfterReturningAdvice对AfterAdvice接口的扩展。在AfterReturningAdvice接口中定义了接口方法,如下所示:

1
2
3
public interface AfterReturningAdvice extends AfterAdvice {
void afterReturning(@Nullable Object returnValue, Method method, Object[] args, @Nullable Object target) throws Throwable;
}

afterReturning方法也是一个回调函数,AOP应用需要在这个接口实现中提供切面增强的具体设计,在这个Advice通知被正确配置以后,在目标方法调用结束并成功返回的时候,接口会被springAOP回调。对于回调参数,有目标方法的返回结果、反射对象以及调用参数(AOP把这些参数都封装在一个对象数组中传递进来)等。与前面分析BeforeAdvice—样。

对于ThrowsAdvice,并没有指定需要实现的接口方法,它在抛出异常时被回调,这个回调是AOP使用反射机制来完成的。

Pointcut切点

​ Pointcut(切点)决定Advice通知应该作用于哪个连接点,也就是说通过Pointcut来定义需要增强的方法的集合,这些集合的选取可以按照一定的规则来完成。在这种情况下,Pointcut通常意味着标识方法,例如,这些需要增强的地方可以由某个正则表达式进行标识,或根据某个方法名进行匹配等。

1
2
3
4
5
public interface Pointcut {
ClassFilter getClassFilter();
MethodMatcher getMethodMatcher();
Pointcut TRUE = TruePointcut.INSTANCE;
}

为了方便用户使用,springAOP提供了具体的切点供用户使用,切点在Spring AOP中的类继承体系如图所示。

​ 在Pointcut的基本接口定义中可以看到,需要返回一个MethodMatcher。对于Point的匹配判断功能,具体是由这个返回的MethodMatcher来完成的,也就是说,由这个MethodMatcher来判断是否需要对当前方法调用进行增强,或者是否需要对当前调用方法应用配置好的Advice通知。在Pointcut的类继承关系中,以正则表达式切点JdkRegexpMethodPointcut的实现原理为例,来具体了解切点Pointcut的工作原理。JdkRegexpMethodPomtcut类完成通过正则表达式对方法名进行匹配的功能。在JdkRegexpMethodPointcut的基类StaticMethodMatcherPointcut的实现中可以看到,设置MethodMatcher为StaticMethodMatcher,同时JdkRegexpMethodPointcut也是这个MethodMatcher的子类,它的类层次关系如下图所示。

可以看到,在Pointcut中,通过这样的类继承关系,MethodMatcher对象实际上是可以被配置成JdkRegexpMethodPointcut来完成方法的匹配判断的。在JdkRegexpMethodPomtcut中,可以看到一个matches方法,这个matches方法是MethodMatcher定义的接口方法。在JdkRegexpMethodPointcut的实现中,这个matches方法就是使用正则表达式来对方法名进行匹配的地方。关于在AOP框架中对matches方法的调用,会在后面的springAOP实现中详细介绍,这里只是先简单提一下,会在JdkDynamicAopProxy.invoke()方法中触发matches()方法的调用,这个invoke()方法就是Proxy对象进行代理回调的入口方法,这个invoke回调的实现是使用JDK动态代理完成AOP功能的一部分。
在JdkRegexpMethodPointcut中,通过JDK来实现正则表达式的匹配。

1
2
3
4
5
@Override
protected boolean matches(String pattern, int patternIndex) {
Matcher matcher = this.compiledPatterns[patternIndex].matcher(pattern);
return matcher.matches();
}

在spnngAOP中,还提供了其他的MethodPointcut,比如通过方法名匹配进行Advice匹配的NameMatchMethodPointcut它的matches方法实现很简单,匹配的条件是方法名相同或者方法名相匹配,如下所示。

1
2
3
4
5
6
7
8
public boolean matches(Method method, Class<?> targetClass) {
for (String mappedName : this.mappedNames) {
if (mappedName.equals(method.getName()) || isMatch(method.getName(), mappedName)) {
return true;
}
}
return false;
}

Advisor通知器

​ 完成对目标方法的切面增强设计(Advice)和关注点的设计(Pointcut)以后,需要一个对象把它们结合起来,完成这个作用的就是Advisor(通知器)。通过Advisor,可以定义应该使用哪个通知并在哪个关注点使用它,也就是说通过Advisor,把Advice和Pointcut结合起来,这个结合为使用IoC容器配置AOP应用,或者说即开即用地使用AOP基础设施,提供了便利。在SpringAOP中,我们以一个Advisor的实现(DefaultPointcutAdvisor)为例,来了解Advisor的工作原理。

在DefaultPointcutAdvisor中,有两个属性,分别是advice和pointcut通过这两个属性,可以分别配置Advice和Pointcut,DefaultPointcutAdvisor的实现如下:

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 DefaultPointcutAdvisor extends AbstractGenericPointcutAdvisor implements Serializable {
private Pointcut pointcut = Pointcut.TRUE;
public DefaultPointcutAdvisor() {
}
public DefaultPointcutAdvisor(Advice advice) {
this(Pointcut.TRUE, advice);
}
public DefaultPointcutAdvisor(Pointcut pointcut, Advice advice) {
this.pointcut = pointcut;
setAdvice(advice);
}
public void setPointcut(@Nullable Pointcut pointcut) {
this.pointcut = (pointcut != null ? pointcut : Pointcut.TRUE);
}
@Override
public Pointcut getPointcut() {
return this.pointcut;
}
@Override
public String toString() {
return getClass().getName() + ": pointcut [" + getPointcut() + "]; advice [" + getAdvice() + "]";
}
}

在DefauItPointcutAdvisor中,pointcut默认被设置为Pointcut.True,TruePointcut的methodMatcher实现中,使用TrueMethodMatcher作为方法匹配器。这个方法匹配器对任何的方法匹配都要求返回true的结果,也就是说对任何方法名的匹配要求,它都会返回匹配成功的结果。

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
final class TruePointcut implements Pointcut, Serializable {
public static final TruePointcut INSTANCE = new TruePointcut();
private TruePointcut() {
}

@Override
public ClassFilter getClassFilter() {
return ClassFilter.TRUE;
}

@Override
public MethodMatcher getMethodMatcher() {
return MethodMatcher.TRUE;
}
private Object readResolve() {
return INSTANCE;
}

@Override
public String toString() {
return "Pointcut.TRUE";
}

}
final class TrueMethodMatcher implements MethodMatcher, Serializable {

public static final TrueMethodMatcher INSTANCE = new TrueMethodMatcher();
private TrueMethodMatcher() {
}
@Override
public boolean isRuntime() {
return false;
}
@Override
public boolean matches(Method method, Class<?> targetClass) {
return true;
}
@Override
public boolean matches(Method method, Class<?> targetClass, Object... args) {
// Should never be invoked as isRuntime returns false.
throw new UnsupportedOperationException();
}
@Override
public String toString() {
return "MethodMatcher.TRUE";
}
private Object readResolve() {
return INSTANCE;
}

}

实现机制

​ Spring AOP 底层实现机制目前有两种:JDK 动态代理、CGLIB 动态字节码生成。在阅读源码前对这两种机制的使用有个认识,有利于更好的理解源码。

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 MyInvocationHandler implements InvocationHandler {

private Object origin;

public MyInvocationHandler(Object origin) {
this.origin = origin;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("invoke start");
Object result = method.invoke(origin, args);
System.out.println("invoke end");
return result;
}
}


public class JdkProxyTest {
public static void main(String[] args) {
UserService proxy = (UserService) Proxy.newProxyInstance(JdkProxyTest.class.getClassLoader(),
new Class[]{UserService.class}, new MyInvocationHandler(new UserServiceImpl()));
proxy.doSomething();
}
}

CGLIB 代理

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

@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("intercept start");
Object result = proxy.invokeSuper(obj, args);
System.out.println("intercept end");
return result;
}
}

public class CglibProxyTest {

public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(CglibObject.class);
enhancer.setCallback(new CglibInterceptor());
CglibObject proxy = (CglibObject) enhancer.create();
proxy.doSomething();
}
}

简单实现

引入maven依赖

使用sprinBoot 开发

1
2
3
4
5
6
7
8
9
10

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.3.RELEASE</version>
</parent>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

创建接口及其实现类

1
2
3
4
5
6
7
8
9
public interface Person {
void say();
}
public class Student implements Person{

public void say(){
System.out.println("这是一个苦逼的程序员");
}
}

创建切面类

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
@Aspect
public class AspectJTest {

//切点表达式
@Pointcut("execution(* com.test.spring..say*(..))")
public void test(){}

//前置通知
@Before("test()")
public void before(){
System.out.println("before test..");
}

//后置通知
@After("test()")
public void after(){
System.out.println("after test..");
}

//环绕通知
@Around("test()")
public Object around(ProceedingJoinPoint p){
System.out.println("before1");
Object o = null;
try {
o = p.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
System.out.println("after1");
return o;
}
}

启动AOP方式

Spring配置文件

创建applicationContext.xml

使用<aop:aspectj-autoproxy/>来启动AOP

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!- 开启AOP的注解->
<aop:aspectj-autoproxy/>

<bean id="student" class="test.Student"/>
<bean class="test.AspectJTest"/>
</beans>
注解方式

使用@EnableAspectJAutoProxy注解来来启用AOP

主要能让该类被Spring扫描到即可

1
2
3
4
@Component
@EnableAspectJAutoProxy
public class EnabledAOP {
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringApplicationContextInitializer.class)
public class SpringTest {

@Autowired
private Person person;

@Test
public void test(){
person.say();
}
}

结果如下

1
2
3
4
5
before1
before test..
这是一个苦逼的程序员
after1
after test..

总结:AOP功能的使用还是比较简单的,把相关bean注入到Spring容器中,编写好相应的Aspect类即可

评论