面向切面编程(AOP)可以实现横切关注点与它们所影响对象之间的解耦,对业务逻辑没有任何侵入
使用场景有很多,如:日志、异常、声明式事物、安全、缓存等,包括但不限于这些场景
常见的AOP实现有Spring-AOP
和AspectJ
,Spring-AOP
基于动态代理实现,主要总结一下Spring-AOP
概念
切面(Aspect)
切面是通知和切点的结合,通知和切点共同定义了切面的全面内容————是什么,在何时和何处完成其功能
通知(Advice)
切面的具体功能被称为通知,同时通知不仅定义了切面功能,还定义了切面何时使用,而根据使用时机可以分为5种类型的通知:
- 前置通知(Before):在目标方法被调用之前调用通知功能
- 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么
- 返回通知(After-returning):在目标方法成功执行之后调用通知
- 异常通知(After-throwing):在目标方法抛出异常后调用通知
- 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。环绕通知是最强大的通知类型
在
Spring-AOP
中的环绕通知必须有ProceedingJoinPoint
这个对象参数,通过它的proceed()
方法来调用被通知的方法
切点(Poincut)
通知定义了切面的 “什么” 和 “何时” ,切点则是定义了切面的 ”何处“
切点定义了通知被应用的具体位置(在哪些连接点)
连接点(Join point)
连接点是在应用执行过程中能够应用通知的 ”所有点“
连接点可以是调用方法时、抛出异常时、甚至修改一个字段时,但因为Spring-AOP
基于动态代理,所以Spring
只支持方法的连接点,而AspectJ
和JBoss
的AOP框架还提供了字段和构造器接入点。
但是方法拦截则满足了大部分的需求,如果需要方法拦截之外的连接点可以利用AspectJ
来补充Spring-AOP
的功能
引入(Introduction)
添加方法或字段到被通知的类
织入(Weaving)
织入是把切面应用到目标对象并创建新的代理对象的过程
织入可以在对象生命周期的多个点织入:
- 编译期:切面在目标类编译期被织入。这种需要特殊的编译器。
AspectJ
的织入编译器就是以这种方式织入 - 类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器,可以在目标类被引入应用之前增强该目标类的字节码。
AspectJ 5
的加载时织入就支持这种方式织入 - 运行期:切面在应用运行的某个时刻被织入。在织入时,AOP容器会为目标对象动态地创建一个代理对象。**
Spring-AOP
就是以这种方式织入切面的**
定义切点
切点用来定位使用通知的地方,在
Spring-AOP
中使用的是AspectJ
的切点表达式语言来定义切点,但是Spring
仅支持AspectJ
切点指示器的一个子集
切点表达式
切点指示器
Spring-AOP
所支持的AspectJ
的切点指示器
AspectJ 指示器 | 描述 |
---|---|
arg() | 限制连接点匹配参数为指定类型的执行方法 |
@args() | 限制连接点匹配参数由指定注解标注的执行方法 |
execution() | 用于匹配是连接点的执行方法 |
this() | 限制连接点匹配AOP代理的bean引用为指定类型的类 |
target | 限制连接点匹配目标对象为指定类型的类 |
@target() | 限制连接点匹配特定的执行对象,这些对象对应的类要具有指定类 型的注解 |
within() | 限制连接点匹配指定的类型 |
@within() | 限制连接点匹配指定注解所标注的类型(当使用Spring AOP时,方 法定义在由指定的注解所标注的类里) |
@annotation | 限定匹配带有指定注解的连接点 |
只有execution
指示器是实际执行匹配的,而其他的指示器都是用来限制匹配的
操作符
支持and
、or
、not
关系来连接指示器
在POJO中使用&&
、||
、!
分别代表and
、or
、not
关系
在XML中直接使用and
、or
、not
来连接指示器
bean指示器
除去AspectJ
指示器外,Spring
还引入了一个新的bean()
指示器,用来在切点表达式中使用bean的ID来标识bean。bean()
使用bean Id
或bean名称
作为参数来限制切点只匹配特定的bean
注解创建切面
AspectJ 5
支持使用注解来创建切面,使用少量的注解就可以把任意类转变为切面
Spring
同时支持AspectJ
注解驱动的切面
AOP配置注解
@Aspect
表明该类不仅仅是个POJO,还是一个切面
通知注解
通知注解对应五种通知类型,来声明通知方法
注解 | 通知 |
---|---|
@After | 通知方法会在目标方法返回或抛出异常后调用 |
@AfterReturning | 通知方法会在目标方法返回后调用 |
@AfterThrowing | 通知方法会在目标方法抛出异常后调用 |
@Around | 通知方法会将目标方法封装起来 |
@Before | 通知方法会在目标方法调用之前执行 |
@Pointcut
@Pointcut
注解可以在一个@AspectJ
切面内定义可重用的切点
E.g.
1 |
|
通过在 performance()
方法上添加 @Pointcut
注解,这样就可以在任何的切点表达式中使用 performance()
了
performance()
方法的实际内容并不重要,在这里它实际上应该是空的。其实该方法本身只是一个标识,供 @Pointcut
注解依附。
通过注解引入新功能
类似
Groovy
不直接修改对象或类的定义就能为对象或类增加新的方法,虽然Java不是动态语言,但是通过AOP
引用新的接口则可以实现其功能
但是当引入接口的方法被调用时,代理会把此调用委托给实现了新接口的某个其他对象。实际上,一个
bean
的实现被拆分到了多个类中
通过@DeclareParents
注解可以将新的接口引入到bean
中
定义新的接口:
1 | package concert; |
创建切面引入接口:
1 | package concert; |
@DeclareParents
注解由三部分组成:
value
属性指定了哪种类型的bean
要引入该接口。在本例中,也就是所有实现Performance
的类型。(标记符后面的加号表示是Performance
的所有子类型,而不是 Performance 本身。)defaultImpl
属性指定了为引入功能提供实现的类。在这里,我们指定的是DefaultEncoreable
提供实现。@DeclareParents
注解所标注的静态属性指明了要引入了接口。在这里,我们所引入的是 Encoreable 接口。
XML中声明切面
AOP配置元素
在 Spring 的 aop 命名空间中,提供了多个元素用来在 XML 中声明切面
AOP 配置元素 | 用途 |
---|---|
<aop:advisor> | 定义 AOP 通知器 |
<aop:after> | 定义 AOP 后置通知(不管被通知的方法是否执行成功) |
<aop:after-returning> | 定义 AOP 返回通知 |
<aop:after-throwing> | 定义 AOP 异常通知 |
<aop:around> | 定义 AOP 环绕通知 |
<aop:aspect> | 定义一个切面 |
<aop:aspectj-autoproxy> | 启用 @AspectJ 注解驱动的切面 |
<aop:before> | 定义一个 AOP 前置通知 |
<aop:config> | 顶层的 AOP 配置元素。大多数的元素必须包含在元素内 |
<aop:declare-parents> | 以透明的方式为被通知的对象引入额外的接口 |
<aop:pointcut> | 定义一个切点 |
E.g.
1 | <aop:config> |
通过切面引入新功能
使用 @DeclareParents
注解可以为被通知的方法引入新的方法,使用 Spring aop
命名空间中的 <aop:declare-parents>
元素,可以实现相同的功能
1 | <aop:aspect> |
types-matching
类型匹配,匹配为哪些bean
引入接口implement-interface
指定新加的接口default-impl
用全限定类名来显式指定新加接口的实现delegate-ref
还支持引用了一个Spring bean
作为引入接口的实现
启用AspectJ自动代理
无论是注解创建、还是XML声明切面,都需要启用自动代理,来创建切面的代理,否则切面不会生效
启用自动代理有两种方式:
在
JavaConfig
中启用配置类的类级别上通过使用
@EnableAspectJAutoProxy
注解启用自动代理功能在
XML
中启用使用 Spring aop 命名空间中的
<aop:aspectj-autoproxy/>
元素
注入AspectJ切面
虽然
Spring AOP
能够满足许多应用的切面需求,但是与AspectJ
相比,Spring AOP
是一个功能比较弱的AOP
解决方案。AspectJ
提供了Spring AOP
所不能支持的许多类型的切点
AspectJ
可以织入到任意的Java应用程序中,而我们可以借助 Spring
的依赖注入把 bean
装配进 AspectJ
切面中,这样更为方便
用AspectJ实现切面
创建 AspectJ
的切面需要使用扩展的Java语言
1 | package com.aspect.test; |
使用aspectOf()方法注入
Spring bean
由 Spring
容器初始化,但是 AspectJ
切面是由 AspectJ
在运行期创建的。等到 Spring
有机会为 AspectJ
切面注入 bean
时,切面已经被实例化了。
所有的 AspectJ
切面都提供了一个静态的 aspectOf()
方法,该方法返回切面的一个单例。所以为了获得切面的实例,我们必须使用 factory-method
来调用 asepctOf()
方法
1 | <bean class="com.aspect.test.AspectInject" factory-method="aspectOf"/> |
参考文献
- 《Spring 实战(第 4 版)》