Spring中的AOP

想了想还是单独来总结一篇吧,关于 AOP 内容不少,分散在两篇文章中也不好看,在 Spring 的两大核心 IoC 和 AOP 中,普遍认为 AOP 是比较难的,因为它的概念比较多吧,并且还都非常抽象,所以单独来一篇。
虽然会和以前写的有重复,但也不删了,重就重吧;这篇大多都是说的理论,实践的话可暂时参考 [4] 或者 [0] 中的连接

什么是 AOP

AOP (Aspect-Oriented Programming),即 面向切面编程, 它与 OOP( Object-Oriented Programming, 面向对象编程) 相辅相成, 提供了与 OOP 不同的抽象软件结构的视角.
在 OOP 中, 我们以类(class)作为我们的基本单元, 而 AOP 中的基本单元是 Aspect(切面) ,什么是切面下面会说。

关于AspectJ

AspectJ 实际上是对 AOP 编程思想的一个实践,当然,除了 AspectJ 以外,还有很多其它的 AOP 实现,例如 ASMDex,但目前最好、最方便的,依然是 AspectJ。也许是因为最早做 Java 的 AOP 实现的原因。
AspectJ 可以干净地模块化横切关注点,基本上可以实现无侵入,同时学习成本低,功能强大(甚至可以通过修改字节码来实现多继承),可扩展性高。

AspectJ 意思就是 Java 的 Aspect,Java 的 AOP。
它其实不是一个新的语言,它就是一个代码编译器(也就是 AJC ),在 Java 编译器的基础上增加了一些它自己的关键字识别和编译方法。
因此,ajc 也可以编译 Java 代码。它在编译期将开发者编写的 Aspect 程序编织到目标程序中,对目标程序作了重构,目的就是建立目标程序与 Aspect 程序的连接(耦合,获得对方的引用(默认情况下,也就是不使用 this 或 target 来约束切点的情况下,那么获得的是声明类型,不是运行时类型)和上下文信息),从而达到 AOP 的目的(这里在编译期还是修改了原来程序的代码,但是是 AJC 替我们做的)。

AspectJ 是一套独立的面向切面解决方案,你可以在 Eclipse 的官网找到它,然后下载安装,然后使用它的 ajc.exe 进行编译,入门请参考:http://zhoujingxian.iteye.com/blog/667214
使用 AspectJ 有两种方法:

  • 完全使用 AspectJ 的语言。
    这语言一点也不难,和 Java 几乎一样,也能在 AspectJ 中调用 Java 的任何类库。AspectJ 只是多了一些关键词罢了。
  • 使用纯 Java 语言开发,然后使用AspectJ注解,也就是 @AspectJ。

当然不论哪种方法,最后都需要 AspectJ 的编译工具 AJC 来编译。
关于 AspectJ 就不多说了,想详细了解的见参考 [1] ;另外 Android 中也有很多人开始使用 AspectJ 了!

SpringAOP与AspectJ

首先,Spring 采用的就是 AspectJ,在 Spring 的 AOP 相关包里你能看到 AspectJ 的影子;
Spring AOP 采用的动态织入,而 AspectJ 是静态织入
静态织入:指在编译时期就织入,即:编译出来的class文件,字节码就已经被织入了。
动态织入又再分静动两种,静则指织入过程只在第一次调用时执行;动则指根据代码动态运行的中间状态来决定如何操作,每次调用 Target Object 的时候都执行。

使用 Spring 自己原生的 AOP,你需要实现大量的接口,继承大量的类,所以Spring AOP一度被人所诟病,这与它的无侵入,低耦合完全冲突,我在笔记一中采用的就是这种方式,确实麻烦。
不过 Spring 对开源的优秀框架,组件向来是采用兼容,并入的态度。所以,后来的 Spring 就提供了 Aspectj 支持,也就是我们后来所说的基于纯 POJO 的 AOP。

AOP中的术语

通知/增强(Advice)

切面的工作被称为通知,通知定义了切面是什么以及何时使用。
除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。它应该应用在某个方法被调用之前?之后?之前和之后都调用?还是只是在方法抛出异常时调用?
简单说就是你想要(切入)的具体功能实现(想要干啥),比如安全,事物,日志操作等。

许多 AOP框架, 包括 Spring AOP, 会将 advice 模拟为一个拦截器(interceptor), 并且在 join point 上维护多个 advice, 进行层层拦截.
例如 HTTP 鉴权的实现, 我们可以为每个使用 RequestMapping 标注的方法织入 advice, 当 HTTP 请求到来时, 首先进入到 advice 代码中, 在这里我们可以分析这个 HTTP 请求是否有相应的权限, 如果有, 则执行 Controller, 如果没有, 则抛出异常. 这里的 advice 就扮演着鉴权拦截器的角色了.

连接点(JoinPoint)

连接点是在应用执行过程中能够插入切面的一个点。这个点可以是调用方法、抛出异常时、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。
就是 Spring 允许你使用通知的地方

另一种说法:程序运行中的一些时间点, 例如一个方法的执行, 或者是一个异常的处理;在 Spring AOP 中, join point 总是方法的执行点, 即只有方法连接点.
Spring 中是方法、方法、方法,重要的事情说三遍!

切点(Pointcut)

一组连接点的总称,用于指定某个通知(增强)应该在何时被调用。
切入点是「在哪干」,你可能在很多地方(连接点)都可以干,但并不是每个地方都要干,要干的地方叫切点

在 Spring 中, 所有的方法都可以认为是 joinpoint, 但是我们并不希望在所有的方法上都添加 Advice, 而 pointcut 的作用就是提供一组规则(使用 AspectJ pointcut expression language 来描述) 来匹配 joinpoint, 给满足规则的 joinpoint 添加 Advice

切面(Aspect)

切面是通知和切点的结合。通知和切点共同定义了切面的全部内容——它是什么,在何时和何处完成其功能
通知说明了干什么和什么时候干(什么时候通过方法名中的before、after、around 等就能知道),而切入点说明了在哪干(指定到底是哪个方法),这就是一个完整的切面定义。
Spring AOP 就是负责实施切面的框架, 它将切面所定义的横切逻辑织入到切面所指定的连接点中.
可以简单地认为, 使用 @Aspect 注解的类就是切面

joinPoint 和 pointCut 的区别

在 Spring AOP 中, 所有的方法执行都是 join point. 而 point cut 是一个描述信息, 它修饰的是 join point, 通过 point cut, 我们就可以确定哪些 join point 可以被织入 Advice. 因此 join point 和 point cut 本质上就是两个不同纬度上的东西.
advice 是在 join point 上执行的, 而 point cut 规定了哪些 join point 可以执行哪些 advice

目标对象(Target)

织入 advice 的目标对象. 目标对象也被称为 advised object.
因为 Spring AOP 使用运行时代理的方式来实现 aspect, 因此 adviced object 总是一个代理对象(proxied object)
注意,adviced object 指的不是原来的类, 而是织入 advice 后所产生的代理类.

织入(weaving)

把切面应用到目标对象来创建新的代理对象的过程。专业点的说法是:将 aspect 和其他对象连接起来, 并创建 adviced object 的过程。
有3种方式,spring 采用的是运行时(上面提到过):

  • 编译器织入, 这要求有特殊的Java编译器.
  • 类装载期织入, 这需要有特殊的类装载器.
  • 动态代理织入, 在运行期为目标类添加增强(Advice)生成子类的方式.
    Spring 采用动态代理织入, 而AspectJ采用编译器织入和类装载期织入.

关注点(concern)

对软件工程有意义的小的、可管理的、可描述的软件组成部分,一个关注点通常只同一个特定概念或目标相关联。

通俗来说其实指的就是重复代码;在一些方法中,我们可能需要写许多重复代码,而真正的核心代码就只有几行。
比如 DAO 层中的 session 处理的代码,又是事务又是连接的,每个方法都需要写,这些代码就可以看作是关注点,我们要做的就是分离它,使用代理,可以在运行期间,执行核心业务代码的时候动态植入关注点代码
AOP 的目标就是让关注点代码和业务代码进行分离
不太严谨的讲,切面也可以理解为关注点形成的类,也就是说很多重复的代码就可以形成一个切面

通知(advice)类型

Spring 中的通知有五种类型:

  1. 前置通知(Before):
    在目标方法被调用之前调用通知功能;
    在 join point 前被执行的 advice. 虽然 before advice 是在 join point 前被执行, 但是它并不能够阻止 join point 的执行, 除非发生了异常(即我们在 before advice 代码中, 不能人为地决定是否继续执行 join point 中的代码)
  2. 后置通知(After):
    在目标方法完成之后调用通知;
    不管是否正常退出或者发生了异常都会执行
  3. 返回通知/最终通知(After-returning):
    在目标方法成功执行之后调用通知,如果有后置通知会在其之后执行;
    在方法正常返回后执行
  4. 异常通知(After-throwing):
    在目标方法抛出异常后调用通知;
  5. 环绕通知(Around):
    通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。
    在 join point 前和 joint point 退出后都执行的 advice. 这个是最常用的 advice.

运行顺序:前置通知/环绕通知目标方法执行返回通知/异常通知后置通知/环绕通知
这里需要注意的是:环绕通知由于和前置、后置处于同一个 aspect 内,所以是无法确定其执行顺序的,当然可以通过其他手段来解决
实际开发中,一般会将顺序执行的 Advice 封装到不同的 Aspect,然后通过注解或者实现接口的方式控制 Aspect 的执行顺序,二选一(对于在同一个切面定义的通知函数将会根据在类中的声明顺序执行)

关于 AOP Proxy

Spring AOP 默认使用标准的 JDK 动态代理(dynamic proxy)技术来实现 AOP 代理, 通过它, 我们可以为任意的接口实现代理.
如果需要为一个类(没有实现接口)实现代理, 那么可以使用 CGLIB 代理.
当一个业务逻辑对象没有实现接口时, 那么Spring AOP 就默认使用 CGLIB 来作为 AOP 代理了. 即如果我们需要为一个方法织入 advice, 但是这个方法不是一个接口所提供的方法, 则此时 Spring AOP 会使用 CGLIB 来实现动态代理. 鉴于此, Spring AOP 建议基于接口编程, 对接口进行 AOP 而不是类.

JDK 中的代理一般是 $ 开头,Cglib 一般就是代理对象名开头后面再加一些东西
这两种我以前都写过,关于 JDK 动态代理的介绍我放在参考[2];关于 Cglib 的介绍使用我放在参考[3]

@AspectJ 支持

@AspectJ 是一种使用 Java 注解来实现 AOP 的编码风格.
@AspectJ 风格的 AOP 是 AspectJ Project 在 AspectJ 5 中引入的, 并且 Spring 也支持@AspectJ 的 AOP 风格.

使能 @AspectJ 支持

@AspectJ 可以以 XML 的方式或以注解的方式来使用, 并且不论以哪种方式使能 @ASpectJ, 我们都必须保证 aspectjweaver.jar 在 classpath 中.
使用 Java Configuration 方式使能 @AspectJ,想深入了解的可见参考[5]:

1
2
3
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {}

@Configuration 注解简单说就是把一个类作为 IoC 容器(还可配合 @Bean 使用,写 Spring 注解的时候有解释);
使用 XML 方式使能 @AspectJ ,顺便记得加上命名空间:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">
<!-- 自动扫描方式加载对象 -->
<context:component-scan base-package="com.bfchengnuo"/>

<!-- 启动 @AspectJ 支持 -->
<aop:aspectj-autoproxy/>
</beans>

常用注解

定义 aspect(切面)

当使用注解 @Aspect 标注一个 Bean 后, 那么 Spring 框架会自动收集这些 Bean, 并添加到 Spring AOP 中, 例如:

1
2
3
@Component
@Aspect
public class MyTest {}

注意:仅仅使用 @Aspect 注解, 并不能将一个 Java 对象转换为 Bean,因此我们还需要使用类似 @Component 之类的注解.

如果一个 类被 @Aspect 标注, 则这个类就不能是其他 aspect 的 advised object 了, 因为使用 @Aspect 后, 这个类就会被排除在 auto-proxying 机制之外.

也就是说:Spring 将会把它当作一个特殊的 Bean(一个切面),也就是说不对这个类本身进行动态代理

声明 pointcut

一个 pointcut 的声明由两部分组成:

  • 一个方法签名, 包括方法名和相关参数
  • 一个 pointcut 表达式, 用来指定哪些方法执行是我们感兴趣的(即因此可以织入 advice).

在 @AspectJ 风格的 AOP 中, 我们使用一个方法来描述 pointcut, 一般用空方法即可,方法名随意,即:

1
2
@Pointcut("execution(* com.xys.service.UserService.*(..))") // 切点表达式
private void dataAccessOperation() {} // 切点前面

这个方法必须无返回值.
这个方法本身就是 pointcut signature;pointcut 表达式使用 @Pointcut 注解指定.
上面我们简单地定义了一个 pointcut, 这个 pointcut 所描述的是: 匹配所有在包 com.xys.service.UserService 下的所有方法的执行,第一个 * 指的是任何返回值,(..) 指的是此方法的任何参数,多个可以使用 || 分割,其他的应该就更好理解了;需要注意的是最后一定是定义到方法的!

声明 advice

advice 是和一个 pointcut 表达式关联在一起的, 并且会在匹配的 join point 的方法执行的前/后/周围 运行.
pointcut 表达式可以是简单的一个 pointcut 名字的引用, 或者是完整的 pointcut 表达式.
下面我们以几个简单的 advice 为例子, 来看一下一个 advice 是如何声明的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component
@Aspect
public class BeforeAspectTest {
// 定义一个 Pointcut, 使用 切点表达式函数 来描述对哪些 Join point 使用 advise.
@Pointcut("execution(* com.xys.service.UserService.*(..))")
public void dataAccessOperation() {
}
}
/*************************分割线*****************************/
@Component
@Aspect
public class AdviseDefine {
// 定义 advise
@Before("com.xys.aspect.PointcutDefine.dataAccessOperation()")
public void doBeforeAccessCheck(JoinPoint joinPoint) {
System.out.println("*****Before advise, method: " + joinPoint.getSignature().toShortString() + " *****");
}
}

可以看出 @Before 引用的是一个 pointcut 的名字,并且这个 pointcut 并不是和它在同一个包下的,如果在同一个类中那直接写名字就可以了,或者直接把表达式放在 @Before 中就行了

这里再着重说下 around advice,因为 around advice 比较特别, 它可以在一个方法的之前之前和之后添加不同的操作, 并且甚至可以决定何时, 如何, 是否调用匹配到的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Component
@Aspect
public class AdviseDefine {
// 定义 advise
@Around("com.xys.aspect.PointcutDefine.dataAccessOperation()")
public Object doAroundAccessCheck(ProceedingJoinPoint pjp) throws Throwable {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 开始
Object retVal = pjp.proceed();
stopWatch.stop();
// 结束
System.out.println("invoke method: " + pjp.getSignature().getName() + ", elapsed time: " + stopWatch.getTotalTimeMillis());
return retVal;
}
}

诺,就是这样,其他的通知就不写了,都是一样的,就是注解不一样而已

切点标志符(designator)

AspectJ5 的切点表达式由标志符(designator)操作参数组成. 如 "execution(* greetTo(..))" 的切点表达式, execution 就是标志符, 而圆括号里的 * greetTo(..) 就是操作参数
下面介绍几个标志符:

  • execution
    匹配 join point 的执行, 例如 "execution(* hello(..))" 表示匹配所有目标类中的 hello() 方法. 这个是最基本的 pointcut 标志符.
    不写包名默认就是本包内,必须要精确到方法

  • within
    匹配特定包下的所有 join point, 例如 within(com.xys.*) 表示 com.xys 包中的所有连接点, 即包中的所有类的所有方法. 而 within(com.xys.service.*Service) 表示在 com.xys.service 包中所有以 Service 结尾的类的所有的连接点.

  • this 与 target
    this 的作用是匹配一个 bean, 这个 bean(Spring AOP proxy) 是一个给定类型的实例(instance of).
    而 target 匹配的是一个目标对象(target object, 即需要织入 advice 的原始的类), 此对象是一个给定类型的实例(instance of).

  • bean
    匹配 bean 名字为指定值的 bean 下的所有方法

    1
    2
    bean(*Service) // 匹配名字后缀为 Service 的 bean 下的所有方法
    bean(myService) // 匹配名字为 myService 的 bean 下的所有方法
  • args
    匹配参数满足要求的的方法,一般和其他的标志符连用,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    @Pointcut("within(com.xys.demo2.*)")
    public void pointcut2() {
    }

    @Before(value = "pointcut2() && args(name)")
    public void doSomething(String name) {
    logger.info("---page: {}---", name);
    }
    /*********************分割线*************************/
    @Service
    public class NormalService {
    private Logger logger = LoggerFactory.getLogger(getClass());

    public void someMethod() {
    logger.info("---NormalService: someMethod invoked---");
    }

    public String test(String name) {
    logger.info("---NormalService: test invoked---");
    return "服务一切正常";
    }
    }

    NormalService.test 执行时, 则 advice doSomething 就会执行, test 方法的参数 name 就会传递到 doSomething 中,常用的一些栗子有:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 匹配只有一个参数 name 的方法
    @Before(value = "aspectMethod() && args(name)")
    public void doSomething(String name) {
    }

    // 匹配第一个参数为 name 的方法
    @Before(value = "aspectMethod() && args(name, ..)")
    public void doSomething(String name) {
    }

    // 匹配第二个参数为 name 的方法
    Before(value = "aspectMethod() && args(*, name, ..)")
    public void doSomething(String name) {
    }

    也就是说,这个标志符是非常重要的,经常能用到,因为使用方式非常的灵活。

  • @annotation
    匹配由指定注解所标注的方法 ,举个栗子:

    1
    2
    3
    // 匹配由注解 AuthChecker 所标注的方法
    @Pointcut("@annotation(com.xys.demo1.AuthChecker)")
    public void pointcut() {}

常用的标志符就是上面的这些了,这些基本能满足大部分的需求了,大概……..

常见的切点表达式

标志符知道了,表达式也不在话下,上面说过表达式是由标志符和操作参数组成,那么来看一下常用的表达式吧:

匹配方法签名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 匹配指定包中的所有的方法
execution(* com.xys.service.*(..))

// 匹配当前包中的指定类的所有方法
execution(* UserService.*(..))

// 匹配指定包中的所有 public 方法
execution(public * com.xys.service.*(..))

// 匹配指定包中的所有 public 方法, 并且返回值是 int 类型的方法
execution(public int com.xys.service.*(..))

// 匹配指定包中的所有 public 方法, 并且第一个参数是 String, 返回值是 int 类型的方法
execution(public int com.xys.service.*(String name, ..))

匹配类型签名:

1
2
3
4
5
6
7
8
9
10
11
// 匹配指定包中的所有的方法, 但不包括子包
within(com.xys.service.*)

// 匹配指定包中的所有的方法, 包括子包
within(com.xys.service..*)

// 匹配当前包中的指定类中的方法
within(UserService)

// 匹配一个接口的所有实现类中的实现的方法
within(UserDao+)

匹配 Bean 的名字:

1
2
// 匹配以指定名字结尾的 Bean 中的所有方法
bean(*Service)

常用的表达式组合:

1
2
3
4
5
// 匹配以 Service 或 ServiceImpl 结尾的 bean
bean(*Service || *ServiceImpl)

// 匹配名字以 Service 结尾, 并且在包 com.xys.service 中的 bean
bean(*Service) && within(com.xys.service.*)

以上,就是常用的了,应该能满足大部分的需求了吧….

使用 XML 定义

首先应当确保引入了命名空间,也就是 schema 的支持,先来看一段简单的栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<aop:config>  
<aop:aspect id="myAspect" ref="aBean">
<aop:pointcut id="businessService"
expression="execution(* com.xyz.myapp.service.*.*(..)) && this(service)"/>
<aop:before pointcut-ref="businessService" method="monitor"/>
...
</aop:aspect>
</aop:config>

<bean id="aBean" class="...">
...
</bean>

<aop:config>
<!-- 配置多个切点,&& || ! -->
<aop:pointcut id="pc" expression="execution(public * com.*.service.*.*(..)) || execution(public * com.*.*.service.*.*(..))" />
<aop:advisor pointcut-ref="pc" advice-ref="userTxAdvice" />
</aop:config>

看以看出,所有的相关配置都写在 aop:config 中,aop:aspect 是定义一个切面,aop:pointcut 是定义一个切点,指明一个表达式,然后就是通过 aop:before 等一系列标签来指定通知了
看过注解后再看 XML 大部分都是能直接看懂的。

补充一点,在 XML 中如果想定义多个切点,第一种可以在表达式使用 &&/and 或者 ||/or 连接,推荐使用小写字母;第二种可以定义多个 <aop:advisor pointcut="" />

关于 Hibernate 事务相关的 AOP 配置:点我跳转

参考&拓展

[0]:https://segmentfault.com/a/1190000007469968
[1]:http://linbinghe.com/2017/65db25bc.html
[2]:JDK动态代理
[3]:关于Cglib
[4]:https://jacksmiththu.github.io/2017/06/26/Spring%E4%B8%AD%E7%9A%84AOP/
[5]:http://blog.csdn.net/ethanwhite/article/details/52050351

喜欢就请我吃包辣条吧!

评论框加载失败,无法访问 Disqus

你可能需要魔法上网~~