面向切面的Spring
Posted 在咖啡里溺水的鱼
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面向切面的Spring相关的知识,希望对你有一定的参考价值。
前导:
在软件开发中,分布于应用中多处的功能称为:横切关注点(cross-cutting concerns)。
横切关注点从概念上是与应用的业务逻辑相分离的,将横切关注点与业务逻辑相分离是面向切面编程AOP要解决的。
4.1 什么是面向切面编程
横切关注点可以被模块化为特殊的类,这些类被称为切面。
4.1.1 AOP术语
通知 Advice
切面的工作 被称为通知。
通知定义了切面是什么、何时使用。
Spring切面可以应用5种类型的通知:
- Before:在方法被调用之前调用通知
- After:在方法完成之后调用通知,无论方法执行是否成功
- After-returning:在方法成功执行之后调用通知
- After-throwing:在方法抛出异常后调用通知
- Around:通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为
连接点 Joinpoint
连接点是在应用执行过程中能够插入切面的一个点。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为
切点 Pointcut
切点用来缩小切面所通知连接点的范围,定义了切面的“何处”。
切点的定义会匹配通知所要织入的一个或多个连接点。通常使用明确的类和方法名称来指定切点,或是利用正则表达式定义匹配的类和方法名称模式来指定这些切点。某些AOP框架允许创建动态切点。
切面 Aspect
切面是通知和切点的结合。通知和切点共同定义了关于面向的全部内容——它是什么,在何时,在何处完成其功能
引入 Introduction
引入允许向现有的类添加新方法或属性。
织入 Weaving
织入是将切面应用到目标对象来创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。在目标对象的生命周期里有多个点可以进行织入。
- 编译期——切面在目标类编译时被织入。需要特殊的编译器,如AspectJ的织入编译器
- 类加载期——切面在目标类加载到JVM时被织入。需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前增强该目标类的字节码。如AspectJ 5的LTW(load-time weaving)
- 运行期——切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。如Spring AOP
小结:连接点是程序执行过程中能够应用通知的所有点。切点定义了通知被应用在哪些连接点上。
4.1.2 Spring对AOP的支持
Spring提供了4种AOP支持:
- 基于代理的经典AOP
- @AspectJ注解驱动的切面
- 纯POJO切面
- 注入式AspectJ切面(适合Spring各版本)
前3中都是Spring基于代理的AOP变体,因此,Spring对AOP的支持局限于方法拦击。如果AOP需求超过了简单方法拦截的范畴,那么应该考虑在AspectJ里实现切面,利用Spring的DI(依赖注入)把Spring Bean注入到AspectJ切面中。
一些Spring AOP框架的关键点:
- Spring通知是Java编写的
- Spring在运行期通知对象
- Spring只支持方法连接点
4.2 使用切点选择连接点
在SpringAOP中需要使用AspectJ的切点表达式语言来定义切点
*Sping仅支持AspectJ切面指示器(pointcut designator)的一个子集。
Spring AOP所支持的AspectJ切点指示器
AspectJ指示器 | 描述 |
arg() | 限制连接点匹配参数为指定类型的执行方法 |
@args() | 限制连接点匹配参数由指定注解标注的执行方法 |
execution() | 用于匹配是连接点的执行方法 |
this() | 限制连接点匹配AOP代理的Bean引用为指定类型的类 |
target() | 限制连接点匹配目标对象为指定类型的类 |
@target() | 限制连接点匹配特定的执行对象,这些对象对应的类要具备指定类型的注解 |
within() | 限制连接点匹配指定的类型 |
@within() | 限制连接点匹配指定注解所标注的类型(使用spring AOP时,方法定义在由指定的注解所标注的类里) |
@annotation | 限制匹配带有指定注解连接点 |
在Spring中尝试使用AspectJ其他指示器时,会抛出IllegalArgumentException异常。
Spring支持的指示器中,只有execution指示器时唯一的执行匹配,其他的都是用于限制匹配的。
4.2.1 编写切点
e.g.
execution( * com.springinaction.springidol.Instrument. play ( .. ) )
方法执行时触发 返回类型任意 方法所属的类型 方法 使用任意参数
可以使用&&、||、!、and、or、not来连接多个指示器
4.2.2 使用Spring的bean()指示器
Spring2.5引入。使用Bean ID或Bean名称作为参数来限制切点只匹配特定的Bean。
4.3 在XML中声明切面
Spring的AOP配置元素
AOP配置元素 | 描述 |
<aop:advisor> | 定义AOP通知器 |
<aop:after> | 定义AOP后置通知(无论被通知的方法是否执行成功) |
<aop:after-returning> | 定义AOP after-returning通知 |
<aop:after-throwing> | 定义after-throwing通知 |
<aop:around> | 定义AOP环绕通知 |
<aop:aspect> | 定义切面 |
<aop:aspectj-autoproxy> | 启用@AspectJ注解驱动的切面 |
<aop:before> | 定义AOP前置通知 |
<aop:config> | 顶层AOP配置元素。大多数<aop:*>元素必须包含在<aop:config>元素内 |
<aop:declare-parents> | 为被通知的对象引入额外的接口,并透明的实现 |
<aop:pointcut> | 定义切点 |
4.3.1 前置和后置通知
e.g.:
audience
public class Audience {
public void takeSeats() {
System. out.println( "The audience is taking their seats");
}
public void turnOffCellPhones() {
System. out.println( "The audience is truning off their cellphones");
}
public void applaud() {
System. out.println( "papapapapapa");
}
public void demandRefund() {
System. out.println( "Boo! We want our money back" );
}
}
spring-idol.xml
< bean id= "audience" class = "com.sa.Audience"/>
<aop:config >
<aop:aspect ref = "audience">
<aop:before
pointcut= "execution(* com.sa.Performer.perform(..))"
method= "takeSeats" />
<aop:before
pointcut= "execution(* com.sa.Performer.perform(..))"
method= "turnOffCellPhones" />
<aop:after-returning
pointcut= "execution(* com.sa.Performer.perform(..))"
method= "applaud" />
<aop:after-throwing
pointcut= "execution(* com.sa.Performer.perform(..))"
method= "demandRefund" />
</aop:aspect >
</aop:config >
可以定义一个命名切点以避免重复定义切点,然后使用pointcut-ref引用它
<aop:config >
<aop:aspect ref = "audience">
< aop:pointcut expression= "execution(* com.sa.Performer.perform(..))" id= "performance" />
<aop:before
pointcut-ref= "performance"
method= "takeSeats" />
...
4.3.2 声明环绕通知
使用环绕通知,可以完成 前置通知和后置通知实现相同的功能,但是只需要在一个方法中实现。因为整个通知逻辑是在一个方法内实现的,所以不需要使用成员变量保存状态
audience
public void watchPerformace(ProceedingJoinPoint joinpoint) {
try {
System. out.println( "The audience is taking their seats");
System. out.println( "The audience is turning off their phones");
long start = System. currentTimeMillis();
joinpoint.proceed();
long end = System. currentTimeMillis();
System. out.println( "PaPaPaPaPa");
System. out.println( "The performance took " + (end - start) + "milliseconds .");
} catch (Throwable t) {
System. out.println( "Boo! We want our money back !");
}
}
spring-idol.xml
< aop:config>
<aop:aspect ref = "audience">
<aop:pointcut expression = "execution(* com.sa.Performer.perform(..))" id= "performance2" />
<aop:around
pointcut-ref= "performance2"
method= "watchPerformance" />
</aop:aspect >
</aop:config >
- 使用ProceedingJoinPoint作为方法的入参(org.aspectj.lang.ProceedingJoinPoint),这个对象可以让我们在通知里调用被通知方法。调用ProceedingJoinPoint的proceed()方法可以把控制转给被通知方法。这里就相当于,前置通知结束时调用proceed来执行被通知方法。
- 如果不主动调用procedd()方法,通知会阻止被通知的方法的调用。
- 可以在通知里多次调用被通知的方法,以实现重试逻辑
4.3.3 为通知传递参数
spring-idol.xml
<aop:config>
<aop:aspect ref = "magician">
<aop:pointcut
id= "thinking"
expression= "execution(* com.sa.Thinker.thinkOfSomething(String)) and args(thoughts)" />
<aop:before
pointcut-ref= "thinking"
method= "interceptThought"
arg-names= "thoughts" />
</aop:aspect >
</aop:config >
在切点中指定参数,在切面中设置arg-names属性为切点中设定的参数,标识该参数必须传递给相应的方法。
4.3.4 通过切面引入新功能
“引入”,切面可以为Spring Bean添加新的方法
切面只是实现了它们所包装Bean的相同接口的代理,“引入”让代理还能发布新的接口,这样切面所通知的Bean看起来实现了新的接口,即便底层实现类并没有实现这些接口。
当引入接口的方法被调用时,代理将此调用委托给实现了新接口的某个其他对象。实际上,Bean的实现被拆分到了多个类。
spring-idol.xml
<aop:aspect>
<!-- 二选一 1.Bean方式 2.全限定类名-->
<aop:declare-parents
types-matching= "com.sa.Performer+"
implement-interface= "com.sa.Contestant"
delegate-ref= "contestantDelegate" />
<aop:declare-parents
types-matching= "com.sa.Performer+"
implement-interface= "com.sa.Contestant"
default-impl= "com.sa.GraciousContestant" />
</aop:aspect >
<aop:declare-parents>声明了此切面所通知的Bean在它的对象层次结构中拥有新的父类型。也就是:类型匹配types-matching属性指定接口的bean,会实现implement-interface属性指定的接口。
两种方式标识标识所引入接口的实现。
- 使用default-impl属性通过全限定类名来显式指定接口的实现。
- 使用delegate-ref属性来引用一个Spring Bean。
区别在于,delegate-ref引入了一个Spring Bean,本身可以被注入,被通知等等……
4.4 注解切面
AspectJ 5 引入,通常称为@AspectJ。
Audience
@Aspect
public class Audience {
@Pointcut( "execution(* com.sa.performer.perform(..))")
public void performance() {
;
}
@Before( "performance()")
public void takeSeats() {
System. out.println( "The audience is taking their seats");
}
@Before( "performance()")
public void turnOffCellPhones() {
System. out.println( "The audience is truning off their cellphones");
}
@AfterReturning( "performance()")
public void applaud() {
System. out.println( "papapapapapa");
}
@AfterThrowing( "performance()")
public void demandRefund() {
System. out.println( "Boo! We want our money back" );
}
}
@Pointcut 注解用于定义一个可以在@AspectJ切面内可重用的切点。其值是一个AspectJ切点表达式。
切点的名称来源于注解所应用的方法名称。
因为该类本身包含了所有它需要定义的切点和通知,所以不需要在XML中声明切点和通知。
我们需要在Spring上下文中声明一个自动代理Bean,该Bean直到如何把@AspectJ注解所标注的Bean转变为代理通知。Spring自带了名为AnnotationAwareAspectJAutoProxyCreator的自动代理创建类。我们可以在Spring上下文中把AnnotationAwareAspectJAutoProxyCreator类注册为一个Bean,为了简化这个操作,Spring在aop命名空间中提供了一个自定义的配置元素:
<aop:aspectj-autoproxy/>
<aop:aspectj-autoproxy/>在spring上下文中创建一个AnnotationAwareAspectJAutoProxyCreator类,它会自动代理一些Bean,这些Bean的方法需要与使用@Aspect注解的Bean中所定义的切点相匹配,而这些切点又是使用@Pointcut注解定义出来的。
要使用<aop:aspectj-autoproxy>配置元素,我们需要在Spring的配置文件中包含aop命名空间:
< 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/beans
http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.2.xsd" >
*<aop:aspectj-autoproxy>仅仅使用@AspectJ注解作为指引来创建基于代理的切面,但本质上它仍然是一个Spring风格的切面。这意味着:我们虽然使用@AspectJ的注解,但是我们仍然限于代理方法的调用。如果想利用AspectJ的所有能力,我们必须在运行时使用AspectJ并不依赖spring来创建基于代理的切面。
4.4.1 注解环绕通知
使用@Around创建环绕通知
@Around( "performance()")
public void watchPerformance(ProceedingJoinPoint joinpoint) {
try {
System. out.println( "The audience is taking their seats");
System. out.println( "The audience is turning off their phones");
long start = System. currentTimeMillis();
joinpoint.proceed();
long end = System. currentTimeMillis();
System. out.println( "PaPaPaPaPa");
System. out.println( "The performance took " + (end - start) + " milliseconds ." );
} catch (Throwable t) {
System. out.println( "Boo! We want our money back !");
}
}
4.4.2 传递参数给所标注的通知
@Aspect
public class Magician implements Mindreader {
private String thoughts;
@Pointcut( "execution(* com.sa.Thinker.thinkOfSomething(Sring)) "
+ "&& args(thoughts)" )
public void thinking(String thoughts) {
//null
}
@Before( "thinking(thoughts)")
public void interceptThought(String thoughts) {
System. out.println( "Intercepting volunteer‘s thoughts");
this. thoughts = thoughts;
}
public String getThoughts() {
return thoughts;
}
}
使用注解方式传递参数给所标注的通知的时候,不需要像XML方式那样显示指定一个“args-names”属性所对应的注解,@AspectJ能够依靠Java语法来判断为通知所传递参数的细节。
4.4.3 标注引入
@AspectJ的@DeclareParents对应于<aop:declare-parents>,在基于@AspectJ注解所标注的类内使用时,@DeclareParents工作方式与<aop:declare-parents>几乎相同。
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.DeclareParents;
@Aspect
public class ContestantIntroducer {
@DeclareParents(
value = "com.sa.Performer + " ,
defaultImpl = GraciousContestant. class)
public static Contestant contestant;
}
ContestantIntroducer是一个切面。这个切面没有提供通知。它为Performer Bean引入了Contestant接口。标注引入对应于<aop:declare-parents>引入的属性:
- value:等同于<aop:declare-parents>的types-matching属性。标识应该被引入指定接口的Bean的类型(也就是被引入的类)
- defaultImpl:等同与<aop:declare-parents>的default-impl属性。它标识该类提供了所引入接口的实现。
- 由@DeclareParents注解所标注的static属性指定了将被引入的接口
需要像其他切面一样,把@Aspect注解标注的切面声明为Spring上下文中的一个Bean。
Spring发现使用了@Aspect注解所标注的Bean时,<aop:aspectj-autoproxy>将自动创建单例。一句被调用的方式是属于被代理的Bean还是引入的接口,该代理把调用委托给被代理的Bean或引入的实现。
*@DeclareParents没有对应于<aop:declare-parents>的delegate-ref属性所对应的等价物。这是因为@DeclareParents是一个@AspectJ注解,是一个不同于Spring的项目,因此它的注解不了解Spring的Bean。这意味着:如果想委托给Spring所配置的Bean,@DeclareParents不能完成,只能使用<aop:declare-parents>
4.5 注入AspectJ切面
spring与aspectJ的配合:如果在执行通知时,切面依赖于一个或多个类,我们可以使用Spring的依赖注入把Bean装配仅AspectJ切面中。
JudgeAspect aspect
package com.sa;
public aspect JudgeAspect {
public JudgeAspect() {
}
pointcut performance():execution(* perform(..));
after() returning() :performance() {
System.out.println(criticismEngine.getCriticism());
}
//injected
private CriticismEngine criticismEngine;
public void setCriticismEngine(CriticismEngine criticismEngine) {
this.criticismEngine = criticismEngine;
}
}
CriticismEngineImpl
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;
}
}
spring-idol.xml
<bean id="criticismEngine"
class="com.sa.CriticismEngineImpl">
<property name="criticisms">
<list>
<value>I‘m not being rude, but that was appalling.</value>
<value>You may be the least talented person in this show.</value>
<value>Do everyone a favor and keep your day job.</value>
</list>
</property>
</bean>
AspectJ切面根本不需要Spring就可以织入应用,但是如果想使用Spring的依赖注入为AspectJ切面注入协作者,那么就需要在Spring配置中把切面声明为一个Spring Bean。
<bean class="com.sa.JudgeAspect"
facotry-method="aspectOf">
<property name="criticismEngine" ref="criticismEngine"/>
</bean>
通常情况下,Spring Bean由Spring容器初始化,但是AspectJ切面是由AspectJ在运行期创建的。当Spring有机会为JudgeAspect注入CriticismEngine时,JudgeAspect已经被实例化了。
因为Spring无法负责创建JudgeAspect,那就不能在Spring中将JudgeAspect普通的声明为一个Bean。我们需要一种方式为Spring获得已经由AspectJ创建的JudgeAspect实例的句柄,从而可以注入CriticismEngine。AspectJ切面都提供了一个静态的asepectOf()方法,返回切面的一个单例。所以为了获得切面的实例,我们必须使用factory-method来调用aspectOf方法来代替调用JudgeAspect的构造器方法。
总结:Spring不能使用<bean>声明来创建一个JudgeAspect实例——它已经在运行时由AspectJ创建了。spring通过aspectOf()工厂方法获得切面的引用,然后像<bean>元素规定的那样在该对象上执行依赖注入。
第4章总结
AOP有效减少了代码冗余,并让我们的类关注自身的主要功能。
以上是关于面向切面的Spring的主要内容,如果未能解决你的问题,请参考以下文章