面向切面的Spring
本篇内容说说Spring对切面的支持,如何把普通类声明为一个切面,以及如何使用注解创建切面,主要有以下几点内容:
- 什么是面向切面编程
- 选择连接点
- 使用注解创建切面
- 在XML中声明切面
什么是面向切面编程
切面能帮助模块化横切关注点,横切关注点可以被描述为影响应用多处的功能。如图,直观呈现横切关注点的概念:
上图展现了一个被划分为模块的典型应用。每个模块的核心功能都是为特定业务领域提供服务,但是这些模块都需要类似的辅助功能,例如安全、事务管理、监控日志等。
使用面向切面编程时,在一个地方定义通用功能,可以通过声明的方式定义这个功能要以何种方式在何处应用,而无需修改受影响的类。横切关注点可以被模块化为特殊的类,这些类被称为切面。这样做有两个好处:首先,每个关注点都集中于一个地方,而不是分散到多处代码中;其次,服务模块更简洁,因为它们只包含主要关注点的代码,而次要关注点的代码被转移到切面中。
定义AOP术语
描述切面的常用术语有通知(advice)、切点(pointcut)和连接点(join point),这几个概念之间的关联如图所示:
通知(Advice)
通知定义了切面是什么以及何时使用。Spring切面可应用的通知有5种类型:
- 前置通知(Before):在目标方法被调用之前调用通知;
- 后置通知(After):在目标方法完成之后调用通知;
- 返回通知(After-returning):在目标方法成功执行之后调用通知;
- 异常通知(After-throwing):在目标方法抛出异常后调用通知;
- 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。
连接点(Join point)
连接点是在应用执行过程中能够插入切面的一个点,这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。
切点(Pointcut)
切点的定义会匹配通知所要织入的一个或多个连接点。通常是使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。
切面(Aspect)
切面是通知和切点的结合,通知和切点共同定义了切面的全部内容 —— 是什么,在何时、何处完成其功能。
引入(Introduction)
引入允许我们向现有的类添加新方法和属性。
织入(Weaving)
织入是把切面应用到目标对象并创建新的代理对象的过程。在目标对象的生命周期里有多个点可以进行织入:
- 编译期:切面在目标类编译时被织入,这种方式需要特殊的编译器。AspectJ的织入编译器就是以这种方式织入切面的。
- 类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前增强目标类的字节码。AspectJ 5的加载时织入(load-time weaving,LTW)就支持这种方式织入切面。
- 运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。Spring AOP就是以这种方式织入切面的。
Spring对AOP的支持
不是所有的AOP框架都是相同的,它们在连接点模型上可能有强弱之分,有些允许在字段修饰符级别应用通知,有些只支持方法调用相关的连接点。它们织入切面的方式和时机也有所不同。总的来说,创建切点来定义切面所织入的连接点是AOP框架的基本功能。
Spring提供了4种类型的AOP支持:
- 基于代理的经典Spring AOP;
- 纯POJO切面;
- @AspectJ注解驱动的切面;
- 注入式AspectJ切面
前三种都是Spring AOP实现的变体,Spring AOP构建在动态代理基础之上,因此,Spring对AOP的支持局限方法拦截。
Spring的经典AOP编程模型曾经非常棒,但现在Spring提供了更简洁和干净的面向切面编程方式。引入了简单的声明式AOP和基于注解的AOP,Spring经典的AOP看超来显得非常笨重和过于复杂,使用ProxyFactory Bean会让人感到繁琐。所以这里只写简单的声明式AOP和基于注解的AOP。
借助Spring的aop命名空间,可以将纯POJO转换为切面。这些POJO只是提供了满足切点条件时所要调用的方法,它需要XML配置。
Spring借鉴了AspectJ的切面,提供注解驱动的AOP。它不需要XML配置,它虽然是基于代理的AOP,但编程模型与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 | 限定匹配带有指定注解的连接点 |
只有execution指示器是实际执行匹配的,而其它的指示器都是用来限制匹配的。execution指示器是编写切点定义时使用最多的指示器。
编写切点
定义Performance接口作为切面的切点:
package concert;
public interface Performance {
public void perform();
}
现在展示一个切点表达式,这个表达式能够设置当perform()方法执行时触发通知的调用:
execution(* concert.Performance.perform(..))
使用execution()指示器选择Performance的perform()方法。方法表达式以“*”号开始,表明不关心方法返回值的类型。还指定了全限定类名和方法名,对于方法参数列表,使用两个点号(..)表明切点要选择任意的perform()方法,无论该方法的入参是什么。
假设需要配置的切点仅匹配concert包。在此场景下,可以使用within()指示器来限制匹配:
execution(* concert.Performance.perform(..)) && within(concert.*)
在切点中选择bean
除了上面所列的指示器外,Spring还引入了一个新的bean()指示器,它允许在切点表达式中使用bean的ID来标识bean。bean()使用bean ID或bean名称作为参数来限制切点只匹配的bean。
execution(* concert.Performance.perform(..)) and bean(‘dancer’)
还可以使用非操作为除了特定ID以外的其他bean应用通知:
execution(* concert.Performance.perform(..)) and !bean(‘dancer’)
使用注解创建切面
Aspect提供了五个注解来定义通知:
- @Before: 通知方法会在目标方法调用之前执行
- @After: 通知方法会在目标方法返回或抛出异常后调用
- @AfterReturning: 通知方法会在目标方法返回后调用
- @Around: 通知方法会将目标方法封装起来
- @AfterThrowing: 通知方法会在目标方法抛出异常后调用
示例代码
package asp; import com.alibaba.fastjson.JSONObject; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; @Aspect @Component public class RecordAspect { @Pointcut("execution(* asp.P30Phone.call(..))") public void excute() { } @Before("excute()") public void before(JoinPoint joinPoint) { // 打印请求入参 System.out.println("parameter:" + JSONObject.toJSONString(joinPoint.getArgs())); } @Around("excute()") public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { long beginTime = System.currentTimeMillis(); Object result = proceedingJoinPoint.proceed(); long endTime = System.currentTimeMillis(); System.out.println("result:"+ result+", execute times:" + (endTime - beginTime) + " ms"); return result; } @After("excute()") public void after(JoinPoint joinPoint) throws Throwable { System.out.println("after:" + joinPoint.toString()); } }
@Aspect定义一个切面,@Pointcut 定义命名的切点
package asp; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; @Configuration @ComponentScan @EnableAspectJAutoProxy public class AspConfig { }
@EnableAspectJAutoProxy 启动AspectJ自动代理
package asp; public interface HWPhone { String call(String user); }
package asp; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Component; @Component("p30Phone") @Qualifier("p30") public class P30Phone implements HWPhone { @Override public String call(String user) { System.out.println("使用 P30 手机呼叫..."+user); return "p30"; } }
package asp; import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class Main { public static void main(String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AspConfig.class); HWPhone hwPhone = (HWPhone) context.getBean("p30Phone"); hwPhone.call("xiaoming"); } }
运行结果:
parameter:["xiaoming"] 使用 P30 手机呼叫...xiaoming result:p30, execute times:51 ms after:execution(String asp.HWPhone.call(String))
在XML中声明切面
在Spring的aop命名空间中, 提供了多个元素用来在XML中声明切面
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> | 定义一个切点 |
示例代码
<?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-4.3.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd"> <bean id="p30Phone" class="asp.P30Phone"/> <bean id="recordAspect" class="asp.RecordAspect" /> <aop:config> <aop:aspect ref="recordAspect"> <aop:pointcut id="execute" expression="execution(* asp.P30Phone.call(..))"/> <aop:before pointcut-ref="execute" method="before" /> <aop:after pointcut-ref="execute" method="after" /> <aop:around pointcut-ref="execute" method="around" /> </aop:aspect> </aop:config> </beans>
package asp; import com.alibaba.fastjson.JSONObject; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.ProceedingJoinPoint; public class RecordAspect { public void before(JoinPoint joinPoint) { // 打印请求入参 System.out.println("parameter:" + JSONObject.toJSONString(joinPoint.getArgs())); } public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { long beginTime = System.currentTimeMillis(); Object result = proceedingJoinPoint.proceed(); long endTime = System.currentTimeMillis(); System.out.println("result:"+ result+", execute times:" + (endTime - beginTime) + " ms"); return result; } public void after(JoinPoint joinPoint) throws Throwable { System.out.println("after:" + joinPoint.toString()); } }
package asp; public interface HWPhone { String call(String user); }
package asp; public class P30Phone implements HWPhone { @Override public String call(String user) { System.out.println("使用 P30 手机呼叫..."+user); return "p30"; } }
package asp; import org.springframework.context.support.ClassPathXmlApplicationContext; public class Main { public static void main(String[] args) { ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("asp/application.xml"); HWPhone hwPhone = (HWPhone) context.getBean("p30Phone"); hwPhone.call("xiaoming"); } }
运行结果:
parameter:["xiaoming"] 使用 P30 手机呼叫...xiaoming result:p30, execute times:0 ms after:execution(String asp.HWPhone.call(String))
参考《Spring实战(第4版)》
相关文章