# Spring AOP

# AOP 基本概念

现实中有一些内容并不是面向对象技术(OOP)可以解决的,比如事务处理。在 JDBC 代码中,最繁琐的问题就是无穷无尽的 try ... catch ... finally ... 语句和『数据库资源关闭』的问题,而且代码会存在大量的重复,而你又不能不写。

一个正常执行的 SQL 的逻辑步骤如下:

# 操作
1 打开通过数据库连接池获得数据库链接资源,并做一定的设置工作。
2 执行对应的 SQL 语句(通常是增删改),对数据进行操作。
3 如果 SQL 执行过程中发生异常,回滚事务。
4 如果 SQL 执行过程中没有发生异常,最后提交事物。
5 到最后的阶段,需要关闭一些连接资源。

参看上述流程,你会发现无论是执行什么具体的 SQL,流程都是一样的!即,到了特定时刻一定会执行某个特定操作,并不因所执行的 SQL 的不同而不同 !

在 OOP 中,模块化单元是『(Class),而在 AOP 中,模块化的单元是『切面(Aspect)

AOP 的概念并非 Spring 首先提出。AOP 的诞生远比 Spring 要早。AOP 最早由名为『AOP 联盟』的组织提出的,并制定了一套规范。Spring AOP 遵守 AOP 联盟所制定的规范。

Spring 的 AOP 的底层用到两种代理机制:

  1. JDK 动态代理

    如果目标类遵循某个接口,Spring AOP 底层采用 JDK 方案生成代理对象

  2. CGLib 动态代理

    如果目标类不遵循任何接口,Spring AOP 底层采用 CGLib 方案生成代理对象。

关于 Spring AOP 的底层实现原理后续有专门章节讲解。

# 核心概念

AOP 涉及到如下问题:在 什么类什么方法什么地方,做出 什么样 的增强。

AOP 的功能简而言之就是:在不修改方法源文件的情况下,为源文件的特定部位增加新的代码

# 切入点表达式

切入点表达式决定了哪些类的哪些方法会被『插入』新代码。它『回答』了:要对『什么类』的『什么方法』做出增强。

最常用的切入点表达式是 execution 表达式,其语法格式如下:

[方法访问修饰符] 方法返回值 包名.类名.方法名(方法的参数)
  • 方法访问修饰符 部分是可选部分;

  • 其它部分是『必要』部分。

例如:

execution( * xxx.yyy.zzz.dao.EmployeeDao.*(..) )

execution( public * xxx.yyy.zzz.dao.EmployeeDao.*(..) )

execution( public String xxx.yyy.zzz.dao.EmployeeDao.*(..) )

execution( public String xxx.yyy.zzz.dao.EmployeeDao.*(String, ..) )

execution( * xxx.yyy.zzz.dao.*.*(..) )
  • 返回值匹配:

    可以为 *,表示任何返回值,全路径的类名等。

  • 方法名匹配:

    指定方法名。

    例如:* 代表所有方法;set*,代表以 set 开头的所有方法.

  • 参数匹配:

    指定方法参数,包括数量、类型及顺序。

    例如:(..) 代表所有参数;(*) 代表一个参数; (*, String) 代表第一个参数为任何值,第二个为 String 类型。

# 通知类型

通知类型回答了:『什么位置』增强『什么样』的代码。

注解 通知 说明
@Before 在被代理对象的方法前调用
@Around 将被代理方法封装起来 环绕通知,它将覆盖原有方法,但是允许通过反射调用原有方法
@After 在被代理对象方法后调用
@AfterReturning 在被代理对象正常返回后调用 要求被代理对象的方法执行过程中没有发生异常
@AfterThrowing 在被代理对象的方法抛出异常后调用 要求被代理对象的方法执行过程中发生异常

# 使用 @AspectJ 注解配置 Spring AOP

Spring 实现 AOP 功能的方式有两种:

  • @AspectJ 注解方式,要求会写。

  • XML 配置方式,要求会看。

题外话,AOP 概念并非 Spring 所特有,Spring 也并非支持 AOP 编程的唯一框架。在 Spring 之前提供 AOP 功能的,具有里程碑式的框架叫『AspectJ 框架』。

AspectJ 框架的使用方式比较独特,使用起来不太简便,很麻烦。在 Spring AOP 出现后就慢慢被 Spring AOP 所取代。但是,AspectJ 框架设计了一套注解,非常简便和合理,并且被广大 AspectJ 的使用者所熟知,所以 Spring AOP 直接借用这套注解,也就是我们这里所说的 @AspectJ 注解。

由于 @AspectJ 注解是 Spring『借用』的别人的注解,所以使用时需要引入它。

<dependency>
  <groupId>org.aspectj</groupId>
  <artifactId>aspectjrt</artifactId>
  <version>${aspectj.version}</version>
</dependency>

<dependency>
  <groupId>org.aspectj</groupId>
  <artifactId>aspectjweaver</artifactId>
  <version>${aspectj.version}</version>
</dependency>

在 Spring 的配置文件中,也需要引入/声明 AOP 的 namspace :

<beans
    ...
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="
        ...
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">

并且,由于 @AspectJ 注解并非 Spring 框架的一部分,所以需要在配置文件中声明『启用 @AspectJ 注解』功能,否则,Spring 并『不认识』 @AspectJ 的一系列注解。

<aop:aspectj-autoproxy />

<!-- 或者使用『@Component 注解 & 包扫描』创建单例对象 -->
<bean id="dept" class="bean.Dept" />
<bean id="aspect1" class="bean.DeptAspect1" />

再重复一遍,使用 Spring AOP 的核心问题:在『什么类』的『什么方法』的『什么地方』,做出『什么样』的增强。

@Aspect // 注意,不要忘记在切面类的头上加 @Aspect 注解。
public class DeptAspect1 {

    @Before("execution(* bean.Dept.sayHi(..))")
    public void before() {
        System.out.println("before ...");
    }

    @After("execution(* bean.Dept.sayHi(..))")
    public void after() {
        System.out.println("after ...");
    }

    @Around("execution(* bean.Dept.sayHi(..))")
    public void around(ProceedingJoinPoint jp) {
        System.out.println("hello");
        try {
            jp.proceed();
        } catch (Throwable e) {
            e.printStackTrace();
        }
        System.out.println("world");
    }

    @AfterReturning("execution(* bean.Dept.sayHi(..))")
    public void afterReturuing() {
        System.out.println("afterReturning ...");
    }

    @AfterThrowing("execution(* bean.Dept.sayHi(..))")
    public void afterThrowing() {
        System.out.println("afterThrowing ...");
    }
}

可以发现,上述代码中 execution(...) 部分有大量重复现象。为此,可以提供一个 @Pointcut 来进行“缩写”。

@Aspect
public class DeptAspect1 {

    @Pointcut("execution(* bean.Dept.sayHi(..))")
    public void xxx() {  // 这个方法是空的。需要的不是它的内容,需要的是它的名字。
    }

    @Before("xxx()")
    public void before() { ... }

    @After("xxx()")
    public void after() { ... }

    @Around("xxx()")
    public void around() { ... }

    @AfterReturning("xxx())")
    public void afterReturning() { ... }

    @AfterThrowing("xxx())")
    public void afterThrowing() { ... }
}

另外,有时你要拦截/增强的方法是有参数的,例如:

public void sayHi(String name, int age) { ... }

为此,你也可以在增强方法中获得这些参数,

@Pointcut("execution(* bean.Dept.sayHi(..))")
public void xxx() {}

@Before("execution(* bean.Dept.sayHi(..)) && args(name, age)")
public void before(String name, int age) {
    ...
}

@After("xxx() && args(name, age)")
public void after(String name, int age) {
    ...
}

# 使用 XML 配置 Spring AOP(了解、自学)

通过 XML 配置 Spring AOP 功能,在 XML 文件中出现的各种『要素』本质上和 @AspectJ 注解中出现过的内容本质上并没有两样。

public class DeptAspect2 {
    public void before() { ... }
    public void after() { ... }
    public void around(ProceedingJoinPoint jp) { ... }
    public void afterReturuing() { ... }
    public void afterThrowing() { ... }
}
<bean id="dept" class="bean.Dept" />
<bean id="aspect2" class="bean.DeptAspect2" />

<aop:config>
    <aop:aspect ref="aspect2">
        <aop:before method="before" pointcut="execution(* bean.Dept.sayHi(..)) and args(name, int)"/>
        <aop:after method="after" pointcut="execution(* bean.Dept.sayHi(..)) and args(name, int)"/>
    </aop:aspect>
</aop:config>