Spring学习笔记-面向切面(AOP)-04
什么是面向切面编程
先大概了解一下部分术语
横切关注点:软件开发中,散布于多出的功能称为横切关注点(cross-cutting concern),简单的可以描述为可以影响应用多处的功能,比如日志、安全。
切面:切面能帮我们模块化横切关注点,如图所示,三个不同的模块,每个模块都是为特定业务服务,但是这些模块都需要类似的辅助功能,例如安全、事务管理。面向切面,可以使我们在一个地方定义通用功能,以声明的方式定义这个功能要以何种方式在何处使用,无需修改受影响的类,横切关注点可以被模块化为一个特殊的类,这些类被称为切面。
AOP带来的好处:
1、每个关注点集中于一个地方,而不是分散到多处代码。
2、服务模块更简洁,因为它们只包含主要关注点(或核心功能)的代码,次要关注点的代码被转移到切面中了。
AOP术语
常见术语:通知(advice)、切点(pointcut)、连接点(point)
1、通知(advice):切面的所做的工作被称为通知(什么时候做)
通知定义了切面是什么以及何时使用,描述切面要完成的工作和何时执行这个工作。
Spring切面可以应用5种类型的通知:
1、前置通知(Before):在目标方法被调用之前调用通知;
2、后置通知(After):在目标方法被调用之后调用通知;
3、返回通知(After-returning):在目标方法成功执行之后调用通知;
4、异常通知(After-throwing):在目标方法抛出异常后调用通知;
5、环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义行为。
2、连接点(Join point):插入切面的时机,在应用执行“过程中”能够插入切面的一个点(做的时机)
应用可能有无数的时机应用通知,这些时机被称为连接点。这个点可以使调用方法时,抛出异常时,甚至是修改一个字段时,切面代码可以利用这些连接点插入到应用的正常流程中。
3、切点(Poincut):插入切面的位置(在什么地方做)
明确要织入的一个或多个连接点,通常使用明确的类和方法名称,或正则表达式匹配类和方法名,来指定切点。
4、切面(Aspect):通知和切点的结合。(在什么时候什么地方做)
通知和切点共同定义了切面的全部内容-----它是什么,在什么时候何处完成其功能。
5、引入(Introduction):不修改仙有泪的情况下,向现有类引入新功能
引入允许我们向现有类添加新方法或属性。
6、织入(Weaving):把切面应用到目标对象,并创建新的代理对象的过程。
切面在指定的连接点被织入到目标对象中,在目标对象的生命周期中有多个点可织入:
1、编译期:切面在目标类编译时被织入,这种方式需要特殊的编译器,AspectJ的织入编译器就是以这种方式织入。
2、类加载期:切面在目标类加载到JVM中被织入,此方式需要特殊的类加载器(ClassLoader),其可以在目标类被引入应用之前增强该类的字节码。AspectJ 5的加载时织入(LTW)就是这种方式织入的。
3、运行期:切面在应用运行时的某个时刻被织入,一般情况下,织入切面时,AOP容器会为目标对象动态的创建一个代理对象,Spring AOP就是以这种方式织入的。
通过切点来选择连接点
Spring AOP中可以使用AspectJ切点表达式语言定义切点,如下为Spring AOP所支持的AspectJ切点指示器:
编写切点
首先写一个接口用于演示如何定义切点,如下接口可以代表任何类型的现场表演,如舞台剧、电影等。
public interface Performance { public void perform(); }
假设我们向编写上面接口的perform()方法触发通知,如下展示了一个接口表达式
execution()指示器选择Performance的perform方法,方法表达式以“ * ” 号开始,表示我们不关心方法的返回值类型,可以理解为一个通配符。
*号之后指定了全限定的类名和方法名,对于方法的参数列表使用2个点好(..)表示切点选择任意参数签名的perform()方法。
现在加入我们需要配置的切点仅匹配concert包,可使用within()指示器来限制匹配,如下图。
使用“&&”操作符连接execution()和within()指示器,形成“与”(AND)的关系,(也可以使用||(或)、!(非)标识操作,也可以使用and、or、not来代替)
在切点中选择bean
除了表4.1所列的指示器外,Spring还引入了一个新的bean()指示器,它允许我们在切点表达式中使用bean的ID来标识bean。bean()使用bean ID或bean名称作为参数来限制切点只匹配特定的bean。例如,考虑如下的切点:在这里,我们希望在执行Performance的perform()方法时应用通知,但限定bean的ID为woodstock。在某些场景下,限定切点为指定的bean或许很有意义,但我们还可以使用非操作为除了特定ID以外的其他bean应用通知:在此场景下,切面的通知会被编织到所有ID不为woodstock的bean中。
使用注解创建切面
使用我们定义的Performance接口,作为切面中切点的目标对象,然后开始使用AspecJ注解定义切面
定义切面
如下Audience类使用AspectJ注解定义了一个切面
@Aspect public class Audience { //前置通知 @Before("execution(** Performance.perform(..))") public void silenceCellPhones(){ System.out.println("表演开始,观众就坐(silenceCellPhones)"); } //前置通知 @Before("execution(** Performance.perform(..))") public void takeSeats(){ System.out.println("表演开始,手机调至静音(takeSeats)"); } //返回通知 @AfterReturning("execution(** Performance.perform(..))") public void applause(){ System.out.println("表演结束,并且很精彩,观众鼓掌喝彩(applause)"); } //异常通知 @AfterThrowing("execution(** Performance.perform)") public void demandRefund(){ System.out.println("表演失败,观众要求退款(demandRefund)"); } }
上面有多个重复的切点,可以使用@Pointcut注解进行优化,performance方法内容不重要,在这里的作用实际上只是一个标识,供@Pointcut注解依附,供其他通知注解使用,避免重复使用相同的切点表达式。
@Aspect public class Audience2 { @Pointcut("execution(** Performance.perform(..))") public void performance(){}; //前置通知 @Before("performance()") public void silenceCellPhones(){ System.out.println("表演开始,观众就坐(silenceCellPhones)"); } //前置通知 @Before("performance()") public void takeSeats(){ System.out.println("表演开始,手机调至静音(takeSeats)"); } //返回通知 @AfterReturning("performance()") public void applause(){ System.out.println("表演结束,并且很精彩,观众鼓掌喝彩(applause)"); } //异常通知 @AfterThrowing("execution(** Performance.perform)") public void demandRefund(){ System.out.println("表演失败,观众要求退款(demandRefund)"); } }
现在定义了切点,但是如果就此而已那么Audience只会是Spring容器的一个bean,并不会视为一个切面,不会被Spring解析,也不会创建将其转换为切面的代理,需要配置以启用。
JavaConfig方式:可在配置类级别上使用@EnableAspectJ-AutoProxy注解启用自动代理。
//JavaConfig配置类 @Configurable @EnableAspectJAutoProxy //启用自动代理 @ComponentScan //组件扫描 public class ConcertConfig { @Bean public Audience audience(){ return new Audience(); } }
XML方式:使用Spring aop命名空间中的<aop:aspectj-autoproxy>元素。
<!--启用AspectJ自动代理--> <aop:aspectj-autoproxy /> <!--声明Audience bean--> <bean class= "concert.Audience"/>
环绕通知
能让你所编写的逻辑将被通知的目标方法完全包装起来,就像同时在一个通知方法中编写前置和后置通知。如下代码,@Around注解表明watchPerformance()方法会作为performance()切点的环绕通知,其中通知方法接受一个ProceedingJoinPoint类型的参数,这个参数不许要提供,因为要在通知中通过它调用被通知的方法,在通知方法完成工作之后,需要调用ProceedingJoinPoint的oriceed()方法,否则这个通知会阻塞被通知方法(目标方法)的调用。
处理通知中的参数
目前为止,所有的切面都没有任何参数,除了环绕通知,在如下通知代码中,切点表达式匹配带参数的目标方法,其中&& args(trackNumber)表示传递给playTrack()方法的int类型参数也会传递到通知方法中,参数中的trackNumberca也与通知方法签名中的参数相匹配,此