文章目录
AOP的实现
AOP实现有两种方式:JDKProxy和CGlib(Code generate library)
- 如果目标类是接口则使用JDKproxy,否则用后者
- JDKProxy的核心:InvocationHandler接口和Proxy类。使用java反射机制实现
- Cglib:以继承的方式动态生成目标类的代理。
1. AOP术语
通知(advice)
通知:即切面所要完成的工作,和什么时候执行这个工作
Spring切面有5种类型的通知:
- 前置通知:在目标方法被调用前调用通知功能
- 后置通知:在目标方法完成后调用通知功能
- 返回通知:在目标方法成功执行后调用通知
- 异常通知:在目标方法抛出异常后调用通知功能
- 环绕通知:通知包裹了被通知的方法,在被通知的方法调用前和调用后执行自定义的行为。
连接点
应用有很多时机应用通知,这些时机被称为连接点。连接点是应用执行过程种插入切面的一个点。这个点可以是调用方法时、抛出异常时、或者修改一个字段时。切面代码利用这些点插入到应用的流程中去。(感觉和通知的when差不多)
切点
通知定义了切面的“什么”和“何时”,切点定义了“何处”。切点会匹配所要织入的一个或多个连接点。
切面
通知和切点定义了切面的全部内容。
织入
织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。
在目标的生命周期中有多个点可以进行织入:
- 编译期:切面在目标编译时被织入。需要特殊的编译器。AspectJ的编译器是以这种方式。
- 类加载期:切面在目标类加载到JVM时被织入。需要特殊的类加载器。AspectJ5的加载时织入支持。
- 运行期:切面在应用运行的某个时刻被织入。Spring AOP是以这种方式。
2. Spring对AOP的支持
Spring提供4种类型的AOP支持:
- 基于代理的经典SpringAOP(被淘汰)
- 纯POJO切面(需要使用XML)
- @AspectJ注解驱动的切面(不需要使用XML)
- 注入式AspectJ切面(适用于Spring各版本)
前三种是Spring AOP实现的变体,Spring AOP建立在动态代理的基础之上,所以Spring对AOP的支持局限于方法拦截。
Spring在运行期把切面织入到Spring管理的bean中。代理类封装了目标类,并拦截被通知方法的调用,再把调用转发给真正的目标bean。当代理拦截到方法调用时,在调用目标bean方法之前,会执行切面逻辑(并不代表是前置通知)。
3. 编写切点
在Spring AOP中,要使用AspectJ的切点表达式语言来定义切点。Spring支持的AspectJ切点指示器有:
- arg() : 限制连接点匹配参数为指定类型
- @args() : 限制连接点匹配参数由指定注解标注
- execution() : 用于匹配连接点
- this() :限制连接点匹配AOP代理的bean引用为指定类型
- target:限制连接点匹配目标对象为指定类型
- @target():限制连接点匹配特定的注解的执行对象
- within():限制连接点匹配指定的类型
- @within():限制连接点匹配指定注解所标注的类型
- @annotation:限制匹配带有指定注解的连接点
假设有一个Performance接口:
package concert;
public interface Performance{
public void perform();
}
现在要编写Performance的perform()方法触发的通知:
execution(* concert.Performance.perform(..))
使用execution()指示器选择方法。“*”代表返回任意类型都可以,然后指定全限定类名方法名,对于参数列表使用(…)表示切点要选择任意的perform()方法,无论该方法入参是什么。
可以进一步限制匹配,配置的切点仅匹配concert包时:
execution(* concert.Performance.perform(..)) && within(concert.*)
还可以限制切点只匹配特定的bean时起作用,这需要指明bean的id:
execution(* concert.Performance.perform(..)) and bean("beanID")
execution(* concert.Performance.perform(..)) and !bean("beanID") //切面通知织入到所有id不为beanID的bean中
4. 编写切面
使用Aspect注解来定义切面:
package concert
@Aspect
public class Audience{
@Before("execution(** concert.Performance.perform(..))")//表演之前
public void silenceCellPhone(){...}//关机
@Before("execution(** concert.Performance.perform(..))")//表演之前
public void takeSeats(){...}//落座
@AfterReturning("execution(** concert.Performance.perform(..))")//表演之后
public void applause(){...}//鼓掌
@AfterThrowing("execution(** concert.Performance.perform(..))")//表演失败之后
public void demandRefund(){...}//退票
}
- @Aspect:这是一个切面
- @Before:通知在方法调用前执行
- @After:通知在方法调用后执行
- @AfterReturning:通知在方法返回后执行
- @AfterThrowing:通知在方法抛出异常后执行
可以对上面的代码进行优化:
package concert
@Aspect
public class Audience{
@PointCut("execution(** concert.Performance.perform(..))")//定义命名的切点
public void performance(){}//这个方法只是一个标识。
@Before("performance()")//表演之前
public void silenceCellPhone(){...}//关机
@Before("performance()")//表演之前
public void takeSeats(){...}//落座
@AfterReturning("performance()")//表演之后
public void applause(){...}//鼓掌
@AfterThrowing("performance()")//表演失败之后
public void demandRefund(){...}//退票
}
Audience类同时也是一个Bean,在配置类中启用自动代理功能以启用这个切面:
@Configuration
@EnableAspectJAutoProxy//启用AspectJ自动代理
@ComponentScan
public class ConcertConfig{
@Bean
public Audience audience(){
return new Audience();
}
}
XML中:
<context:component-scan base-package="concert" />
<aop:aspectj-autoproxy /> //启用代理
<bean class="concert.Audience" /> //声明bean
5. 创建环绕通知
package concert
@Aspect
public class Audience{
@PointCut("execution(** concert.Performance.perform(..))")
public void performance(){}
@Around("performance()")//环绕通知方法
public void watchPerformance(ProceedingJoinPoint jp){
try{
System.out.println("开始进行目标方法执行前的操作");
jp.proceed();//被通知方法执行
System.out.println("开始进行目标方法执行后的操作");
} catch (Throwable e){
System.out.println("目标方法抛出异常时执行的操作");
}
}
}
- 在环绕通知中 ProceedingJoinPoint 必须作为参数传入
- ProceedingJoinPoint 的 proceed()方法调用被通知方法
- 如果不执行 proceed() 方法会对被通知方法进行阻塞
- 可以多次执行 proceed() 方法来多次调用目标方法
6. 处理通知中的参数
这段代码的上下文背景是:soundsystem包下CompactDisc类里有一个tracks数组,playTrack(int count) 方法会执行对应数组下标的track。现在的需求是:记录每个track被调用的次数
@Aspect
public class TrackCounter{
private Map<Integer, Integer> trackCounts = new HashMap<Integer, Integer>();
@PointCut("execution(** soundsystem.CompactDisc.playTrack(int))" + "&& args(trackNumber)")
public void trackPlayed(int trackNumber){}
@Before("trackPlayed(trackNumber)")//播放前计数加一
public void countTrack(int trackNumber){
int currentCount = getPlayCount(trackNumber);
trackCounts.put(trackNumber, currentCount+1);
}
public int getPlayCount(int trackNumber){
return trackCounts.containsKey(trackNumber) ? trackCounts.get(trackNumber) : 0;
}
}
这个切点声明了要给通知方法的参数,args(trackNumber)表明传递给playTrack()方法的int类型参数也会传递到通知方法中去。countTrack(int trackNumber)与args中的参数名称相匹配。这样就完成了从命名切点到通知方法的参数转移。
7. 通过注解引入新功能
在Spring中,切面只是实现了它所包装的bean相同接口的代理。同时它也可以暴露新的接口,即使底层被代理的bean没有实现这个接口。
例如:为Peformance引入Encore接口以及它的实现类:
package concert;
public interface Encore(){
public void performEncore();
}
实现类:
package concert;
@Component
public class DefaultEncore implements Encore(){
@Overwrite
public void performEncore(){
System.out.println("默认返场");
}
}
创建一个新的切面:
package concert;
@Aspect
@Component
public class EncoreIntroducer{
@DeclareParents(value="concert.Performance+", defaultImpl=DefaultEncore.class)
public static Encore encore;
}
@DeclareParents注解由三部分组成:
- value 属性指定了哪种类型的bean要引入接口。本例中代表所有实现了Performance的类型(加号表示performance所有的子类)
- defaultImpl 属性指定了为引入功能提供实现的类。你想添加的功能的实现。
- @DeclareParents 标注的静态属性指明了要引入的接口,你想添加的功能。在这里引入的是Encore接口。
将bean注入Spring容器并开启代理:
package concert;
@Configuration
@ComponentScan
@EnableAspectAutoProxy
public class AnnotationConfig{}
当Spring发现一个bean使用了@Aspect注解,Spring就会创建一个代理,然后调用委托给被代理的bean或被引入的实现。
面向注解的切面声明有一个弊端:必须能够为通知类添加注解,必须要有通知类的源码。
8. 在XML中声明切面
如果需要声明切面,但又不能为通知类添加注解,那么就要使用XML配置了。
AOP配置元素 | 用途 |
---|---|
aop:config | 顶层的AOP配置元素 |
aop:advisor | 定义AOP通知器 |
aop:before | 定义AOP前置通知 |
aop:after | 定义AOP后置通知 |
aop:after-returning | 定义AOP返回通知 |
aop:after-throwing | 定义AOP异常通知 |
aop:around | 定义AOP环绕通知 |
aop:aspect | 定义一个切面 |
aop:aspectj-autoproxy | 启用@AspectJ注解驱动的切面 |
aop:declare-parents | 以透明的方式为被通知的对象引入额外的接口 |
aop:pointcut | 定义一个切点 |
将之前Audience类的注解全部拿掉:
package concert
public class Audience{
public void silenceCellPhone(){...}//关机
public void takeSeats(){...}//落座
public void applause(){...}//鼓掌
public void demandRefund(){...}//退票
}
8.1 声明前置和后置通知
<aop:config>
<aop:aspect ref="audience" <!-- 引用audience Bean作为切面--!>
<aop:before
pointcut="execution(** concert.Performance.perform(..))"
method="silenceCellPhone" /> <!-- perform方法调用前执行--!>
<aop:before
pointcut="execution(** concert.Performance.perform(..))"
method="takeSeats" />
<aop:after-returning
pointcut="execution(** concert.Performance.perform(..))"
method="applause" />
<aop:after-throwing
pointcut="execution(** concert.Performance.perform(..))"
method="demandRefund" />
</aop:aspect>
</aop:config>
使用aop:pointcut标签:
<aop:config>
<aop:aspect ref="audience" <!-- 引用audience Bean作为切面--!>
<aop:pointcut id="performance"
expression="execution(** concert.Performance.perform(..))" />
<aop:before
pointcut-ref="performance"
method="silenceCellPhone" /> <!-- perform方法调用前执行--!>
<aop:before
pointcut-ref="performance"
method="takeSeats" />
<aop:after-returning
pointcut-ref="performance"
method="applause" />
<aop:after-throwing
pointcut-ref="performance"
method="demandRefund" />
</aop:aspect>
</aop:config>
aop:pointcut标签在 aop:aspect 标签内外均可,只是作用域的不同。
8.2 声明环绕通知
Audience类:
package concert;
public class Audience{
public void watchPerformance(ProceedingJoinPoint jp){
try{
System.out.println("开始进行目标方法执行前的操作");
jp.proceed();//被通知方法执行
System.out.println("开始进行目标方法执行后的操作");
} catch (Throwable e){
System.out.println("目标方法抛出异常时执行的操作");
}
}
}
在XML中调用aop:around元素
<aop:config>
<aop:aspect ref="audience">
<aop:pointcut id="performance"
expression="execution(** concert.Performance.perform(..))" />
<aop:around
pointcut-ref="performance"
method="watchPerformance" />
</aop:aspect>
</aop:config>
8.3 为通知传递参数
移除TrackCounter上的@AspectJ注解
package soundsystem;
public class TrackCounter{
private Map<Integer, Integer> trackCounts = new HashMap<Integer, Integer>();
public void countTrack(int trackNumber){
int currentCount = getPlayCount(trackNumber);
trackCounts.put(trackNumber, currentCount+1);
}
public int getPlayCount(int trackNumber){
return trackCounts.containsKey(trackNumber) ? trackCounts.get(trackNumber) : 0;
}
}
XML中将TrackCounter配置为切面:
<beans>
<bean id="trackCounter" class="soundsystem.TrackCounter" />
<aop:config>
<aop:aspect ref="trackCounter">
<aop:pointcut id="trackPlayed"
expression="execution(** soundsystem.CompactDisc.playTrack(int)) and args(trackNumber)" /> <!--切点为另一个bean中的playTrack(trackNumber)方法 -->
<aop:before
pointcut-ref="trackPlayed"
method="countTrack" />
</aop:aspect>
</aop:config>
</beans>
XML中&符号会被解析为实体的开始,这里使用and替代&&。
8.4 通过切面引入新功能
<aop:aspect ref="beanId">
<aop:declare-parents
types-matching="concert.Performance+" <!-- 类型匹配为Performance接口的bean -->
implement-interface="concert.Encore" <!-- 要增加的接口 -->
default-impl="concert.DefaultEncore" /> <!-- 加入的接口的方法实现 -->
</aop:aspect>
除了使用default-impl还可以使用delegate-ref来指明具体的实现:
<aop:declare-parents
types-matching="concert.Performance+"
implement-interface="concert.Encore"
delegate-ref="encoreDelegate" />
这种方法需要存在一个id为encoreDelegate的bean
<bean id="encoreDelegate" class="concert.DefaultEncore" />
这种方法的好处是这个bean本身可以被注入、通知。
9. 注入AspectJ切面
AspectJ切面与Spring是相互独立的,它可以织入到任意的Java应用中。同时也可以借助Spring的依赖注入把bean装配到Aspect切面中。需要单独学习AspectJ语法。
例如:
package concert;
public aspect CriticAspect{
public CriticAspect(){}
pointcut performance() : execution(** perform(..));
afterReturning() : performance(){
Systemout.println("执行目标方法结束后的操作");
}
private CriticismEngine criticismEngine;//这是一个接口,要对CriticismEngine进行注入
public void setCriticismEngine(CriticismEngine criticismEngine){
this.criticismEngine = criticismEngine;
}
}
为AspectJ切面使用Spring依赖注入时,要先将这个切面设置为bean。然后再将 CriticismEngineImpl注入到切面当中:
<bean class="concert.CriticAspect" factory-method="aspectOf" >
<property name="criticismEngineImpl" ref="criticismEngineImpl" /> <!-- 假设criticismEngineImpl已经设置为bean -->
</bean>
这里要使用factory-method属性调用aspectOf方法,因为AspectJ切面是在AspectJ运行期创建的,等到Spring给CriticAspect注入CriticismEngine时,CriticAspect已经被实例化了。要先通过asspectOf()方法获取切面的引用,再执行依赖注入。