第四章 面向切面编程java
面向切面编程的基本原理正则表达式
经过POJO建立切面spring
使用@AspectJ注解express
为AspectJ切面注入依赖编程
AspectJ是一个面向切面的框架,它扩展了java语言。AspectJ定义了AOP语法,它有一个专门的编译器用来生成遵循java字节编码规范的Class文件。缓存
在第2章,咱们介绍了如何使用依赖注入(DI)管理和配置咱们的应用对象。DI有助于应用对象之间的解耦,而AOP能够实现横切关注点与它们所影响的对象之间的解耦。安全
日志是应用切面的常见范例,但它并非切面适用的惟一场景。经过本书,咱们还会看到切面锁适用的多个场景,包括声明式事务、安全和缓存。app
本章展现了Spring对切面的支持,包括如何把普通类声明为一个切面和如何使用注解建立切面。除此以外,咱们还会看到AspectJ,另外一种流行的AOP实现,如何补充spring AOP框架的功能。可是,咱们先无论事务、安全和缓存,先看一下spring是如何实现切面的,就从AOP的基础知识开始提及。框架
1、什么是面向切面编程模块化
如前所述,切面能帮助咱们模块化横切关注点。简而言之,横切关注点能够被,描述为影响应用多处的功能。例如,安全就是一个横切关注点,应用中的许多方法都会涉及到安全规则,以下图直观呈现了横切关注点的概念。
图中展现了一个被划分为模块的典型应用。每一个模块的核心功能都是为特定业务领域提供服务,若是在整个应用中都使用相同的基类,继承每每致使一个脆弱的对象提醒;而使用委托可能须要对委托对象进行复杂的调用。
切面提供了取代继承和委托的另外一种可选方案,并且在不少场景下更清晰简洁。在使用面向切面编程时,咱们仍然在一个地方定义通用功能。横切关注点能够被模块化为特殊的类,这些类被称为切面(aspect)。这样作有两个好处:首先,如今每一个关注点都集中在一个地方,而不是分散到多处代码中;其次,服务模块功能简洁,由于它们只包含主要关注点(或核心功能)的代码,而次要关注点的代码被移到切面中。
一、定义AOP术语
AOP已经造成了本身的术语,描述切面的经常使用术语有通知(advice)、切点(pointcut)和链接点(join point)。下图展现了这些概念是如何关联在一块儿的。
通知(advice)
在AOP术语中,切面的工做被称为通知,①描述切面要完成工做,②什么时候执行这个工资。
Spring切面能够应用5种类型的通知:
链接点(join point)
应用中可能有数以千计的时机应用通知,这些时机被称为链接点。链接点是在应用执行过程当中可以插入切面的一个点。这个点能够是在调用方法时、抛出异常时、甚至修改一个字段时。切面代码能够利用这些点插入到应用的正常流程之中,并添加新的行为。
切点(pointcut)
切点有助于缩小切面所通知的链接点的范围。
切点的定义会匹配通知所要织入的一个或多个链接点,咱们一般使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。有些AOP框架容许咱们建立动态的切点,能够根据运行时的决策来决定是否应用通知。
切面(Aspect)
切面是通知和切点的集合。通知和切点共同定义了切面的所有内容,它是什么,在什么时候何处完成其功能。
引入(Introduction)
引入容许咱们向现有的类添加新方法或属性。
织入(Weaving)
织入是吧切面应用到目标对象并建立新的代理对象的过程。切面在指定的链接点被织入到目标对象中,在目标对象的生命周期里有多个点能够进行织入:
二、spring对AOP的支持
Spring提供了4种类型的AOP支持:
spring通知是java写的,定义通知所应用的切点一般会使用注解或在spring配置文件里采用XML来编写。
AspectJ与之相反。虽然AspectJ如今支持基于注解的切面,但AspectJ最初是以JAVA语言扩展的方式实现的。经过特有的AOP语言,咱们能够得到更强大的细粒度的控制以及更丰富的AOP工具类,可是咱们须要额外学习新的工具和语法。
spring在运行时通知对象
经过在代理类中包裹切面,spring在运行期把切面织入到spring管理的bean中。以下图所示,代理类封装了目标类,并拦截通知方法的调用,再把调用转发给真正的目标bean。当大力拦截到方法调用时,在调用目标bean方法以前,会执行切面逻辑。
直到应用须要被代理的bean时,spring才建立对象。若是使用的是ApplicationContext的话,在ApplicationContext从beanFactory中加载全部bean的时候,spring才会建立被代理的对象。由于spring运行时才建立代理对象,因此咱们须要特殊的编译器来织入spring AOP的切面。
spring只支持方法级别的链接点
2、经过切点来选择链接点
在spring AOP中,要使用AspectJ的切点表达式语言来定义切点。若是你已经很熟悉AspectJ,那么在spring中定义切点就感受很是天然。可是若是你一点都不了解AspectJ的话,本小节咱们将快速介绍一下如何编写AspectJ风格的切点。
关于spring AOP的AspectJ切点,最重要的一点就是spring仅支持AspectJ切点指示器(pointcut designator)的一个子集。
下表列出了spring AOP所支持的AspectJ切点指示器。
AspectJ指示器 | 描述 |
arg() | 限制链接点匹配参数为指定类型的执行方法 |
@args() | 限制链接点匹配参数由指定注解标注的执行方法 |
execution | 用于匹配链接点的执行方法 |
this() | 限制链接点匹配AOP代理的bean引用为指定类型的类 |
target | 限制链接点匹配目标对象为指定类型的类 |
@target() | 限制链接点匹配特定的执行对象,这些对象对应的类要具备特定类型的注解 |
within() | 限制链接点匹配指定的类型 |
@within() | 限制链接点匹配指定注解所标注的类型(当使用spring AOP时,方法定义在由指定的注解所标注的类里) |
@annotation | 限定匹配带有指定注解的链接点 |
在spring中尝试使用AspectJ其余指示器时,将会抛出IllegalArgument-Exception异常。
当咱们查看如上所展现的这些spring支持的指示器时,注意只有execution指示器时实际执行匹配的,而其它的指示器都是用来限制匹配的。这说明execution指示器时咱们在编写切面定义时最主要使用的指示器。在此基础上,咱们使用其它指示器来限制所匹配的切点。
一、编写切点
为了阐述spring中的切面,咱们须要有个主题来定义切面的切点。为此,咱们定义一个Performance接口:
package oschina; public interface Performance{ public void perform(); }
Performance能够表明任何类型的现场表演,如舞台剧、电影或音乐会。假设咱们想编写Performance的perform()方法触发的通知。以下图展现了一个切点表达式,这个表达式可以设置当perform()方法执行时触发通知的调用。
咱们使用execution()指示器选择Performance的perform()方法。方法表达式以“*”号开始,代表了咱们不关心方法返回值的类型。而后,咱们指定了全限定类名和方法名。对于方法参数列表,咱们使用两个点号(..)代表切点要选择任意的perform()方法,不管该方法的入参是什么。
如今假设咱们须要配置的切点仅匹配concert包。在此场景下,可使用within()指示器来限制匹配,以下图所示:
请注意咱们使用了“&&”操做符把execution()和within()指示器链接在一块儿造成与(and)关系(切点必须匹配全部的指示器)。相似地,咱们可使用“||”操做符来标识或(or)关系,而使用“!”操做符来标识非(not)操做。
由于“&”在XML中有特殊含义,因此在Spring的XML配置里面描述切点时,咱们使用and来代替“&&”。一样,not和or分别带图“!”和“||”。
二、在切点中选择bean
除了上述介绍的指示器外,spring还引入了一个新的bean()指示器,它容许咱们在切点表达式中使用bean的ID来标识bean。bean()使用bean ID或bean名称做为参数来限制切点只匹配特定的bean。
例如,考虑以下的切点:
execution(* concert.Performance.perform()) and bean('woodstock')
在此场景下,切面的通知会被编织到全部ID不为Woodstock的bean中。
3、使用注解建立切面
使用注解来建立切面是AspectJ5引入的关键特性。AspectJ5以前,编写AspectJ切面须要学习一种java语言的扩展,可是AspectJ面向注解的模型能够很是简便的经过少许注解把任意类转变为切面。
咱们已经定义了Performance()接口,它是切面中切点的目标对象。如今,让咱们使用AspectJ注解来定义切面。
一、定义切面
package oschina; @Aspect public class Audience{ //表演以前 @Before("execution(** concert.Performance.perform(..))") public void silenceCellPhones(){ System.out.println("silencing cell phones"); } //表演以前 @Before("execution(** concert.Performance.perform(..))") public void takeSeats(){ System.out.println("taking seats"); } //表演以后 @AfterReturning("execution(** concert.Performance.perform(..))") public void applause(){ System.out.println("CLAP CLAP CLAP!!!"); } //表演失败以后 @AfterThrowing("execution(** concert.Performance.perform(..))") public void demandRefund(){ System.out.println("Demanding a refund"); } }
Audience类使用@AspectJ注解进行了标注。该注解声明Audience不只仅是一个POJO,仍是一个切面。Audience类中的方法都使用注解来定义切面的具体行为。
Audience有四个方法,定义了一个观众在观看演出时可能会作的事情。在演出以前,观众要就坐(takeSeats())并将手机调至静音状态silenceCellPhones()。若是演出很精彩的话,观众应该会鼓掌喝彩applause()。不过演出没有达到预期的话,观众会要求退票demandRefund()。
能够看到,这些方法都使用了通知注解来代表他们应该在何时调用。AspectJ提供了五个注解来定义通知,以下图所示:
spring使用AspectJ注解来声明通知方法
注解 | 通知 |
@After | 通知方法会在目标方法返回或抛出异常后调用 |
@AfterReturning | 通知方法会在目标方法返回后调用 |
@AfterThrowing | 通知方法会在目标方法抛出异常后调用 |
@Around | 通知方法会将目标方法封装起来 |
@Before | 通知方法会在目标方法调用以前执行 |
Audience使用到了前面五个注解中的三个。
相同的切点表达式咱们重复写了四遍,这可不是什么光彩的事情。
经过@Pointcut注解声明频繁使用的切点表达式
package oschina; @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"); } }
在Audience中,performance()方法使用了@Pointcut注解。为@Pointcut注解设置的值是一个切点表达式,就像以前在通知注解上所设置的那样。经过在performance()方法上添加@Pointcut注解,咱们实际上扩展了切点表达式语言,这样就能够在任何的切点表达式中使用performance()了。
须要注意的是,除了注解和没有实际操做的performance()方法,Audience类依然是一个POJO。咱们可以像其余的Java类那样调用它的方法,它的方法可以进行独立的单元测试。
二、建立环绕通知
环绕通知是最为强大的通知类型。它可以让你所编写的逻辑将通知的目标方法彻底包装起来。实际上就像在一个通知方法中同时编写前置通知和后置通知。
为了阐述环绕通知,咱们重写Autience切面。
@Aspect public class Audience{ //定义命名的切点 @Pointcut("execution(** concert.Performance.perform(..))") public void performance(){} //环绕通知方法 @Around("performance()") public void watchPerformance(ProceedingJoinPoint jp){ try{ System.out.println("Silencing cell phones"); System.out.println("Taking seats"); jp.proceed(); System.out.println("CLAP CLAP CLAP!!!"); }catch(Exception e){ System.out.println("Demanding a refund"); } } }
在这里,@Around注解声明watchPerformance()方法会做为Performance()切点的环绕通知。这个通知所达到的效果和前置通知和后置通知是同样的。
三、处理通知中的参数
四、经过注解引入新功能
当引入接口的方法被调用时,代理会把此调用委托给实现了新接口的某个其它对象,实际上,一个bean的实现被拆分到多个类中。
借助于AOP的引入功能,为了实现该功能,咱们要建立一个新的切面:
package oschina; @Aspect public class EncoreableIntroducer{ @DeclareParents(value="concert.Performance+",defaultImpl=DefaultEncoreable.class) public static Encoreable encoreable; }
能够看到EncoreableIntroducer是一个切面,经过@DeclareParents注解,将Encoreable接口引入到Performance bean中。
@DeclareParents注解由三部分组成:
和其它切面同样,咱们须要在spring应用中将EncoreableIntroducer声明为一个bean:
<bean class="concert.EncoreableIntroducer" />
spring的自动代理机制将会得到它的声明,当spring发现一个bean使用了@Aspect注解时,spring就会建立一个代理,而后将调用委托给被代理的bean或被引入的实现,这取决于调用的方法属于被代理的bean仍是属于被引入的接口。
4、在XML中声明切面
基于注解的配置优于基于java的配置,基于java的配置优于基于XML的配置。
在spring的AOP命名空间中,提供了多个元素用来在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:config>元素内 |
<aop:declare-parents> | 以透明的方式为被通知的对象引入额外的接口 |
<aop:pointcut> | 定义一个切点 |
咱们已经看到了<aop:aspectj-autoproxy>元素,它可以自动代理AspectJ注解的通知类。aop命名空间的其它元素可以让咱们直接在spring配置中声明切面,而不须要使用注解。
一、声明前置和后置通知
<aop:config> <aop:aspect ref="audience"> --引用audience bean <aop:before pointcut="execution(** concert.Performance.perform(..))" method="silencePhones" /> <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>
关于Spring AOP配置元素,第一个须要注意的事项是大多数的AOP配置元素必须在<aop:config>元素的上下文内使用。
在全部的通知元素中,pointcut属性定义了通知所应用的切点,它的值是使用AspectJ切点表达式语法锁定义的切点。
二、声明环绕通知
使用环绕通知能够完成前置通知和后置通知所实现的相同功能,并且只须要在一个方法中。
watchPerformance()方法提供了AOP环绕通知:
package oschina; public class Audience{ public void watchPerformance(ProceedingJoinPoint jp){ try{ System.out.println("Silencing cell phone"); System.out.println("Taking seats"); jp.proceed(); }catch(Exception e){ System.out.println("Demanding a refund"); } } }
watchPerformance()方法中包含了四个通知方法的全部功能。
在XML中使用<aop:around>元素声明环绕通知:
<aop:config> <aop:aspect ref="audience"> --引用audience bean <aop:before pointcut id="perfoemance" expression="execution(** concert.Performance.perform(..))" /> <aop:around pointcut-ref="performance" method="watchPerformance" /> </aop:aspect> </aop:config>
像其余通知的XML元素同样,<aop:around>指定了一个切点和一个通知方法的名字。在这里,咱们使用跟以前同样的切点,可是为该切点所设置的method属性值为watchPerformance()。
三、为通知传递参数
四、经过切面引入新的功能
五、注入AspectJ切面
public aspect CriticAspect{ public CriticAspect(){} pointcut perfoemance():execution(* perfoem(...)); afterReturning():performance(){ System.out.println(criticismEngine.getCriticism); } private CriticismEngine(CriticismEngine criticismEngine){ this.criticismEngine = criticismEngine; } }
CriticAspect的主要职责是在表演结束后为表演发表评论。performance()切点匹配perform()方法。当它与afterReturning()通知一块儿配合使用时,咱们可让该切面在表演结束时起做用。
六、小结
AOP是面向对象编程的一个强大扩充,经过AspectJ,咱们如今能够把以前分散在应用各处的行为放入可重用的模板中,咱们显示的声明在何处如何应用该行为,这有效的减小了代码冗余,并让咱们额类关注自身的主要功能。
spring提供了一个AOP框架,让咱们把切面插入到方法执行的周围,咱们如今已经学会如何把通知织入前置、后置和环绕方法的调用中,以及处理异常增长自定义的行为。
关于spring应用中如何使用切面,咱们能够有多种选择。经过使用@AspectJ注解和简化的配置命名空间,在spring中装配通知和切点变得很是简单。
咱们了解了如何使用spring为AspectJ切面注入依赖。