面向切面的Spring
AOP
术语
通知(Advice
):简单理解为是一段增强代码,由切面添加到特定连接点的功能代码
Spring
有5种类型的通知Before
--在方法调用之前调用通知After
--在方法完成后调用通知,无论方法是否执行成功After-returning
--在方法成功执行后调用通知After-throwing
--在抛出异常后调用通知Around
--通知包裹了被通知的方法,在被通知方法调用前后调用后执行自定义的行为
连接点(Joinpoint
)
- 需要被增强方法的执行点
切点(Pointcut
)
- 用
AspectJ
来描述规则,这个规则用来匹配连接点(Joinpoint
),给满足规则的连接点增加增强代码
连接点(Joinpoint
)和切点(Pointcut
)关系
- 在
Spring AOP
中所有的方法执行都是连接点,而切入点是一个描述信息用于修饰连接点,通过切入点的描述我们可以确定哪些连接点可以织入增强代码。增强代码在连接点执行,而切点规定了哪些连接点可以执行哪些增强代码。
织入
- 将切面应用到目标对象来创建新代理对象的过程
- 织入的时机
- 编译期:切面在目标类编译时被织入
- 类加载期:切面在目标类加载到JVM时被织入。
- 运行期:切面在应用运到某个时刻被织入。
Spring对AOP的支持
常见的AOP框架
AspectJ
JBoss AOP
Srping AOP
Spring
提供了4种AOP
支持
- 基于代理的经典
AOP
@AspectJ
注解驱动的切面- 纯
POJO
切面 - 注入式
AspectJ
切面
- 前三种都是
Spring
基于代理的AOP
变体,因此Spring
对AOP
的支持局限于方法的拦截,如果需要对构造器和属性的拦截,那么应该考虑在AspectJ
中实现切面。
Spring
代理的几个特点
Spring
通知是Java
编写的,AspectJ
与之相反。Spring
在运行期间通知对象,Spring
在运行期间将切面织入到Spring
管理的Bean
中,因为运行时才床架代理对象,所以我们不需要特殊的编译器来织入AOP
的切面。Spring
只支持方法连接点,因为Spring
是基于动态代理。AspectJ
、Jboos
等AOP
框架还支持更细粒度的拦截,比如:字段和构造器的拦截。如果方法拦截不能满足我们的要求时可以通过AspectJ
配合来满足我们的要求。
使用切点选择连接点
- 切点用于准确定位该在什么地方应用切面通知。切点和通知都是切面的最基本元素。在
Spring AOP
中需要使用AspectJ
切点表达式来定义切点。 Spring AOP
所支持的AspectJ
切点指示器:arg()
:限制连接点匹配参数为指定类型的执行方法@args()
:限制连接点匹配参数由指定注解标注的执行方法execution()
:用于匹配是连接点的执行方法this()
:限制连接点匹配AOP代理的Bean引用为指定类型的类target()
:限制连接点匹配的对象为指定类型的类@target()
:限制连接点匹配特定的执行对象,这些对象对应了类要具备指定类型的注解within()
:限制连接点匹配指定的类型@within()
:限制连接点匹配指定注解所标注的类型(当使用Spring AOP时,方法定义在有指定注解所标注的类里)@annotation
:限制匹配带有指定注解连接点
- 如果使用
AspectJ
除上面以外的指示器将会抛出异常 - 只有
execution()
指示器是唯一的执行匹配,其他指示器都是用于限制匹配的,这说明execution()
是我们在编写切点定义时最主要的指示器,在此基础上其他指示器用来限制所匹配的切点。
编写切点
-
例如:假设在执行
play()
方法前执行通知execution(* com.zealzhangz.common.play(..))
- 上面的*表示任意返回类型
- ..表示任意参数
-
使用
within
来限制匹配execution(* com.zealzhangz.common.play(..)) and within(com.zealzhangz.common.*)
- 此时限制必须在
com.zealzhangz.commo
包中。 - 类似的可以使用
or
、not
- 此时限制必须在
Spring的Bean()指示器
- 除了上面说的指示器,Spring2.5还引入了一个新的Bean指示器,该指示器允许我们使用Bean的ID来标识bean,例如:
execution(* com.zealzhangz.common.play(..)) and bean(piano)
- 或者排除指定名称的bean
execution(* com.zealzhangz.common.play(..)) and !bean(piano)
在XML中声明切面
- AOP配置元素
- aop:advisor:定义
AOP
通知器。 - aop:after:定义
AOP
后置通知(不管被通知的方法是否执行成功)。 - aop:after-returning:定义
AOP after-returning
(在方法成功执行后调用通知)。 - aop:throwing:定义
AOP after-throwing
(在抛出异常后调用通知)。 - aop:around:定义环绕通知。
- aop:aspect:定义切面。
- aop:aspecj:autoproxy:启用注解
@AspectJ
驱动的切面。 - aop:before:定义
AOP
前置通知。 - aop:config:顶层
AOP
配置元素,大多数<aop:*>
必须包含在aop:config中。 - aop:declare-parents:为被通知的对象引入而外的接口,并透明的实现。
- aop:pointcut:定义切点。
- aop:advisor:定义
声明前置和后置通知
- 把
audience
(观众变成一个切面)
<aop:config>
<aop:aspect ref="audience">
<!--前置通知,表演之前观众就坐-->
<aop:before method="takeSeats" pointcut="execution(* com.zealzhangz.common.Instrument.play(..))"/>
<!--前置通知,表演之前观众关闭手机-->
<aop:before method="takeOffCellphones" pointcut="execution(* com.zealzhangz.common.Instrument.play(..))"/>
<!--成功表演观众鼓掌-->
<aop:after-returning method="applaud" pointcut="execution(* com.zealzhangz.common.Instrument.play())"/>
<!--表演失败,观众要求退钱-->
<aop:after-throwing method="demandRefund" pointcut="execution(* com.zealzhangz.common.Instrument.play()))"/>
</aop:aspect>
</aop:config>
- 观察上面的切面,发现相同的切点我们重复定义了4次,这违反了
DRY
原则,为了避免上述重复代码我们可以简化如下
<aop:config>
<aop:aspect ref="audience">
<aop:pointcut id="player" expression="execution(* com.zealzhangz.common.Instrument.play(..))"/>
<!--前置通知,表演之前观众就坐-->
<aop:before method="takeSeats" pointcut-ref="player"/>
<!--前置通知,表演之前观众关闭手机-->
<aop:before method="takeOffCellphones" pointcut-ref="player"/>
<!--成功表演观众鼓掌-->
<aop:after-returning method="applaud" pointcut-ref="player"/>
<!--表演失败,观众要求退钱-->
<aop:after-throwing method="demandRefund" pointcut-ref="player"/>
</aop:aspect>
</aop:config>
声明环绕通知
-
环绕通知相比前置、后置通知的有优点,假设有场景需要计算前置通知与后置通知代码执行的时间,那不得不在切面类中声明一个字段保存起始时间在后置通知中使用结束时间减去字段中的起始时间。如果这么实现的话至少存在以下两个问题。
- 因为切面类也是
Bean
,Bean
是以单列的形式存在。那么存在字段的话会产生线程安全问题 - 即便不存在线程安全的问题,代码的实现逻辑也相对复杂
- 因为切面类也是
-
这种情景环绕通知能很好的解决这个问题,如下代码:
<aop:config>
<aop:aspect ref="audience">
<aop:pointcut id="player" expression="execution(* com.zealzhangz.common.Instrument.play(..))"/>
<!--环绕通知-->
<aop:around method="watchPerformance" pointcut-ref="player"/>
</aop:aspect>
</aop:config>
public void watchPerformance(ProceedingJoinPoint joinPoint){
try{
System.out.println("The audience taking their seat.");
System.out.println("The audience take off their cellphones");
long start = System.currentTimeMillis();
joinPoint.proceed();
long end = System.currentTimeMillis();
System.out.println("CLAP CLAP CLAP CLAP");
System.out.println("The performance took " + (end - start) + " milliseconds");
} catch (Throwable t){
System.out.println("Boo! We want our money back");
}
}
注意: 以上环绕通知方法包含了入参ProceedingJoinPoint
这里是必须的,joinPoint.proceed();
这行代码是必须的,
为通知传递参数
- 有时候通知不仅仅是对方法的简单包装,还需要校验传递给方法的参数值。
- 例如
MindReader
有两个接口分别是截听志愿者的思想、获取志愿者的思想。
public interface MindReader {
/**
* 截听志愿者的思想
* @param thoughts
*/
void interceptThoughts(String thoughts);
/**
* 获取志愿者的思想
* @return
*/
String getThoughts();
}
//预言家`Magician`是MindReader的具体实现
public class Magician implements MindReader {
private String thoughts;
@Override
public void interceptThoughts(String thoughts){
System.out.println("Intercepting volunteer's thoughts");
this.thoughts = thoughts;
}
@Override
public String getThoughts(){
return this.thoughts;
}
}
//现在为读心者定义一个读心对象Thinker
public interface Thinker {
/**
* 思考者
* @param thoughts
*/
void thinkOfSomething(String thoughts);
}
//一个思考着具体实现志愿者
public class Volunteer implements Thinker {
private String thoughts;
@Override
public void thinkOfSomething(String thoughts){
this.thoughts = thoughts;
}
public String getThoughts(){
return thoughts;
}
}
- 定义切面读取志愿者的思想
<bean id="magician" class="com.zealzhangz.pojo.Magician"/>
<bean id="volunteer" class="com.zealzhangz.pojo.Volunteer"/>
<aop:config>
<aop:aspect ref="magician">
<aop:pointcut id="thinking"
expression="execution(* com.zealzhangz.common.Thinker.thinkOfSomething(String)) and args(thoughts)"/>
<aop:before pointcut-ref="thinking"
method="interceptThoughts"
arg-names="thoughts"/>
</aop:aspect>
</aop:config>
- Magician超感官知觉的关键之处在于在于切点定义
<aop:before>
和arg-names
属性,切点标识了Thinker
的方法thinkOfSomething()
指定了String参数,然后在args
参数中标识了将thoughts
作为参数。<aop:before>
中也引用了参数thoughts
通过切面引入新功能(为Bean添加新方法)
- 因为Java是静态语言,一旦编译完成就很难再为类添加新功能了。
- 直接上代码
<aop:config>
<aop:aspect>
<aop:declare-parents types-matching="com.zealzhangz.common.Instrument+"
implement-interface="com.zealzhangz.common.Contestant"
default-impl="com.zealzhangz.pojo.GraciousContestant"/>
</aop:aspect>
</aop:config>
<!--或者可以把实现直接引用默认的bean-->
<bean id="graciousContestant" class="com.zealzhangz.pojo.GraciousContestant"/>
<aop:config>
<aop:aspect>
<aop:declare-parents types-matching="com.zealzhangz.common.Instrument+"
implement-interface="com.zealzhangz.common.Contestant"
delegate-ref="graciousContestant"/>
</aop:aspect>
</aop:config>
types-matching
匹配实现了com.zealzhangz.common.Instrument
接口的所有beanimplement-interface
对所有匹配的Bean添加实现接口com.zealzhangz.common.Contestant
default-impl
接口的默认实现为com.zealzhangz.pojo.GraciousContestant
注解切面
- 我们以上的切面都是通过在XML中定义实现的,对于不喜欢XML配置的程序员来说,可直接在Java中使用注解实现切面的功能。
- 直接上代码
public interface Athlete {
/**
* 具体运动
*/
void running();
}
public class Swimmer implements Athlete {
@Override
public void running(){
System.out.println("I'm swimming");
}
}
@Aspect
@Component
public class AthleteAop {
/**
* 定义切点
*/
@Pointcut(
"execution(* com.zealzhangz.common.Athlete.running(..))")
public void sports(){
}
@Before("sports()")
public void readySports(){
System.out.println("I'm going to ready to sports");
}
@Before("sports()")
public void haveReady(){
System.out.println("Prepare to end");
}
@AfterReturning("sports()")
public void calculationResult(){
System.out.println("Good job.");
}
@AfterThrowing("sports()")
public void failed(){
System.out.println("I screwed up");
}
}
- 除了以上代码还需要在
Spring
上下文中声明一个自动代理Bean
,该Bean
知道如何把@AspectJ
注解所标注的Bean
转变为代理通知,为此Spring
提供了AnnotationAwareAspectJAutoProxyCreator
的自动代理创建类,但是使用起来比较复杂,替代方案是使用一个XML
配置,配置如下:
<aop:aspectj-autoproxy/>
注意事项:切面类AthleteAop
一定要注解@Component
再注解@Aspect
,@Component
是必须的且顺序不能变,否则切面不会生效。
注解环绕通知
- 需使用注解
@Around
,一个例子如下:
@Aspect
@Component
public class AthleteAop {
/**
* 定义切点
*/
@Pointcut(
"execution(* com.zealzhangz.common.Athlete.running(..))")
public void sports(){
}
@Around("sports()")
public void watchingAthlete(ProceedingJoinPoint proceedingJoinPoint){
try{
System.out.println("I'm going to ready to sports");
System.out.println("Prepare to end");
proceedingJoinPoint.proceed();
System.out.println("Good job.");
}catch (Throwable t){
System.out.println("I screwed up");
}
}
}
传递参数给标注的通知
- 代码如下
public interface Athlete {
/**
* 具体运动
* @param myDream
*/
void running(String myDream);
}
public class Swimmer implements Athlete {
@Override
public void running(String myDream){
System.out.println("I'm swimming");
System.out.println(myDream);
}
}
@Aspect
@Component
public class AthleteAop {
@Pointcut(
"execution(* com.zealzhangz.common.Athlete.running(String)) && args(myDream)")
public void sports(String myDream){
}
@Before("sports(myDream)")
public void readySports(String myDream){
System.out.println("I'm going to ready to sports");
System.out.println("Swimmer dream is " + myDream);
}
}
@Test
public void TestSwim(){
Athlete swimmer = (Athlete)this.context.getBean("swimmer");
swimmer.running("I want to win.");
}
- 注意切点定义时候的
args
参数,以及前置通知注解中的参数@Before("sports(myDream)")
标注引入(使用注解为Bean添加新方法)
- 等价于aop:declare-parents的注解是@AspectJ的@DeclareParents
- 直接上代码
@Aspect
@Component
public class ContestantIntroducer {
@DeclareParents(value = "com.zealzhangz.common.Instrument+",
defaultImpl = GraciousContestant.class)
public static Contestant contestant;
}
-
如上代码
ContestantIntroducer
是一个切面,和之前创建切面所有不同的是该切面并没有前置、环绕、后置通知,它为实现了Instrument
接口的bean引入了Contestant
接口,Contestant
的实现为GraciousContestant
.value
属性等价于<aop:declare-parents>
的ypes-matching
defaultImpl
等价于<aop:declare-parents>
的default-impl
- 由
@DeclareParents
所标注的static属性指定了将被引入的接口 - 一个主意事项是
@DeclareParents
并没有对应<aop:declare-parents>
的delegate-ref
,这是因为@AspectJ
并不是属于Spring的一个项目,因此它并不了解Spring的bean
-
我们需要把
ContestantIntroducer
申明为Spring上下文中的一个,可以采用以下两个方法之一- 在XML中配置为bean:
<bean class="com.zealzhangz.pojo.ContestantIntroducer"/>
- 直接使用
@Component
注解,这种方式需要在XML中配置bean扫描范围<context:component-scan base-package="com.zealzhangz"/>
- 在XML中配置为bean:
-
使用注解的形式也必须记得在XML中引入
<aop:aspectj-autoproxy/>
注入AspectJ切面
- 上代码
public aspect JudgeAspect {
public JudgeAspect(){}
//定义切点
pointcut sports():execution(* com.zealzhangz.common.Athlete.running(..));
//后置通知
after() returning():sports(){
System.out.println(criticismEngine.getCriticism());
}
private CriticismEngine criticismEngine;
public void setCriticismEngine(CriticismEngine criticismEngine) {
this.criticismEngine = criticismEngine;
}
}
public interface CriticismEngine {
String getCriticism();
}
public class CriticismEngineImpl implements CriticismEngine{
private String[] criticismPool;
@Override
public String getCriticism(){
int i = (int)(Math.random()*this.criticismPool.length);
return this.criticismPool[i];
}
/**
* by spring inject
* @param criticismPool
*/
public void setCriticismPool(String[] criticismPool) {
this.criticismPool = criticismPool;
}
}
- 简单的分析一下以上代码:
JudgeAspect
为切面类,定义了切点和后置通知;CriticismEngineImpl
实现了接口CriticismEngine
,这个评价类会随机的获取评价信息。 XML
代码如下
<bean id="criticismEngine" class="com.zealzhangz.pojo.CriticismEngineImpl">
<property name="criticismPool">
<list>
<value>I'm not rude.</value>
<value>You are great.</value>
<value>I'm sorry.Too bad.</value>
<value>Are you serious.</value>
<value>Bye!Bye!</value>
</list>
</property>
</bean>
<bean class="com.zealzhangz.pojo.JudgeAspect"
factory-method="aspectOf">
<property name="criticismEngine" ref="criticismEngine"/>
</bean>
- 切面类
JudgeAspect
也被注册成了bean
,其中评价字段属性也由bean
装配。 POM
文件依赖
<properties>
<spring.version>3.2.8.RELEASE</spring.version>
<aspectj.version>1.8.9</aspectj.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.source-target.version>1.7</java.source-target.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>${aspectj.version}</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>${aspectj.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.7</version>
<configuration>
<showWeaveInfo>true</showWeaveInfo>
<source>${java.source-target.version}</source>
<target>${java.source-target.version}</target>
<Xlint>ignore</Xlint>
<complianceLevel>${java.source-target.version}</complianceLevel>
<encoding>UTF-8</encoding>
<verbose>true</verbose>
</configuration>
<executions>
<execution>
<!-- IMPORTANT -->
<phase>process-sources</phase>
<goals>
<goal>compile</goal>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjtools</artifactId>
<version>${aspectj.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
注意: 这里有个特别要注意的地方,编译aspectj语法的代码需要需要使用Ajc编译器,而不能使用javac编译器。否则会出现找不到类的异常提示Caused by: java.lang.ClassNotFoundException: com.zealzhangz.pojo.JudgeAspect
,如上POM
文件中的<build>
节点就定义了相关信息。
- 总结:①虽然
Spring AOP
能满足很多场景切面需求,但相比AspectJ
,Spring AOP
是一个功能比较弱的AOP
解决方案。AspectJ
提供了Spring AOP
所不能支持的许多类型的切点。②当Spring AOP
不能满足需求时,我们可以使用强大的@AspectJ
,对于这些场景我们可以使用Spring
为AspectJ
切面注入依赖。这些核心技术为创建松耦合的应用奠定了基础。
本文由 zealzhangz 创作,采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
最后编辑时间为:
2018/07/11 15:50