内容简介:使用面向切面编程时,我们仍然在一个地方定义通用功能,但是可以通过声明的方式定义该功能要以何种方式在何处应用,而无需修改受影响的类。影响应用多处的功能(日志、事务、安全)增强定义了切面要完成的功能以及什么时候执行这个功能。
使用面向切面编程时,我们仍然在一个地方定义通用功能,但是可以通过声明的方式定义该功能要以何种方式在何处应用,而无需修改受影响的类。
术语
横切关注点
影响应用多处的功能(日志、事务、安全)
增强(Advice)
增强定义了切面要完成的功能以及什么时候执行这个功能。
Spring 切面可以应用 5 种类型的增强:
- 前置增强(Before) 在目标方法被调用前调用增强功能
- 后置增强(After) 在目标方法完成之后调用增强, 不关注方法输出是什么 。
- 返回增强(After-returning) 在目标方法成功执行之后调用增强
- 异常增强(After-throwing) 在目标方法抛出异常后调用增强
- 环绕增强(Around) 在被增强的方法调用之前和调用之后执行自定义行为,即包括前置增强和后置增强。
连接点(Join Point)
应用中每一个有可能会被增强的点被称为连接点。
切点(Pointcut)
切点是规则匹配出来的连接点。
切面(Aspect)
切面是增强和切点的结合,定义了在何时和何处完成其功能。
引入(Introduction)
引入允许我们向现有的类中添加新方法和属性。可以在不修改现有的类的情况下,让类具有新的行为和状态。
织入(Weaving)
织入是把切面应用到目标对象中并创建新的代理对象的过程。在目标对象的生命周期里有多个点可以进行织入:
- 编译器:切面在目标类编译时织入。这种方式需要特殊的编译器。AspectJ 的织入编译器就是以这种方式织入切面的。
- 类加载器:切面在目标类加载到 JVM 时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ5 的加载时织入(LTW)支持以这种方式织入。
- 运行期:切面在应用运行时的某个时刻被织入。一般情况下,在织入切面时,AOP 容器会为目标对象动态地创建一个代理对象。Spring AOP 就是以这种方式织入切面的。
Spring 对 AOP 的支持
Spring 对 AOP 的支持在很多方面借鉴了 AspectJ 项目。目前 Spring 提供了 4 种类型的 AOP 支持:
- 基于代理的经典 AOP
- 纯 POJO 切面
- @AspectJ 注解驱动的切面
- 注入式 AspectJ 切面
Spring AOP 构建在动态代理基础之上,因此 Spring 对 AOP 的支持局限于方法拦截。
运行时增强
通过在代理中包裹切面,Spring 在运行期把切面织入到 Spring 管理的 bean 中。代理类封装了目标类,并拦截被增强方法的调用,再把调用转发给真正的目标 bean。在代理拦截到方法调用时,在调用目标 bean 方法之前,会执行切面逻辑。
直到应用需要代理的 bean 时,Spring 才创建代理对象。如果使用 ApplicationContext
的话,在 ApplicationContext
从 BeanFactory
中加载所有 bean 的时候,Spring 才会创建被代理的对象。
方法级别的连接点
Spring 基于动态代理实现 AOP,所以 Spring 只支持方法连接点。其他的 AOP 框架比如 AspectJ 与 JBoss,都提供了字段和构造器接入点,允许创建细粒度的增强。
切点表达式
Spring AOP 中,使用 AspectJ 的切点表达式来定义切点。Spring 只支持 AspectJ 切点指示器(pointcut designator)的一个子集。
指示器
AspectJ 指示器 | 描述 |
---|---|
arg( ) | 限制连接点匹配参数为指定类型的执行方法 |
execution( ) | 用于匹配连接点 |
this | 指定匹配 AOP 代理的 bean 引用的类型 |
target | 指定匹配对象为特定的类 |
within( ) | 指定连接点匹配的类型 |
@annotation | 匹配带有指定注解的连接点 |
编写切点
package concert; public interface Performance { public void perform(); } 复制代码
Performance
类可以代表任何类型的现场表演,比如电影、舞台剧等。现在编写一个切点表达式来限定 perform() 方法执行时触发的增强。
execution(* concert.Performance.perform(..)) 复制代码
每个部分的意义如下图所示:
也可以引入其他注解对匹配规则做进一步限制。比如
execution(* concert.Performance.perform(..)) && within(concert.*) 复制代码
within()
指示器限制了切点仅匹配 concert 包。
Spring 还有一个 bean() 指示器,允许我们在切点表达式中使用 bean 的 ID 表示 bean。
execution(* concert.Performance.perform(..)) && bean('woodstock') 复制代码
以上的切点就表示限定切点的 bean 的 ID 为 woodstock
。
使用注解创建切面
定义切面
在一场演出之前,我们需要让观众将手机静音且就座,观众在表演之后鼓掌,在表演失败之后可以退票。在观众类中定义这些功能。
@Aspect public class Audience { @Pointcut("execution(* concert.Performance.perform(..)))") public void performance(){} @Before("performance()") public void silenceCellPhones() { System.out.println("Silencing cell phones"); } @Before("performance()") public void takeSeats() { System.out.println("Taking seats"); } @AfterReturning("performance()") public void applause() { System.out.println("CLAP CLAP CLAP!!!"); } @AfterThrowing("performance()") public void demandRefund() { System.out.println("Demanding a refund"); } } 复制代码
@AspectJ
注解表名了该类是一个切面。 @Pointcut
定义了一个类中可重用的切点,写切点表达式时,如果切点相同,可以重用该切点。 其余方法上的注解定义了增强被调用的时间,根据注解名可以知道具体调用时间。
到目前为止, Audience
仍然只是 Spring 容器中的一个 bean。即使使用了 AspectJ 注解,但是这些注解仍然不会解析,因为目前还缺乏代理的相关配置。
如果使用 JavaConfig,在配置类的类级别上使用 @EnableAspectJAutoProxy
注解启用自动代理功能。
@Configuration @EnableAspectJAutoProxy @ComponentScan public class ConcertConfig { @Bean public Audience audience() { return new Audience(); } } 复制代码
如果使用 xml ,那么需要引入 <aop:aspectj-autoproxy>
元素。
<?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="concert"/> <aop:aspectj-autoproxy/> <bean class="concert.Audience"/> </beans> 复制代码
环绕增强
环绕增强就像在一个增强方法中同时编写了前置增强和后置增强。
@Aspect public class Audience { @Pointcut("execution(* concert.Performance.perform(..)))") public void performance(){} @Around("performance()") public void watchPerformance(ProceedingJoinPoint joinPoint) { try { System.out.println("Silencing cell phones"); System.out.println("Taking seats"); joinPoint.proceed(); System.out.println("CLAP CLAP CLAP!!!"); } catch (Throwable throwable) { System.out.println("Demanding a refund"); } } } 复制代码
可以看到,这个增强达到的效果与分开写前置增强与后置增强是一样的,但是现在所有的功能都位于同一个方法内。 注意该方法接收 ProceedingJoinPoint
作为参数,这个对象必须要有,因为需要通过它来调用被增强的方法。 注意,在这个方法中,我们可以控制不调用 proceed()
方法,从而阻塞对增强方法的访问。同样,我们也可以在增强方法失败后,多次调用 proceed()
进行重试。
增强方法参数
修改 Perform#perform()
方法,添加参数
package concert; public interface Performance { public void perform(int audienceNumbers); } 复制代码
我们可以通过切点表达式来获取被增强方法中的参数。
@Pointcut("execution(* concert.Performance.perform(int)) && args(audienceNumbers)))") public void performance(int audienceNumbers){} 复制代码
注意,此时方法接收的参数为 int 型, args(audienceNumbers)
指定参数名为 audienceNumbers
,与切点方法签名中的参数匹配,该参数不一定与增强方法的参数名一致。
引入增强
切面不仅仅能够增强现有方法,也能为对象新增新的方法。 我们可以在代理中暴露新的接口,当引入接口的方法被调用时,代理会把此调用委托给实现了新接口的某个其他对象。实际上,就是一个 bean 的实现被拆分到多个类中了。 定义 Encoreable
接口,将其引入到 Performance
的实现类中。
public interface Encoreable { void performEncore(); } 复制代码
创建一个新的切面
@Aspect public class EncoreableIntroducer { @DeclareParents(value = "concert.Performance+",defaultImpl = DefaultEncoreable.class) public static Encoreable encoreable; } 复制代码
我们使用了 @Aspect
将 EncoreableIntroducer
标记为一个切面,但是它没有提供前置、后置或环绕增强。通过 @DeclareParents
注解将 Encoreable
接口引入到了 Performance bean
中。
@DeclareParents
注解由三部分组成:
- value 属性指定了哪种类型的 bean 要引入该接口。在上述代码中,类名后面的
+
号表示是Performance
的所有子类型,而不是它本身。 - defaultImpl 属性指定了为引入功能提供实现的类。
-
@DeclareParents
注解所标注的静态属性指明了要引入的接口。
同样地,我们在 Spring 应用中将该类声明为一个 bean:
<bean class="concert.EncoreableIntroducer" /> 复制代码
Spring 的自动代理机制将会获取到它的声明,并创建相应的代理。然后将调用委托给被代理的 bean 或者被引入的实现,具体取决于调用的方法属于被代理的 bean 还是属于被引入的接口。
在 XML 中声明切面
更新一下 Audience
类,将它的 AspectJ
注解全部移除。
public class Audience { public void silenceCellPhones() { System.out.println("Silencing cell phones"); } public void takeSeats() { System.out.println("Taking seats"); } public void applause() { System.out.println("CLAP CLAP CLAP!!!"); } public void demandRefund() { System.out.println("Demanding a refund"); } } 复制代码
声明前置与后置增强
<?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/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <aop:config> <aop:aspect ref="audience"> <aop:before pointcut="execution(* concert.Performance.perform(..))" method="silenceCellPhone"/> <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> </beans> 复制代码
如上所示,就将一个普通方法变为了增强。 大多数的 AOP 配置元素都必须在 <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/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <aop:config> <aop:aspect ref="audience"> <aop:pointcut id="performance" expression="execution(* concert.Performance.perform(..))"/> <aop:before pointcut-ref="performance" method="silenceCellPhone"/> <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> </beans> 复制代码
注意,此时 <aop:pointcut>
标签位于 <aop:aspect>
下层,故只能在该切面中引用。如果想要一个切点能够被多个切面引用,可以将 <aop:aspect>
元素放在 <aop:config>
下第一层。
环绕增强
定义环绕增强方法
public class Audience { public void performance(int audienceNumbers){} public void watchPerformance(ProceedingJoinPoint joinPoint) { try { System.out.println("Silencing cell phones"); System.out.println("Taking seats"); joinPoint.proceed(); System.out.println("CLAP CLAP CLAP!!!"); } catch (Throwable throwable) { System.out.println("Demanding a refund"); } } } 复制代码
在 xml 中使用 <aop:around>
指定方法名与切点即可。
<?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/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <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> </beans> 复制代码
为增强传递参数
获取参数主要就在于切点表达式。
<aop:pointcut id="performance" expression="execution(* concert.Performance.perform(int)) and args(audienceNumbers)"/> 复制代码
这样能在 xml 中定位到一个参数类型为 int ,参数名为 audienceNumbers 的切点。 注意在 xml 中使用了 and
代替 &&
(在 XML 中, &
符号会被解析为实体的开始)。
引入增强
<aop:declare-parents types-matching="concert.Performance+" implement-interface="concert.Encoreable" default-impl="concert.DefaultEncoreable"/> 复制代码
types-matching
指定了要匹配的类型,与注解中的 value 值功能相同。
注入 AspectJ 切面
AspectJ
切面提供了 Spring AOP
所不能支持的许多类型的切点。 切面很有可能依赖其他类来完成它们的工作。我们可以借助 Spring 的依赖注入把 bean 装配进 AspectJ
切面中。
创建一个新切面。
public aspect CriticAspect { private CriticismEngine criticismEngine; public CriticAspect() { } pointcut performance():execution(* perform(..)); afterReturning() : performance() { System.out.println(criticismEngine.getCriticism()); } public void setCriticismEngine(CriticismEngine criticismEngine) { this.criticismEngine = criticismEngine; } } 复制代码
注入的 CritismEngine
的实现类
public class CriticismEngineImple implements CriticismEngine { public CriticismEngineImple() { } public String getCriticism() { int i = (int) (Math.random() * criticismPool.length); return criticismPool[i]; } private String[] criticismPool; public void setCriticismPool(String[] criticismPool) { this.criticismPool = criticismPool; } } 复制代码
CriticAspect
主要作用是在表演结束后为表演发表评论。 实际上, CriticAspect
是调用了 CriticismEngine
的方法来发表评论。通过 setter
依赖注入为 CriticAspect
设置 CriticismEngine
。
在配置文件中将 CriticismEngine bean
注入到 CriticAspect
中。
<bean class="om.springinaction.springidol.CriticAspect" factory-method="aspectOf"> <property name="criticismEngine" ref="criticismEngine"/> </bean> 复制代码
一般情况下,Spring bean 由 Spring 容器初始化,但是 AspectJ 切面是由 AspectJ 在运行期创建的。所以在运行期间,AspectJ 创建好了 CriticAspect
实例,每个 AspectJ 都会提供一个静态的 aspectOf()
方法,返回切面的的单例。 使用 factory-method
调用 aspectOf()
方法向 CriticAspect
中注入 CriticismEngine
。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。