面向切面编程的Spring
- 面向切面编程的基本原理
- 通过POJO创建切面
- 使用@AspectJ注解
- 为AspectJ切面注入依赖
面向切面编程
解决横切关注点与业务逻辑相分离
AOP术语
通知(Advice):切面的工作目标,定义切面执行的工作以及何时执行。如调用前、调用后等;通知共分为5类
前置通知(Before):在目标方法被调用之前调用通知功能;
后置通知(After):在目标方法完成之后后调用通知,此时不会关心方法的输出是什么;
返回通知(After-returning):在目标方法成功调用之后调用通知;
异常通知(After-throwing):在目标方法抛出异常之后调用通知
环绕通知(Aroud):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。
//AOP通知执行顺序 仅供参考
try {
try {
before(); //前置通知,目标方法前执行
around(); //环绕通知,环绕目标方法,目标方法前后执行
catch (Exception e) {
after(); //后置通知,最终执行
throw e; //继续抛出异常,执行异常通知
}
after();
afterReturning(); //返回通知,目标方法成功后执行
} catch (Exception e) {
afterThrowing(); //异常通知,方法抛出异常后执行
}
连接点(Join point):程序执行过程中能够插入切面的一个点。可以是调用方法时,抛出异常时,甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。
切点(Pointcut):切点的定义会匹配通知所要织入的一个或多个连接点。通常使用明确的类和方法名称,或利用正则表达式定义所匹配的类和方法名称来指定这些切点。有些AOP框架允许我们创建动态的切点,可以根据运行时的决策(比如方法的参数值)来决定是否应用通知。
切面(Aspect):切面是通知和切点的结合。通知和切点共同定义了切面的全部内容——它是什么,在何时和何处完成其功能。
引入(introduction):引入允许我们向现有的类添加新方法或属性。无需改变现有的类的情况下,让他们具有新的行为和状态。
织入(Weaving):织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。在目标对象的生命周期里有多个点可以进行织入。
编译器:切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ的织入编译器就是以这种方式织入切面的。
类加载器:切面在被目标类加载到JVM时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以再目标类被引入应用之前增强该目标类的字节码。AspectJ 5的加载时织入(load-time weaving,LTW)就支持以这种方式织入切面。
运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。Spring AOP就是以这种方式织入切面的。
Spring对AOP的支持
并非所有AOP框架都是相同的,她们在连接点模型上可能有强弱之分。有些允许在字段修饰符级别应用通知,而另一些只支持与方法调用相关的连接点。它们织入切面的方式和时机也有所不同。但是无论如何,创建切点来定义切面所织入的连接点是AOP框架的基本功能。
关于Spring AOP,Spring 和 AspectJ项目之间有大量的协作,Spring AOP在很多方面借鉴了AspectJ项目。
Spring 提供4种类型的AOP支持
- 基于代理的经典Spring AOP:已过时
- 纯POJO切面:借助Spring的aop命名空间,将纯POJO转换为切面。需要XMl配置,Spring AOP显式声明
- @Aspect注解的切面:Spring AOP提供注解声明切面,不需要XML配置。
- 注入式AspectJ切面:AOP需求超过简单的方法调用(如构造器或属性拦截),考虑使用AspectJ来实现切面。
Spring通知是Java编写的
Spring创建的通知都是标准Java编写。使用与普通Java开发的集成开发环境(IDE)来开发切面。定义通知所应用的切面可使用注解或XML配置。
Spring在运行时通知对象
通过在代理类中包裹切面,Spring在运行期把切面织入到Spring管理的bean中。
代理类封装了目标类,并拦截被通知方法的调用,再把调用转发给真正的目标bean。当代理拦截到方法调用时,在调用目标bean方法之前,会执行切面逻辑。如下图:
因为Spring运行时才创建代理对象,不需要特殊的编译器来织入Spring AOP的切面。
Spring只支持方法级别的连接点
通过支持各种AOP方案可以支持多种连接点模型。因为Spring基于动态代理,所以Spring只支持方法连接点,缺少字段、构造器连接点的支持,无法创建细粒度的通知。
通过切点来选择连接点
切点用于定位在何处应用切面的通知。通知和切点是切面的最基本元素,了解如何编写切点非常重要
Spring AOP 使用AspectJ的切点表达式语言来定义切点。但Spring仅支持AspectJ切点指示器(pointcut designdator)的一个子集,Spring是基于代理的,而某些切点表达式与基于代理的AOP无关。下表仅列出Spring AOP支持的表达式
AspectJ指示器 | 描述 |
---|---|
args() | 限制连接点匹配参数为指定类型的执行方法,既能限制运行时入参参数类型,又能传递参数 |
@args() | 限制连接点匹配参数由指定注解标注的执行方法 |
execution() | 用于匹配是连接点的执行方法 |
this() | 限制连接点匹配AOP代理的bean引用为指定类型的类,包括子类 |
target() | 限制连接点匹配目标对象为指定类型的类,包括子类 |
@target() | 限制连接点匹配的特定的执行对象,这些对象对应的类要有指定类型的注解 |
within() | 限制连接点匹配指定的类型 |
@within() | 限制连接点匹配指定注解所标注的类型 |
@annotation | 限定匹配带有指定注解的连接点 |
bean():Spring引入的bean()指示器,使用Bean ID或name作为参数限制只匹配特定的bean
在Spring 尝试使用AspectJ其他指示器时,将会抛出IllegalArgument-Exception异常。
上述Spring支持的指示器中,只有execution是实际执行匹配的,而其他指示器都是用来限制匹配的。
说明exection指示器是我们编写切点定义时最主要使用的指示器。在此基础上,使用其他指示器限制匹配的切点。
编写切点
execution表达式语法:
execution(返回值类型 包名.类名.方法名(方法参数类型))
- 包.* 表示该层包下的类 包.*.* 该层子包下的类 以此类推(包.*.*.* 等)
- 包..*表示该层下所有类,包括子孙类
多指示器操作符
&&(and) 与关系
||(or) 或关系
!(not) 非关系
使用注解创建切面
AspectJ 5 引入关键特性 使用注解创建切面,在这之前,编写AspectJ切面需学习一种Java语言扩展。
@Aspect | 声明切面 |
@Pointcut | 声明切点,应在空方法上定义,通过 方法名() 调用 |
@Before | 通知方法会在目标方法调用之前执行 |
@Around | 通知方法会将目标方法封装起来 |
@AfterReturning | 通知方法会在目标方法返回后调用 |
@AfterThrowing | 通知方法会在目标方法抛出异常后调用 |
@After | 通知方法会在目标方法返回或抛出异常后调用 |
在Java中开启切面
使用@Aspect声明切面,通过@Bean或@Compent将其注入Spring容器,并在配置类上使用@EnableAspectJAutoProxy开启自动代理功能。
在XML中开启切面
使用<bean>注入切面类,通过<aop:aspectj-autoproxy />开启自动代理功能。
注意,Spring的AspectJ自动代理仅仅使用@Aspect作为创建切面的指导,本质依然是SPring基于代理的切面。
这意味着,使用@Aspect注解,依然限于代理方法的调用。需利用AspectJ的所有能力,必须在运行时使用AspectJ并不依赖Spring创建切面。
Spring AOP实现引入功能
package cn;
@Compent
public class Perpon {
public void say() {
System.out.println("hello");
}
}
package cn;
public Interface Reader {
void read();
}
package cn;
public class ReaderImpl {
public void read() {
System.out.println("read《Spring In Action》");
}
}
@Aspect
public class ReaderIntroducer {
@DeclareParents(value = "cn.demo",
defaultImpl = ReaderImpl.class)
public Reader reader;
}
@Test
public void test() {
Person person = ctx.getBean(person,Person.class);
person.say();
}
@DeclareParents 注解所标注的静态属性指明了要引入的接口,接口中声明了要引入的方法。
属性 | 说明 |
---|---|
value | 指定引入该接口的bean类型,(标示符后可添加'+'表示其所有子类型,而非其本身) |
defaultImpl | 指定为引入功能提供实现的类。 |
在XML中声明切面
基于注解的配置优于基于Java的配置,基于Java的配置优于基于XML的配置。
需要声明切面,但又不能为通知类添加注解时,只能转向XML配置(如事务管理切面等第三方通知)
Spring 的AOP配置能以非侵入性方式声明切面
AOP配置元素 | 用途 |
---|---|
<aop:config> | 顶层的AOP配置元素,大多数的<aop:*>必须包含在<aop:config>元素内 |
<aop:aspect> | 定义一个切面 |
<aop:pointcut> | 定义一个切点 |
<aop:advisor> | 定义AOP通知器 |
<aop:before> | 定义AOP前置通知 |
<aop:around> | 定义AOP环绕通知 |
<aop:after-returning> | 定义AOP返回通知 |
<aop:after-throwing> | 定义AOP异常通知 |
<aop:after> | 定义AOP后置通知(不管被通知的方法是否执行成功) |
<aop:aspectj-autoproxy> | 启用@Aspect注解的切面 |
<aop:declare-parents> | 以透明的方式为被通知的对象引入额外的接口 |
注入AspectJ切面
package cn;
public aspect AspectJDemo {
pointcut say() : execution(* say(..))
before() : say() { System.out.println("before"); }
after() : say() { System.out.println("after"); }
after()returning() : say() { System.out.println("after returning"); }
after()throwing() : say() { System.out.println("after throwing"); }
private String name;
public void setName(String name) {
this.name = name;
}
}
具体请参考 [AspectJ 编译时织入(Compile Time Weaving, CTW)]
Spring 注入AspectJ切面
1 Spring为AspectJ切面注入属性,需先将AspectJ切面声明为Spring bean;
2 AspectJ切面织入与Spring无关,其实例由AspectJ在运行期创建,而不能由Spring容器初始化;
3 所有AspectJ切面提供静态方法aspectOf(),返回AspectJ创建的切面实例;
4 Spring可通过工厂模式将AspectJ切面注入Spring容器,实现依赖注入。
<bean class="cn.AspectJDemo" factory-method="aspectOf">
<property name="name" value="aspectJ">
</bean>
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。