1

面向切面编程的Spring

  1. 面向切面编程的基本原理
  2. 通过POJO创建切面
  3. 使用@AspectJ注解
  4. 为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支持

  1. 基于代理的经典Spring AOP:已过时
  2. 纯POJO切面:借助Spring的aop命名空间,将纯POJO转换为切面。需要XMl配置,Spring AOP显式声明
  3. @Aspect注解的切面:Spring AOP提供注解声明切面,不需要XML配置。
  4. 注入式AspectJ切面:AOP需求超过简单的方法调用(如构造器或属性拦截),考虑使用AspectJ来实现切面。

Spring通知是Java编写的

Spring创建的通知都是标准Java编写。使用与普通Java开发的集成开发环境(IDE)来开发切面。定义通知所应用的切面可使用注解或XML配置。

Spring在运行时通知对象

通过在代理类中包裹切面,Spring在运行期把切面织入到Spring管理的bean中。
代理类封装了目标类,并拦截被通知方法的调用,再把调用转发给真正的目标bean。当代理拦截到方法调用时,在调用目标bean方法之前,会执行切面逻辑。如下图:

clipboard.png

因为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(返回值类型 包名.类名.方法名(方法参数类型))

  1. 包.* 表示该层包下的类 包.*.* 该层子包下的类 以此类推(包.*.*.* 等)
  2. 包..*表示该层下所有类,包括子孙类

多指示器操作符
&&(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>

roylion
204 声望25 粉丝

读书破万卷