面向切面的Spring
在软件开发中,散布于应用中多处的功能被称为横切关注点。通常来说,这些横切关注点从概念上是与应用的业务逻辑相分离的。把这些横切关注点与业务逻辑相分离正是面向切面编程(AOP)所要解决的问题。DI有助于应用对象之间的解耦,而AOP可以实现横切关注点与他们所影响的对象之间解耦。
AOP的术语
切面(Aspect)
横切关注点可以被模块化为特殊的类,这些类可以称为切面(aspect)。这样做有两个好处:首先,现在每个关注点都集中于一个地方,而不是分散到多处代码中;其次,服务模块更加简洁,因为它们主要关注业务代码,而次要关注的代码被移入切面中。
通知(Advice)
在AOP术语中,切面的工作就被称为通知。除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。Spring切面可以应用5种类型的通知:
- 前置通知(Before):在目标方法被调用之前调用通知功能
- 后置通知(After):在目标方法完成之后调用通知,不关心方法的输出是什么
- 返回通知(After-returning):在目标方法成功执行之后调用
- 异常通知(After-throwing):在目标方法抛出异常之后调用
- 环绕通知(Around):通知包裹被通知方法,在被通知的繁华调用之前和之后执行自定义的行为
连接点(Join point)
连接点是应用执行过程中能够插入切面的一个点,这个点可以是调用方法时,抛出异常时,甚至修改一个字段时。切点代码可以利用这些点插入到应用的正常流程中,并添加新的行为。
切点(Poincut)
一个切点并不需要通知应用的所有连接点,切点有助于缩小切面所通知的连接点的范围。切点的定义,我们需要使用明确的类和方法名称或者利用正则表达式来指定切点(切点表达式)
织入(Weaving)
织入是把切面应用到目标对象并创建新的代理的过程。切面在指定的连接点被织入到目标对象中。目标对象的生命周期你有多个阶段可以被织入:
- 编译期:切面在目标类编译阶段被织入。这种方式需要特殊的编译器。
- 类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器。
- 运行期:切面在应用运行的某个阶段被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态创建一个代理对象。
AspectJ这三种方式方式都支持,Spring AOP只支持在运行期织入切面。
引入(introduction)
AOP的作用就在于增强目标对象现有的属性或方法,而引入允许我们向现有的类中添加新方法或属性。
Spring 对AOP的支持
并不是所有AOP框架都是相同的,它们在连接点模型上可能有强弱之分,它们织入切面的方式和时机也会有不同。但是无论如何,创建切点来定义切面所织入的连接点是所有AOP框架的基本功能。Spring AOP构建在动态代理基础上,因此,Spring对AOP的支持局限于方法拦截,这是Spring作为AOP框架的局限性。
如果AOP的需求超过了简单的方法调用(如构造器或属性拦截),那么就需要考虑使用AspectJ来实现切面。
Spring只支持方法级别的连接点
Spring基于动态代理,所以Spring只支持方法级别的连接点,缺少对字段连接点的支持,无法让我们创建细颗粒度的通知,例如拦截对象字段的修改;而且它不支持构造器连接点,我们无法在bean创建的时候应用通知。
虽然方法拦截可以满足大部分的需求,但要拦截其他,就需要利用AspectJ来补充Spring AOP的功能。
通过切点来选择连接点
在Spring AOP中,要使用AspectJ的切点表达式语言来定义切点。AspectJ的切点指示器只有execution是实际用于执行匹配的,其他的只是限制匹配的。execution指示器是我们在编写切点表达式时最主要使用的。
编写切点
execution(返回类型 全限定的类.方法(参数))
例如: execution(* cn.lynu.Performance.perform(..))
表达式以"*"号开始,表明可以返回任意类型,然后我们使用全限定的类名和方法名,对于方法参数列表,我们使用两个点号(..)表明该方法可以使用任意入参
我们还可以使用限制的指示器来匹配,例如使用execution()和within()限制切点。使用的是“&&”操作符进行连接:
execution(* cn.lynu.Performance.perform(..) && within(cn.*))
类似的还可以使用“||”运算符表示或的关系,“!”运算符表示非的关系。
但是因为“&&”在XML中有特殊含义,Spring的XML配置里面使用切点可以使用and替代“&&”,or和not分别替代“||” 和“!”.
使用Java代码定义切面
使用AspectJ的@Aspect注解表明一个Jav类作为切面,这个类中的方法都可以使用注解来定义切面的具体行为。AspectJ使用5个注解来对应5中通知方式:
- @After 后置通知
- @Before 前置通知
- @AfterReturning 返回通知
- @AfterThrowing 异常通知
- @Around 环绕通知
所有的这些通知注解都可以使用一个切点表达式作为它的值。
@Aspect public class Audience { @Before("execution(* test04.Performance.perform(..))") public void silenceCellPhone() { System.out.println("将手机调置静音"); } @Before("execution(* test04.Performance.perform(..))") public void taskSeats() { System.out.println("观众就坐"); } @AfterReturning("execution(* test04.Performance.perform(..))") public void applause() { System.out.println("鼓掌"); } @AfterThrowing("execution(* test04.Performance.perform(..))") public void demandRefund() { System.out.println("表演失败,观众要求退款"); } }
如果所有的这些切点表达式都是相同的,我们可以使用@Pointcut注解定义个可重用的切点:
@Pointcut("execution(* test04.Performance.perform(..))") public void performance() {} @Before("performance()") public void silenceCellPhone() { System.out.println("将手机调置静音"); }
performance方法是一个空方法,其本身只是作为一个标识,供@Pointcut注解依附。其实这个已经是切面的Audience,我们依然可以像其他Java类那样使用它的方法,它的方法也可以独立地进行测试,这与其他Java类并没有什么不同。只是使用了@Aspect注解,并不会被视为切面,这些注解也不会解析,也不会转换为切面的代理,还需要启动自动代理功能。
如果使用的是JavaConfig的话,可以在配置类的类级别上使用@EnableAspectJAutoProxy注解启用:
@Configuration @EnableAspectJAutoProxy @ComponentScan(basePackageClasses= {Performance.class}) public class Config { @Bean public Audience audience() { return new Audience(); } }
如果使用XML来装配bean,就需要使用aop命名空间的<aop:aspect-autoproxy>元素
<!--启用AspectJ自动代理-->
<aop:aspectj-autoproxy />
<bean class="cn.Audience"/>
不要忘了将切面声明为一个Spring bean,不论是用JavaConfig还是XML。
虽然我们使用了AspectJ的注解来创建切面,但是这个切面依然是基于代理的,它依然是Spring基于代理的切面,仍然受限于代理方法的调用,并不能利用AspectJ所有的能力。
接下来,我们可以测试这个切面的效果了:
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes= {Config.class}) public class Test { @Autowired private Performance performance; @org.junit.Test public void test01() { performance.perform(); } }
创建环绕通知
环绕通知是最为强大的通知类型,它能够让所编写的逻辑将被通知的目标方法完全包装起来,事实上就像在一个通知方法中同时编写前置和后置通知。
@Aspect public class Audience { @Pointcut("execution(* test04.Performance.perform(..))") public void performance() {} //环绕通知 @Around("performance()") public void watchPerformance(ProceedingJoinPoint pj) { try { System.out.println("关闭手机"); System.out.println("就坐"); //调用被通知方法 pj.proceed(); System.out.println("鼓掌"); } catch (Throwable e) { System.out.println("表演失败,观众要求退款"); } } }
注意环绕通知方法的参数是ProceedingJoinPoint作为入参的,这个对象是必须的,因为需要在环绕通知方法中通过它来调用目标方法,使用的是它的proceed()方法,不要忘记调用这个方法,如果不调这个方法,则会阻塞被通知方法的调用。
处理通知中的参数
切面所通知的法拉伐确实有参数该怎么办?如何在切面中访问和使用传递给被通知方法的参数?
我们换一个有参数的切点,并改造切点表达式:
@Pointcut("execution(* cn.lynu.CompactDisc.playTrack(int)) && args(trackNumber)") public void trackPlayed(int trackNumber){} @Before("trackPlayed(trackNumber)") public void before(int trackNumber){ system.out.print(trackNumber); }
被通知的方法入参是int类型,并使用args限制器,参数的名称是与切点方法签名中的参数名相匹配的。这样一来,就可以在通知方法中使用传递给切点方法的参数了
通过注解引入新功能
之前,我们一直是为目标对象以拥有的方法添加新功能,实际上,利用引入的概念,AOP可以为对象添加新的方法。在Spring中,切面只是实现了它们所包裹的bean所现有接口的代理。如果这些代理可以暴露新的接口,那么目标类看起来也实现了新的接口,即使底层实现类并没有实现这些接口。但调用这个新引入的方法时,代理会把调用传递给实现了新接口的某个对象。
@Aspect public class EncoreableIntroducer { @DeclareParents(value="test04.Performance+",defaultImpl=DefaultEncorable.class) public static Encoreable encoreable; }
通过@DeclareParents注解,将Encoredable接口引入到Performance bean中。这个注解有三个部分组成:value属性指定了哪种类型bean要引入该接口,标记符后面的加号表示是Performance的所有子类型,而不是Performance本身。defaultImpl属性指定为引入功能提供实现的类。注解所标注的静态属性指明了要引入的接口。
接下来,我们可以测试调用这个引入的新方法:
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes= {Config.class}) public class Test { @Autowired private Performance performance; @org.junit.Test public void test01() { //需要先强转为引入的接口类型,再调用新方法 Encoreable encoreable=(Encoreable) performance; encoreable.performEncore(); } }
运行之后,可以正常使用这个新方法,如果不通过引入的方法,直接强转会出现ClassCaseException。
使用注解的方式真的太方便了,但是这种方式由一个明显的缺点:必须可以看到和修改源码。如果没有源码,或不想将AspectJ的注解放在代码中,我们就需要使用XML的方式。
在XML中声明切面
在Spring aop命名空间中,提供了多个元素用在XML中声明切面:
AOP配置元素 | 用途 |
<aop:advisor> | 定义AOP通知器 |
<aop:after> | 定义AOP后置通知 |
<aop:after-returning> | 定义AOP返回通知 |
<aop:after-throwing> | 定义AOP异常通知 |
<aop:around> | 定义环绕通知 |
<aop:aspect> | 定义切面 |
<aop:before> | 定义前置通知 |
<aop:aspectj-autoproxy> | 这个之前就见过,是为启用@Aspec注解 |
<aop:config> | 顶层到的AOP配置,大多数<aop:*>元素必须包裹在<aop:config>元素内 |
<aop:declare-parents> | 引入 |
<aop:pointcut> | 定义切点 |
<bean id="audience" class="test04.Audience"></bean> <aop:config> <aop:aspect ref="audience"> <aop:pointcut expression="execution(* test04.Performance.perform(..))" id="pointcut"/> <aop:before pointcut-ref="pointcut" method="silenceCellPhone"/> <aop:before pointcut-ref="pointcut" method="taskSeats"/> <aop:after-returning pointcut-ref="pointcut" method="applause"/> <aop:after-throwing pointcut-ref="pointcut" method="demandRefund"/> <aop:around pointcut-ref="pointcut" method="watchPerformance"/> </aop:aspect> </aop:config>
关于Spring AOP配置元素,注意的是大多数AOP配置元素必须在<aop:config>元素上下文内使用.这里使用<aop:pointcut>将相同的切点抽取出来,如果通知的切点不一致,在通知中使用pointcut属性而不是pointcut-ref。<aop:pointcut>元素还可以放在<aop:config>元素范围内,提供其他切面使用。
为通知传递参数
在AspectJ注解的方式中,我们可以获得目标方法的参数,使用XML的方式也可以:
<aop:pointcut expression="execution(* test04.CompactDisc.playTrack(int)) and args(trackNumber)" id="pointcut"/>
只不过在XML中用and or not表示与或非,而不是&& || !
在XML中引入新功能
AspectJ中使用的是@DeclareParents注解,在XML中对应的就是Spring aop命名空间中的<aop:declare-parents>元素:
<bean id="audience" class="test04.Audience"></bean> <aop:config> <aop:aspect ref="audience"> <aop:declare-parents types-matching="test04.Performance+" implement-interface="test04.Encoreable" delegate-ref="myPerformance"/> </aop:aspect> </aop:config>
最后再说一点,相比较AspectJ,SpringAOP只局限与对方法的增强,AOP的功能较弱,如果对于构造器,属性等类型的切点,就需要直接使用AspectJ。