面向切面是Spring又一大核心,本章我们就来详细了解下:面向切面编程的基本原理以及创建使用切面的常用方法,如何为POJO创建切面,使用@AspectJ注释,为AspectJ切面注入依赖。
为什么要AOP:
日志、安全和事物管理是软件中很重要的组成部分,但是如果每个应用对象都需要自己处理这些问题,不仅开发者会十分厌烦,而且也不利于维护、复用以及扩展。这些分布于应用多处的功能被称为
横切关注点。
依赖注入(DI)是为了应用对象之间的解耦,面向切面(AOP)则是为了横切关注点和应用对象之间的解耦。
1. 什么是面向切面(继承、委托和切面):
AOP也是一种复用的手段,那些横切关注点都是应该与核心功能分离的职责。就复用来说还有继承和委托。
如果使用继承,整个应用系统很多地方都得使用相同的基类,比如在Android开发中Activity和Service它们都有可能用到缓存处理(举个例子,缓存可以和网络请求处理一起放在数据处理层和控制层分离),如果把缓存放到基类中,那么activity和service都要分别创建一个包含该功能的基类,因为很难修改activity和service的基类,它在framework中。因此基于继承的复用在系统级别是很难实现的,应为应用范围越大,系统中可能包含多个角度的划分,你要修改基类会造成很大影响。另外使用继承本身还会造成类数量”爆炸“,使得系统庞大臃肿。
而是用委托可以很大程度上解决,但是委托一般使用组合的方式对接受委托对象进行调用,被委托的服务对委托者来说仍旧是可见的,有时可能需要对委托对象进行复杂的调用,使得类与类之间的联系变得不是很清晰。
切面提供了另一种选择,仍然是在集中的地方定义通用功能,横切关注点可以被模块化为特殊的类,首先每个关注点关注一个问题集中到一处不是分散到多处代码。其次,服务模块更加简洁,只需要关心核心功能,因为如果调用切面服务模块不用关心,交给AOP容器来完成(运行期是这样,编译期和类加载期稍后讨论),很类似依赖倒转,但它解决是行为之间的耦合。
1.1 AOP术语:
![](https://app.yinxiang.com/shard/s24/res/d1bc6f11-4678-4c68-9baf-78868620027b.png)
通知(advice):通知决定切面何时使用,是方法前还是方法后,spring提供了5种选择,Before,After,After-returning(成功后调用),After-throwing(异常后调用),Around(包含方法,前后执行自定义行为);
连接点(join point):在应用中会有很多可以应用通知的时机,调用方法时,抛出异常时,修改字段时,这些都是连接点,切面代码可以利用这些连接点插入添加新的行为;
切点(pointcut):通知定义了“什么”和“何时”,切点就定义了“何处”,通常使用明确的类和方法名称来指定这些切点,或是利用正则表达式定义匹配的类和方法名来指定切点,定义切点就是选择那些连接点可以织入;
切面(Aspect):切面就是完成的横切关注点的功能,它是切点和通知的结合,最后通知和切点共同决定了是什么、在何时和何处完成其功能;
引入(introduction):向现有类添加新的方法和属性,无需修直接改现有类;
织入(Weaving):将切面应用到目标对象来创建新的代理对象的过程,切面在指定的连接点被织入到目标对象中:
编译期:AspectJ提供的织入编译器织入切面;
类加载器:AspectJ提供LTW类加载器织入切面;
运行期:AOP容器会为目标对象动态地创建一个代理对象织入切面(动态代理,可以参看《Java编程思想》);
1.2 Spring对AOP的支持:
Spring提供4种AOP支持(前三种都是基于代理的AOP变体):
(1)基于代理的经典AOP;
(2)@AspectJ注解驱动的切面;
(3)纯POJO切面;
(4)注入式AspectJ切面;
Spring是运行期AOP通知对象的,在调用目标bean方法时,才会创建代理对象。
Spring支持方法连接点,因为Spring基于动态代理所以只支持方法连接点,而AspectJ和Jboss则不同。
![](https://app.yinxiang.com/shard/s24/res/c59fc950-265a-4e2a-8a5a-e1d7fbb06c7a.png)
2. 使用切点选择连接点:
介绍完了aop和spring中的aop后,首先看一看如何编写切点:
Spring借助AspectJ的切点表达式来定义切面,只能使用AspectJ中基于代理的部分,否则会抛出异常。
![](https://app.yinxiang.com/shard/s24/res/e2adb9da-272d-4b32-b139-7dcf07828b8a.png)
2.1 编写切点:
定义pointcut:execution(* com.springection.springdol.Instrument.play(..));
*表示任意返回类型,play(..)表示任意参数;
execution是主要使用的AspectJ表达式,其他的可以用来限定pointcut的范围。
2.2 bean()指示器:
execution(* com.springinaction.springdol.Instrument.play()) and !bean(eddie)
可以指定切点位于ID不为eddie的Instrument类型的Bean上。
3. 在XML中声明切面:
通过如下的标签可以定义基于POJO的不同位置的切面声明:
![](https://app.yinxiang.com/shard/s24/res/6a79d737-109a-4dba-a62c-5e49d2629bfa.png)
我们可以定义一个简单的Java类:
public class Audience {
public void takeSeats() {...}
public void turnOffCellPhones(){}
public void applaud() {...}
public void demandRefund() {...}
}
接着我们可以定义xml来设置切面的配置:
<aop:config>
<aop:aspect ref="audience">
<aop:pointcut id="performance" expresstion="【aspectJ表达式】"
<aop:before pointcut-ref='performance' method="takeSeats" />
<aop:before pointcut-ref="performance" method="turnOffCellPhones" />
...
</aop:aspect>
</aop:config>
通知的逻辑是如下组织的:
![](https://app.yinxiang.com/shard/s24/res/4c643cf7-ef42-49ae-9dfc-bd7d15fdd9bb.png)
声明环绕通知:
你可能想如果前置通知和后置通知如何来共享一些必要信息呢?Audience类是单例的,因此成员变量的方式不能保证线程安全。因此环绕的方式就很适合解决这类问题。
public void watchPerformance(ProceedingJ joinpoint) {
try {
//前置功能
joinpoint.proceed();
//后置功能
} catch(Throwable t) {
//失败后
}
}
再定义一个<aop:around />就可以实现了,可以看到,切点是通过参数的方式传入,同样可以实现前置、后置和异常时的切面功能,也不用担心信息共享的问题。
为通知传递参数:
之前几种方式你可能会发现没没传入额外的参数,当然spring提供传参的功能:
<aop:config>
<aop:aspect ref="nagician">
<aop:pointcut id="thinking" expression="...
and args(thougths)" />
<aop:before pointcut-ref="thinking" method="interceptThoughts"
arg-names="thoughts" />
</aop:aspect>
</aop:config>
通过切面引入新功能:
![](https://app.yinxiang.com/shard/s24/res/3ba14216-b00c-4c2f-ad23-5b05777af7be.png)
虽然java不是一个开放式的语言,但我们知道sprin是通过代理的方式实现aop,因此可以通过为代理定义新的方法的方式来增加新的功能。
你可能会问什么要这么做,有什么意义,设想需求更改有时你可能需要需要为每一类及其子类添加新的功能,但是不同的子类可能有不同的实现,因此你不能直接基类,那么你可能会想到增加一些抽象类,这种方式增加了类的数量,另外使用接口,这就要你手动修改很多类,设置扩展jar包中的类的时候,难道不断的继承吗,显示spring作为开发框架就解决这类问题的。
例:
为Performer添加Contestant接口:
public interface Contestant {
void receiveAward();
}
方式一:
<aop:aspect>
<aop:declare parents
types-matching="com.springinaction.springidol.performer+"
implement-interface="xxx.Contestant"
default-impl="xxx.ConcreteContestant" /> 具体的实现
</aop:aspect>
方式二:
<aop:aspect>
<aop:declare parents
types-matching="com.springinaction.springidol.performer+"
implement-interface="xxx.Contestant"
default-refs="contestantDelegate" /> 这里是Bean的ID
</aop:aspect>
这样就可以不修改原有类的代码了,减少了入侵式编程,实现了“开发-封闭”;
4. 注解切面:
使用注解可以代替XML定义切面,直接在POJO上添加注解可以实现切面的定义:
@Aspect
public
class
Audiance {
@Pointcut
(
"execution(* com.yjh.example.Performer.performance())"
)
public
void
performance() {
}
@Before
(
"performance()"
)
public
void
takeSeats() {
System.
out
.println(
"[before]take seats"
);
}
@Before
(
"performance()"
)
public
void
turnOffLights() {
System.
out
.println(
"[before]turn off lights"
);
}
}
这样可以通过注解方便的定义切面,不需要再XML中配置aop了。我们已经知道Spring的aop是基于动态代理的,因此需要在使用时为Bean创建代理。
Spring通过创建一个AnnotationAwareAspecJProxyCreator类的Bean,因此要将它注册为Bean,通过它来创建自动代理类。为了简化注册的过程,你只要在配置xml中添加,
<aop:aspectj-autoproxy />Spring就知道注册该Bean了。
使用AspectJ注解的优缺点:
优点:可以减少XML的量,方便使用;
缺点:对于无法修改源码的类不能应用切面,还得用<aop:aspect>
4.1 注解环绕通知:
使用@around可以实现环绕通知:
@Around
(
"performance()"
)
public
void
watchPerformance(ProceedingJoinPoint
proccedJoinPoint
) {
//定义环绕通知,比如统计处理过程的耗时
try
{
System.
out
.println(
"start proceeding..."
);
long
startTime
= System.currentTimeMillis ();
proccedJoinPoint
.proceed();
long
endTime
= System.currentTimeMillis ();
System.
out
.println(
"proceed used: "
+ (
endTime
-
startTime
) +
"ms in total."
);
}
catch
(Throwable
e
) {
System.
out
.println(
"[ERROR]There is a error caughted in proceeding."
);
}
}
这个例子展示了计算程序处理过程时间统计的功能,如果不用切面,这样的功能写到业务逻辑中,再反复删掉想想都是一件很麻烦的事。
4.2 注解传递参数给通知:
@Aspect
public
class
MindReader {
private
String
thoughts
;
@Pointcut
(
"execution(* com.yjh.example.Thinker.thinkAboutSomething(String)) && args(thoughts)"
)
public
void
thinkAboutSomething(String
toughts
) {
}
@Before
(
"thinkAboutSomething(thoughts)"
)
public
void
interceptThought(String
thoughts
) {
this
.
thoughts
=
thoughts
;
}
public
String getThoughts() {
return
thoughts
;
}
}
4.3 标注引入:
之前已经提到过,引入是在不修改源码的情况下给Bean添加新的行为,Spring已经提供了<aop:declared-parents>来实现xml引入。
通过@DeclareParents注解我们也可以实现为Bean添加新行为的过程。
@Aspect
public
class
ContestantIntroducer {
@DeclareParents
(
value=
"com.yjh.example.Performer+"
,
defaultImpl=ConcreteContestant.
class
)
public
static
Contestant
contestant
;
}
注册
ContestantIntroducer为Bean,<bean class="xxx.
ContestantIntroducer
" />
注意:使用@DeclareParents并没有和declare-ref属性相对应的注解。因此如果要委托的对象是Spring Bean的话,还是要使用<aop:declare-parents>
5. 注入AspectJ切面:
Spring只能提供基于方法的动态代理的AOP,如果你需要更强大的切面功能,比如在创建通知时应用通知,Spring就无能为力了。而你可以使用AspectJ的其他切面功能来完成。
在使用AspectJ切面的时候,Spring可以做什么呢?切面本身也是一个类,它可能也有一些依赖关系,因此使用Spring的依赖注入来解决切面的属性注入问题。
public aspect JudgeAspect {
public JudgeAspect() {}
pointcut performance() : execution(* perform(..));
after() returning() : performance() {...}
//injected
private CriticismEngine criticismEngine;
public void setCriticismEngine(CriticismEngine criticismEngine) {
this.criticismEngine = criticismEngine;
}
}
这就是一个AspectJ切面,接下来可以使用spring来注入依赖:
<bean class="com.springinaction.springdol.JudgeAspect"
factory-method="aspectOf">
<property name="critismEngine" ref="criticismEngine" />
</bean>
注意这里使用了factory-method,AspectJ切面是AspectJ在运行期创建的,Spring无法创建,因此应该使用AspectJ切面提供的aspectOf()静态方法作为工厂方法。