Spring in Action之AOP
AOP术语
通知(Advice)
通知定义了切面是什么以及何时使用。Spring切面可以应用5种类型的通知:
- 前置通知(Before):在目标方法被调用之前调用通知功能;
- 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么;(会在目标方法返回或抛出异常后调用)
- 返回通知(After-returning):在目标方法成功执行之后调用通知功能;
- 异常通知(After-throwing):在目标方法抛出异常后调用通知功能;
- 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和之后执行自定义的行为。
连接点(Join point)
连接点是应用执行过程中能够插入切面的一个点。
切点(Pointcut)
切点的定义会匹配通知所要织入的一个或者多个连接点。通常使用明确的类和方法名称,或者利用正则表达式定义所匹配的类和方法名称来指定这些切点。
切面(Aspect)
切面是通知和切点的结合。
引入(Introduction)
引入允许我们向现有的类添加新的方法或属性。
织入(Weaving)
织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。在目标对象的生命周期里有多个点可以进行织入:
- 编译期:切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ的织入编译器就是以这种方式织入切面的。
- 类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器,它可以在目标类被引用之前增强该目标类的字节码。AspectJ5的加载时织入(loading-time weaving ,LTW)就支持以这种方式织入切面。
- 运行期:切面在应用程序运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态的创建一个代理对象。Spring AOP就是以这种方式织入切面的。
非常重要一点:切点定义了哪些连接点会得到通知。
Spring提供了4种类型的AOP支持:
- 基于代理的经典SpringAOP
- 纯POJO切面
- @AspectJ注解驱动的切面
- 注入式AspectJ切面(适用于Spring各个版本)
Spring对AOP的支持局限于方法拦截。
通过在代理类中包裹切面,Spring在运行期把切面织入到Spring管理的bean中。Spring的切面由包裹了目标对象的代理类实现。代理类处理方法的调用,执行额外的切面逻辑,并调用方法。
切点表达式:
execution(* com.up.vincent.Performance.perform(..)) and within(com.up.vincent.*)
任意返回类型,方法所属的类,方法,任意参数,指定包下。
execution(* com.up.vincent.Performance.perform(..)) and !bean('woodstock')
切面的通知会被编织到所有ID不为woodstock的bean中。
切面样例代码:
@Aspect
public class Audience {
@Pointcut("execution(* com.soundsystem.Performance.perform(..))")
public void performance() {
}
@Before("performance()")
public void silencePhone() {
System.out.println("xx xx");
}
@Before("performance()")
public void takeSeates() {
System.out.println("xx xx");
}
@AfterReturning("performance()")
public void applause() {
System.out.println("xx xx");
}
@AfterThrowing("performance()")
public void demandRefund() {
System.out.println("xx xx");
}
}
在配置类的类级别上通过使用@EnableAspectJAutoProxy注解启动自动代理功能。
@EnableAspectJAutoProxy
@Configuration
@ComponentScan
public class ConcertConfig {
@Bean
public Audience audience() {
return new Audience();
}
}
如果使用XML配置的方式:
<?xml version="1.0" encoding="UTF-8"?>
<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"
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">
<context:component-scan base-package="com.soundsystem"/>
<!-- 启动AspectJ自动代理 -->
<aop:aspectj-autoproxy/>
<!-- 声明Audience这个bean -->
<bean id="audience" class="com.soundsystem.Audience"/>
</beans>
注意:不管使用JavaConfig还是XML,Aspect自动代理都会为使用@Aspect注解的bean创建一个代理,这个代理会围绕着所有该切面的切点所匹配的bean。
环绕通知:
@Aspect
public class Audience {
@Pointcut("execution(* com.soundsystem.Performance.perform(..))")
public void performance() {
}
@Around("performance()")
public void watchPerformance(ProceedingJoinPoint joinPoint) {
try {
System.out.println("xx xx");
joinPoint.proceed();
System.out.println("yy yy");
} catch (Throwable throwable) {
System.out.println("zz zz");
}
}
}
处理通知中的参数:
@Aspect
public class TrackCounter {
private Map<Integer, Integer> trackCounts = new HashMap<>();
@Pointcut("execution(* com.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;
}
}
注意:参数的名称trackNumber也与切点方法签名中的参数想匹配。从命名切点到通知方法的参数转移。
通过注解引入新的功能
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.DeclareParents;
@Aspect
public class EncoreableIntroducer {
@DeclareParents(value="concert.Performance+", defaultImpl=DefaultEncoreable.class)
public static Encoreable encoreable;
}
@DeclareParents解释说明:
- value属性指定了哪种类型的bean要引入该接口,在本例中,也就是所有实现Performance的类型。(标记符后面的+(加号)表示Performance的所有子类型,而不是Performance本身)
- defaultImpl属性指定了为引入功能提供实现的类,在这里,我们指定的是DefaultEncoreable提供实现。
- @DeclareParents注解所标注的静态属性指明了要引入的接口,在这里,我们所引入的是Encoreable接口。
在XML中声明切面
Spring中AOP配置元素能够以非侵入性的方式声明切面
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:*>元素都必须包含在此元素之内 |
< aop:declare-parents> | 以透明的方式为被通知的对象引入额外的接口 |
< aop:pointcut> | 定义一个切点 |
代码样例:
<bean id="audience" class="com.springinaction.springidol.Audience"/>
<aop:config>
<aop:pointcut id="performance" expression="execution(* *.perform(..))"/>
<aop:aspect ref="audience">
<!-- 注意在这些通知中引用方法的时候,只需要写方法名,不需要() -->
<aop:before method="takeSeats" pointcut-ref="performance"/>
<aop:before method="turnOffCellPhones" pointcut-ref="performance"/>
<aop:after-returning method="applaud" pointcut-ref="performance"/>
<aop:after-throwing method="demandRefund" pointcut-ref="performance"/>
</aop:aspect>
</aop:config>
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="trackCounter" class="soundsystem.TrackCounter"/>
<bean id="cd" class="soundsystem.BlankDisc">
<property name="title" value="Sgt. Pepper's Lonely Hearts Club Band"/>
<property name="artist" value="The Beatles"/>
<property name="tracks">
<list>
<value>Sgt. Pepper's Lonely Hearts Club Band</value>
<value>With a Little Help from My Friends</value>
<value>Lucy in the Sky with Diamonds</value>
<value>Getting Better</value>
<value>Fixing a Hole</value>
</list>
</property>
</bean>
<aop:config>
<aop:aspect ref="trackCounter">
<aop:pointcut id="trackPlayed" expression="execution(* soundsystem.CompactDisc.playTrack(int)) and args(trackNumber)"/>
<aop:before pointcut-ref="trackPlayed" method="countTrack"/>
</aop:aspect>
</aop:config>
</beans>
XML方式通过切面引入新的功能:
<aop:aspect>
<aop:declare-parents
types-matching="concert.Performance+"
implement-interface="concert.Encoreable"
default-impl="concert.DefaultEncoreable"
/>
</aop:aspect>
注意:default-impl必须使用全限定类名来显示指定Encoreable的实现。除此之外,还可以使用delegate-ref属性来标识。
<aop:aspect>
<aop:declare-parents
types-matching="concert.Performance+"
implement-interface="concert.Encoreable"
delegate-ref="encoreableDelegate"
/>
</aop:aspect>
<bean id="encoreableDelegate" class="concert.DefaultEncoreable" />
注入AspectJ切面
代码JudgeAspect文件的后缀为.aj
public aspect JudgeAspect {
public JudgeAspect() {
}
pointcut performance(): execution(* perform(..));
after() returning(): performance() {
System.out.println(criticismEngine.getCriticism());
}
private CriticismEngine criticismEngine;
public void setCriticismEngine(CriticismEngine criticismEngine) {
this.criticismEngine = criticismEngine;
}
}
public class CriticismEngineImpl implements CriticismEngine {
public CriticismEngineImpl() {}
public String getCriticism() {
int i = (int) (Math.random() * criticismPool.length);
return criticismPool[i];
}
// injected
private String[] criticismPool;
public void setCriticismPool(String[] criticismPool) {
this.criticismPool = criticismPool;
}
}
<bean id="criticismEngine"
class="com.springinaction.springidol.CriticismEngineImpl">
<property name="criticisms">
<list>
<value>Worst performance ever!</value>
<value>I laughed, I cried, then I realized I was at the wrong show.</value>
<value>A must see show!</value>
</list>
</property>
</bean>
<bean class="com.springinaction.springidol.CriticAspect" factory-method="aspectOf">
<property name="criticismEngine" ref="criticismEngine" />
</bean>
通常情况下,Spring bean由Spring容器初始化,但是AspectJ切面是由AspectJ在运行期创建的。
等到Spring为CriticAspect注入CriticismEngine时,CriticAspect已经被实例化了。Spring需要通过aspectOf()工厂方法获得切面的引用。