Spring AOP基础-基于xml的aspect实现AOP

从 0 开始深入学习 Spring 专栏目录总览

好,咱这一章就开始学习 AOP 的使用了,在开始之前呢,咱要先构建好一套测试代码。

本章源码均在 com.linkedbear.spring.aop.a_xmlaspect 目录。

0. 测试代码的搭建

为了接下来的演示更具有通用性,咱这里造一个 Service 层的接口,一个接口的实现类,一个普通的 Service 类,以及一个切面类 Logger :

OrderService :

public interface OrderService {
    
    void createOrder();
    
    void deleteOrderById(String id);
    
    String getOrderById(String id);
    
    List<String> findAll();
}

OrderServiceImpl :

public class OrderServiceImpl implements OrderService {
    
    @Override
    public void createOrder() {
        System.out.println("OrderServiceImpl 创建订单。。。");
    }
    
    @Override
    public void deleteOrderById(String id) {
        System.out.println("OrderServiceImpl 删除订单,id为" + id);
    }
    
    @Override
    public String getOrderById(String id) {
        System.out.println("OrderServiceImpl 查询订单,id为" + id);
        return id;
    }
    
    @Override
    public List<String> findAll() {
        System.out.println("OrderServiceImpl 查询所有订单。。。");
        return Arrays.asList("111", "222", "333");
    }
}

FinanceService :

public class FinanceService {
    
    public void addMoney(double money) {
        System.out.println("FinanceService 收钱 === " + money);
    }
    
    public double subtractMoney(double money) {
        System.out.println("FinanceService 付钱 === " + money);
        return money;
    }
    
    public double getMoneyById(String id) {
        System.out.println("FinanceService 查询账户,id为" + id);
        return Math.random();
    }
}

Logger 切面类:

public class Logger {
    
    public void beforePrint() {
        System.out.println("Logger beforePrint run ......");
    }
    
    public void afterPrint() {
        System.out.println("Logger afterPrint run ......");
    }
    
    public void afterReturningPrint() {
        System.out.println("Logger afterReturningPrint run ......");
    }
    
    public void afterThrowingPrint() {
        System.out.println("Logger afterThrowingPrint run ......");
    }
}

OK,有了这些,咱就可以开干了!

1. 基于xml的基本环境搭建【掌握】

环境搭建当然是简单的,咱先把该导入的依赖导入进去,该写的配置文件都写好。

1.1 导入Maven坐标依赖

既然是学习 SpringFramework 的 AOP ,那自然就要引入 Spring 的 AOP 模块对应的依赖:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>5.2.8.RELEASE</version>
</dependency>

注意,这里导入 aop 的依赖之后,借助 IDEA 的 Maven 窗口,可以发现 spring-aop 模块其实已经被 spring-context 模块依赖了:

Spring AOP基础-基于xml的aspect实现AOP

所以导不导 aop 的模块,当前工程中早就已经有 spring-aop 这个 jar 包的依赖啦。

1.2 编写配置文件

既然是基于 xml 配置文件的,那咱先把配置文件搞定。

在工程的 resources 目录下新建一个 xmlaspect.xml 文件,并首先把上面提到的几个类都注册进 IOC 容器中:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="financeService" class="com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService"/>

    <bean id="orderService" class="com.linkedbear.spring.aop.a_xmlaspect.service.impl.OrderServiceImpl"/>

    <bean id="logger" class="com.linkedbear.spring.aop.a_xmlaspect.component.Logger"/>
</beans>

1.3 测试运行

先不干任何多余的事情,直接编写启动类,驱动 IOC 容器并取出 FinanceService ,调用它的方法:

public class XmlAspectApplication {
    
    public static void main(String[] args) throws Exception {
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("aop/xmlaspect.xml");
        FinanceService financeService = ctx.getBean(FinanceService.class);
        financeService.addMoney(123.45);
        System.out.println(financeService.getMoneyById("abc"));
    }
}

运行 main 方法,控制台打印原生的对象输出的结果:

FinanceService 收钱 === 123.45
FinanceService 查询账户,id为abc
0.08681906193896294

至此,这些都是在前面 IOC 的基础内容了,接下来才是正儿八经的基于 xml 的 AOP 。

2. 基于xml的AOP实现【掌握】

要配置 xml 的 AOP ,需要几个步骤,咱一步一步来。

2.1 导入命名空间

要编写 AOP 的配置,需要在 xml 上导入命名空间:

<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.xsd 
                           http://www.springframework.org/schema/aop 
                           https://www.springframework.org/schema/aop/spring-aop.xsd">

如果小伙伴使用 IDEA 的话,该步骤可以跳过,IDEA 会帮我们加的。

然后,在配置文件中按提示键,会发现多了 3 个 aop 开头的标签:

Spring AOP基础-基于xml的aspect实现AOP

2.2 编写aop配置

接下来就要利用上面的这三个标签中的 <aop:config> 来配置 AOP 了。这个配置也比较简单,就两步。第一步要先声明一个切面:

<bean id="logger" class="com.linkedbear.spring.aop.a_xmlaspect.component.Logger"/>

<aop:config>
    <aop:aspect id="loggerAspect" ref="logger">
        
    </aop:aspect>
</aop:config>

一个 aspect 就是一个切面,id 随便起,只要是全局唯一即可;ref 跟 IOC 部分提到的 ref 一样,都是引用容器中的某个 bean ,这里咱要使用 Logger 作为切面类,所以 ref 就引用 logger 这个 bean 。

接下来,咱要配置一下通知类型。上一章咱说过了 Spring 一共有 5 种通知类型,这里咱先配置一个前置通知:

<bean id="logger" class="com.linkedbear.spring.aop.a_xmlaspect.component.Logger"/>

<aop:config>
    <aop:aspect id="loggerAspect" ref="logger">
        <aop:before method="beforePrint"
                    pointcut="??????"/>
    </aop:aspect>
</aop:config>

有了通知方法 method 了,切入点怎么搞定呢?哎,这里咱要学习一个新的知识点:切入点表达式

2.3 切入点表达式入门

最开始学习切入点表达式,咱先介绍最最常用的一种写法,而且这种写法刚好对标的就是 AOP 术语中的切入点

这样,小册先写一个,小伙伴们先瞅瞅这都什么含义:

execution(public void com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.addMoney(double))

是不是貌似还有点门道呢?下面咱来解释这个表达式的含义:

  • execution :以此法编写的切入点表达式,将使用方法定位的模式匹配连接点
    • 说白了,用 execution 写出来的表达式,都是直接声明到类中的方法的
  • public :限定只切入 public 类型的方法
  • void :限定只切入返回值类型为 void 的方法
  • com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService :限定只切入 FinanceService 这个类的方法
  • addMoney :限定只切入方法名为 addMoney 的方法
  • (double) :限定只切入方法的参数列表为一个参数,且类型为 double 的方法

所以,用这个表达式,就可以直接锁定到上面 FinanceService 的 addMoney 方法。

2.4 应用切入点表达式

接下来咱把上面写好的切入点表达式填到 pointcut 里:

<bean id="logger" class="com.linkedbear.spring.aop.a_xmlaspect.component.Logger"/>

<aop:config>
    <aop:aspect id="loggerAspect" ref="logger">
        <aop:before method="beforePrint"
                    pointcut="execution(public void com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.addMoney(double))"/>
    </aop:aspect>
</aop:config>

写完之后,使用 IDEA 的小伙伴会发现在 aop:before 的左边多了一个标识:

Spring AOP基础-基于xml的aspect实现AOP

点过去,会立马跳转到 FinanceService 的类中,并且 addMoney 方法的左边也有一个标识:

Spring AOP基础-基于xml的aspect实现AOP

这说明 IDEA 也知道这个切入点表达式的作用范围了。所以接下来咱即便不运行代码,光看这个标识也能知道切入点表达式的作用范围了。

2.5 测试运行

编写测试启动类,使用 xml 配置文件驱动 IOC 容器,并从 IOC 容器中取出 FinanceService ,分别执行它的三个方法:

public class XmlAspectApplication {
    
    public static void main(String[] args) throws Exception {
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("aop/xmlaspect.xml");
        FinanceService financeService = ctx.getBean(FinanceService.class);
        financeService.addMoney(123.45);
        financeService.subtractMoney(543.21);
        financeService.getMoneyById("abc");
    }
}

运行 main 方法,控制台打印了 Logger 的前置通知方法 beforePrint :

Logger beforePrint run ......
FinanceService 收钱 === 123.45
FinanceService 付钱 === 543.21
FinanceService 查询账户,id为abc

确实,上面编写的切入点表达式已经生效了,AOP 的效果得以体现。

3. 切入点表达式的多种写法【掌握】

咱继续讲解切入点表达式的编写方式哈。切入点表达式的写法比较多,咱先掌握 execution 风格写法,后面再学习更多的风格。

3.1 基本通配符

把上面的切入点表达式改一下,看看小伙伴们是否能猜得到它的含义:

execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.*(double))

还是很好猜的吧!这里有两个地方替换成了通配符 * ,咱解释一下它的含义:

  • void 的位置替换为 * ,代表不限制返回值类型,是什么都可以
  • FinanceService.*(double) 这里面的方法值替换为 * ,代表不限制方法名,什么方法都可以切入

所以,这样被切入的方法就变多了,除了 addMoney 方法之外,subtractMoney 也应该被切入了。

是不是这样呢,咱可以继续配置一个方法来检验一下。在 aop:config 中,继续添加后置通知:

<aop:config>
    <aop:aspect id="loggerAspect" ref="logger">
        <aop:before method="beforePrint"
                    pointcut="execution(public void com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.addMoney(double))"/>
        <aop:after method="afterPrint"
                   pointcut="execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.*(double))"/>
    </aop:aspect>
</aop:config>

其它的不需要任何改动,直接运行 main 方法,控制台会打印两次 afterPrint 方法,分别是 addMoney 与 subtractMoney 方法的调用,证明确实切到了两个方法。

Logger beforePrint run ......
FinanceService 收钱 === 123.45
Logger afterPrint run ......
FinanceService 付钱 === 543.21
Logger afterPrint run ......
FinanceService 查询账户,id为abc

注意:这个方法参数中,对于基本数据类型,直接声明即可;引用数据类型则要写类的全限定名

3.2 方法通配符

继续修改上面的切入点表达式:

execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.*(*))

这次的参数列表中标注了一个 * ,它代表方法的参数列表中必须有一个参数,至于类型那无所谓。

将 aop:after 的切入点表达式换为上面的写法,重新运行 main 方法,会发现 getMoneyById 方法也生效了:

Logger beforePrint run ......
FinanceService 收钱 === 123.45
Logger afterPrint run ......
FinanceService 付钱 === 543.21
Logger afterPrint run ......
FinanceService 查询账户,id为abc
Logger afterPrint run ......

3.3 类名通配符

咱继续变化切入点表达式:

execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.*.*(*))

这次连类名都任意了,所以这下 OrderService 接口也会被切入了。

咱继续编写一个 aop:after-returning 的通知:

<aop:config>
    <aop:aspect id="loggerAspect" ref="logger">
        <aop:before method="beforePrint"
                    pointcut="execution(public void com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.addMoney(double))"/>
        <aop:after method="afterPrint"
                   pointcut="execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.*(..))"/>
        <aop:after-returning method="afterReturningPrint"
                             pointcut="execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.*.*(..))"/>
    </aop:aspect>
</aop:config>

然后咱点击 aop:after-returning 标签左边的通知标识,发现 OrderService 的实现类也被切入了!

Spring AOP基础-基于xml的aspect实现AOP

所以我们又得知一个关键点:如果切入点表达式覆盖到了接口,那么如果这个接口有实现类,则实现类上的接口方法也会被切入增强

3.3 方法任意通配

如果我们重载一个 subtractMoney 方法,在方法的参数列表加上一个 id :

public double subtractMoney(double money, String id) {
    System.out.println("FinanceService 付钱 === " + money);
    return money;
}

注意写完这个方法后,IDEA 的左边并没有切入点的影响:

Spring AOP基础-基于xml的aspect实现AOP

说明 (*) 并不能切入两个参数的方法。那如果我想无论方法参数有几个,甚至没有参数,我都想切入,那该怎么写呢?

答案是换用 .. ,就像这样:

execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.*(..))

这样写完再切到 FinanceService 的类中,就发现所有方法都被切入了。

3.4 包名通配符

与类名、方法名的通配符一样,一个 * 代表一个目录,比如下面的这个切入点表达式:

execution(public * com.linkedbear.spring.aop.a_xmlaspect.*.*.*(..))

它代表的是切入 com.linkedbear.spring.aop.a_xmlaspect 包下的一级包下的任意类的任意方法(好绕。。。)。

注入 com.linkedbear.spring.aop.a_xmlaspect.controller 、com.linkedbear.spring.aop.a_xmlaspect.service 、com.linkedbear.spring.aop.a_xmlaspect.dao 等包下的所有类,都会被切到。

如果要切多级包怎么办呢?总不能一个 * 接着一个 * 写吧!所以方法参数列表中的 .. 在这里也能用:

execution(public * com.linkedbear.spring..*.*(..))

这个切入点表达式就代表 com.linkedbear.spring 包下的所有类的所有方法都会被切入。

最后多说一嘴,public 这个访问修饰符可以直接省略不写,代表切入所有访问修饰符的方法,那就相当于变成了这样:

execution(* com.linkedbear.spring..*.*(..))

3.5 抛出异常的切入

最后说下抛出异常的切入,对于某些显式声明了会抛出异常的方法,可以使用异常通知来切入这部分方法。

例如咱给 subtractMoney 方法添加一个 Exception 的抛出:

public double subtractMoney(double money, String id) throws Exception {
    System.out.println("FinanceService 付钱 === " + money);
    return money;
}

这样,在切入方法时,可以在类名方法名后面加上 throws 的异常类型即可:

execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.*(..) throws java.lang.Exception)

好了,到这里基本上 execution 风格的切入点表达式写法就差不多了,小伙伴们多多练习几个写法,并配合着 IDE 和测试代码,一定要掌握呀。

好,咱这一章就开始学习 AOP 的使用了,在开始之前呢,咱要先构建好一套测试代码。

本章源码均在 com.linkedbear.spring.aop.a_xmlaspect 目录。

0. 测试代码的搭建

为了接下来的演示更具有通用性,咱这里造一个 Service 层的接口,一个接口的实现类,一个普通的 Service 类,以及一个切面类 Logger :

OrderService :

public interface OrderService {
    
    void createOrder();
    
    void deleteOrderById(String id);
    
    String getOrderById(String id);
    
    List<String> findAll();
}

OrderServiceImpl :

public class OrderServiceImpl implements OrderService {
    
    @Override
    public void createOrder() {
        System.out.println("OrderServiceImpl 创建订单。。。");
    }
    
    @Override
    public void deleteOrderById(String id) {
        System.out.println("OrderServiceImpl 删除订单,id为" + id);
    }
    
    @Override
    public String getOrderById(String id) {
        System.out.println("OrderServiceImpl 查询订单,id为" + id);
        return id;
    }
    
    @Override
    public List<String> findAll() {
        System.out.println("OrderServiceImpl 查询所有订单。。。");
        return Arrays.asList("111", "222", "333");
    }
}

FinanceService :

public class FinanceService {
    
    public void addMoney(double money) {
        System.out.println("FinanceService 收钱 === " + money);
    }
    
    public double subtractMoney(double money) {
        System.out.println("FinanceService 付钱 === " + money);
        return money;
    }
    
    public double getMoneyById(String id) {
        System.out.println("FinanceService 查询账户,id为" + id);
        return Math.random();
    }
}

Logger 切面类:

public class Logger {
    
    public void beforePrint() {
        System.out.println("Logger beforePrint run ......");
    }
    
    public void afterPrint() {
        System.out.println("Logger afterPrint run ......");
    }
    
    public void afterReturningPrint() {
        System.out.println("Logger afterReturningPrint run ......");
    }
    
    public void afterThrowingPrint() {
        System.out.println("Logger afterThrowingPrint run ......");
    }
}

OK,有了这些,咱就可以开干了!

1. 基于xml的基本环境搭建【掌握】

环境搭建当然是简单的,咱先把该导入的依赖导入进去,该写的配置文件都写好。

1.1 导入Maven坐标依赖

既然是学习 SpringFramework 的 AOP ,那自然就要引入 Spring 的 AOP 模块对应的依赖:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>5.2.8.RELEASE</version>
</dependency>

注意,这里导入 aop 的依赖之后,借助 IDEA 的 Maven 窗口,可以发现 spring-aop 模块其实已经被 spring-context 模块依赖了:

Spring AOP基础-基于xml的aspect实现AOP

所以导不导 aop 的模块,当前工程中早就已经有 spring-aop 这个 jar 包的依赖啦。

1.2 编写配置文件

既然是基于 xml 配置文件的,那咱先把配置文件搞定。

在工程的 resources 目录下新建一个 xmlaspect.xml 文件,并首先把上面提到的几个类都注册进 IOC 容器中:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="financeService" class="com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService"/>

    <bean id="orderService" class="com.linkedbear.spring.aop.a_xmlaspect.service.impl.OrderServiceImpl"/>

    <bean id="logger" class="com.linkedbear.spring.aop.a_xmlaspect.component.Logger"/>
</beans>

1.3 测试运行

先不干任何多余的事情,直接编写启动类,驱动 IOC 容器并取出 FinanceService ,调用它的方法:

public class XmlAspectApplication {
    
    public static void main(String[] args) throws Exception {
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("aop/xmlaspect.xml");
        FinanceService financeService = ctx.getBean(FinanceService.class);
        financeService.addMoney(123.45);
        System.out.println(financeService.getMoneyById("abc"));
    }
}

运行 main 方法,控制台打印原生的对象输出的结果:

FinanceService 收钱 === 123.45
FinanceService 查询账户,id为abc
0.08681906193896294

至此,这些都是在前面 IOC 的基础内容了,接下来才是正儿八经的基于 xml 的 AOP 。

2. 基于xml的AOP实现【掌握】

要配置 xml 的 AOP ,需要几个步骤,咱一步一步来。

2.1 导入命名空间

要编写 AOP 的配置,需要在 xml 上导入命名空间:

<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.xsd 
                           http://www.springframework.org/schema/aop 
                           https://www.springframework.org/schema/aop/spring-aop.xsd">

如果小伙伴使用 IDEA 的话,该步骤可以跳过,IDEA 会帮我们加的。

然后,在配置文件中按提示键,会发现多了 3 个 aop 开头的标签:

Spring AOP基础-基于xml的aspect实现AOP

2.2 编写aop配置

接下来就要利用上面的这三个标签中的 <aop:config> 来配置 AOP 了。这个配置也比较简单,就两步。第一步要先声明一个切面:

<bean id="logger" class="com.linkedbear.spring.aop.a_xmlaspect.component.Logger"/>

<aop:config>
    <aop:aspect id="loggerAspect" ref="logger">
        
    </aop:aspect>
</aop:config>

一个 aspect 就是一个切面,id 随便起,只要是全局唯一即可;ref 跟 IOC 部分提到的 ref 一样,都是引用容器中的某个 bean ,这里咱要使用 Logger 作为切面类,所以 ref 就引用 logger 这个 bean 。

接下来,咱要配置一下通知类型。上一章咱说过了 Spring 一共有 5 种通知类型,这里咱先配置一个前置通知:

<bean id="logger" class="com.linkedbear.spring.aop.a_xmlaspect.component.Logger"/>

<aop:config>
    <aop:aspect id="loggerAspect" ref="logger">
        <aop:before method="beforePrint"
                    pointcut="??????"/>
    </aop:aspect>
</aop:config>

有了通知方法 method 了,切入点怎么搞定呢?哎,这里咱要学习一个新的知识点:切入点表达式

2.3 切入点表达式入门

最开始学习切入点表达式,咱先介绍最最常用的一种写法,而且这种写法刚好对标的就是 AOP 术语中的切入点

这样,小册先写一个,小伙伴们先瞅瞅这都什么含义:

execution(public void com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.addMoney(double))

是不是貌似还有点门道呢?下面咱来解释这个表达式的含义:

  • execution :以此法编写的切入点表达式,将使用方法定位的模式匹配连接点
    • 说白了,用 execution 写出来的表达式,都是直接声明到类中的方法的
  • public :限定只切入 public 类型的方法
  • void :限定只切入返回值类型为 void 的方法
  • com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService :限定只切入 FinanceService 这个类的方法
  • addMoney :限定只切入方法名为 addMoney 的方法
  • (double) :限定只切入方法的参数列表为一个参数,且类型为 double 的方法

所以,用这个表达式,就可以直接锁定到上面 FinanceService 的 addMoney 方法。

2.4 应用切入点表达式

接下来咱把上面写好的切入点表达式填到 pointcut 里:

<bean id="logger" class="com.linkedbear.spring.aop.a_xmlaspect.component.Logger"/>

<aop:config>
    <aop:aspect id="loggerAspect" ref="logger">
        <aop:before method="beforePrint"
                    pointcut="execution(public void com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.addMoney(double))"/>
    </aop:aspect>
</aop:config>

写完之后,使用 IDEA 的小伙伴会发现在 aop:before 的左边多了一个标识:

Spring AOP基础-基于xml的aspect实现AOP

点过去,会立马跳转到 FinanceService 的类中,并且 addMoney 方法的左边也有一个标识:

Spring AOP基础-基于xml的aspect实现AOP

这说明 IDEA 也知道这个切入点表达式的作用范围了。所以接下来咱即便不运行代码,光看这个标识也能知道切入点表达式的作用范围了。

2.5 测试运行

编写测试启动类,使用 xml 配置文件驱动 IOC 容器,并从 IOC 容器中取出 FinanceService ,分别执行它的三个方法:

public class XmlAspectApplication {
    
    public static void main(String[] args) throws Exception {
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("aop/xmlaspect.xml");
        FinanceService financeService = ctx.getBean(FinanceService.class);
        financeService.addMoney(123.45);
        financeService.subtractMoney(543.21);
        financeService.getMoneyById("abc");
    }
}

运行 main 方法,控制台打印了 Logger 的前置通知方法 beforePrint :

Logger beforePrint run ......
FinanceService 收钱 === 123.45
FinanceService 付钱 === 543.21
FinanceService 查询账户,id为abc

确实,上面编写的切入点表达式已经生效了,AOP 的效果得以体现。

3. 切入点表达式的多种写法【掌握】

咱继续讲解切入点表达式的编写方式哈。切入点表达式的写法比较多,咱先掌握 execution 风格写法,后面再学习更多的风格。

3.1 基本通配符

把上面的切入点表达式改一下,看看小伙伴们是否能猜得到它的含义:

execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.*(double))

还是很好猜的吧!这里有两个地方替换成了通配符 * ,咱解释一下它的含义:

  • void 的位置替换为 * ,代表不限制返回值类型,是什么都可以
  • FinanceService.*(double) 这里面的方法值替换为 * ,代表不限制方法名,什么方法都可以切入

所以,这样被切入的方法就变多了,除了 addMoney 方法之外,subtractMoney 也应该被切入了。

是不是这样呢,咱可以继续配置一个方法来检验一下。在 aop:config 中,继续添加后置通知:

<aop:config>
    <aop:aspect id="loggerAspect" ref="logger">
        <aop:before method="beforePrint"
                    pointcut="execution(public void com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.addMoney(double))"/>
        <aop:after method="afterPrint"
                   pointcut="execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.*(double))"/>
    </aop:aspect>
</aop:config>

其它的不需要任何改动,直接运行 main 方法,控制台会打印两次 afterPrint 方法,分别是 addMoney 与 subtractMoney 方法的调用,证明确实切到了两个方法。

Logger beforePrint run ......
FinanceService 收钱 === 123.45
Logger afterPrint run ......
FinanceService 付钱 === 543.21
Logger afterPrint run ......
FinanceService 查询账户,id为abc

注意:这个方法参数中,对于基本数据类型,直接声明即可;引用数据类型则要写类的全限定名

3.2 方法通配符

继续修改上面的切入点表达式:

execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.*(*))

这次的参数列表中标注了一个 * ,它代表方法的参数列表中必须有一个参数,至于类型那无所谓。

将 aop:after 的切入点表达式换为上面的写法,重新运行 main 方法,会发现 getMoneyById 方法也生效了:

Logger beforePrint run ......
FinanceService 收钱 === 123.45
Logger afterPrint run ......
FinanceService 付钱 === 543.21
Logger afterPrint run ......
FinanceService 查询账户,id为abc
Logger afterPrint run ......

3.3 类名通配符

咱继续变化切入点表达式:

execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.*.*(*))

这次连类名都任意了,所以这下 OrderService 接口也会被切入了。

咱继续编写一个 aop:after-returning 的通知:

<aop:config>
    <aop:aspect id="loggerAspect" ref="logger">
        <aop:before method="beforePrint"
                    pointcut="execution(public void com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.addMoney(double))"/>
        <aop:after method="afterPrint"
                   pointcut="execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.*(..))"/>
        <aop:after-returning method="afterReturningPrint"
                             pointcut="execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.*.*(..))"/>
    </aop:aspect>
</aop:config>

然后咱点击 aop:after-returning 标签左边的通知标识,发现 OrderService 的实现类也被切入了!

Spring AOP基础-基于xml的aspect实现AOP

所以我们又得知一个关键点:如果切入点表达式覆盖到了接口,那么如果这个接口有实现类,则实现类上的接口方法也会被切入增强

3.3 方法任意通配

如果我们重载一个 subtractMoney 方法,在方法的参数列表加上一个 id :

public double subtractMoney(double money, String id) {
    System.out.println("FinanceService 付钱 === " + money);
    return money;
}

注意写完这个方法后,IDEA 的左边并没有切入点的影响:

Spring AOP基础-基于xml的aspect实现AOP

说明 (*) 并不能切入两个参数的方法。那如果我想无论方法参数有几个,甚至没有参数,我都想切入,那该怎么写呢?

答案是换用 .. ,就像这样:

execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.*(..))

这样写完再切到 FinanceService 的类中,就发现所有方法都被切入了。

3.4 包名通配符

与类名、方法名的通配符一样,一个 * 代表一个目录,比如下面的这个切入点表达式:

execution(public * com.linkedbear.spring.aop.a_xmlaspect.*.*.*(..))

它代表的是切入 com.linkedbear.spring.aop.a_xmlaspect 包下的一级包下的任意类的任意方法(好绕。。。)。

注入 com.linkedbear.spring.aop.a_xmlaspect.controller 、com.linkedbear.spring.aop.a_xmlaspect.service 、com.linkedbear.spring.aop.a_xmlaspect.dao 等包下的所有类,都会被切到。

如果要切多级包怎么办呢?总不能一个 * 接着一个 * 写吧!所以方法参数列表中的 .. 在这里也能用:

execution(public * com.linkedbear.spring..*.*(..))

这个切入点表达式就代表 com.linkedbear.spring 包下的所有类的所有方法都会被切入。

最后多说一嘴,public 这个访问修饰符可以直接省略不写,代表切入所有访问修饰符的方法,那就相当于变成了这样:

execution(* com.linkedbear.spring..*.*(..))

3.5 抛出异常的切入

最后说下抛出异常的切入,对于某些显式声明了会抛出异常的方法,可以使用异常通知来切入这部分方法。

例如咱给 subtractMoney 方法添加一个 Exception 的抛出:

public double subtractMoney(double money, String id) throws Exception {
    System.out.println("FinanceService 付钱 === " + money);
    return money;
}

这样,在切入方法时,可以在类名方法名后面加上 throws 的异常类型即可:

execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.*(..) throws java.lang.Exception)

好了,到这里基本上 execution 风格的切入点表达式写法就差不多了,小伙伴们多多练习几个写法,并配合着 IDE 和测试代码,一定要掌握呀。

好,咱这一章就开始学习 AOP 的使用了,在开始之前呢,咱要先构建好一套测试代码。

本章源码均在 com.linkedbear.spring.aop.a_xmlaspect 目录。

0. 测试代码的搭建

为了接下来的演示更具有通用性,咱这里造一个 Service 层的接口,一个接口的实现类,一个普通的 Service 类,以及一个切面类 Logger :

OrderService :

public interface OrderService {
    
    void createOrder();
    
    void deleteOrderById(String id);
    
    String getOrderById(String id);
    
    List<String> findAll();
}

OrderServiceImpl :

public class OrderServiceImpl implements OrderService {
    
    @Override
    public void createOrder() {
        System.out.println("OrderServiceImpl 创建订单。。。");
    }
    
    @Override
    public void deleteOrderById(String id) {
        System.out.println("OrderServiceImpl 删除订单,id为" + id);
    }
    
    @Override
    public String getOrderById(String id) {
        System.out.println("OrderServiceImpl 查询订单,id为" + id);
        return id;
    }
    
    @Override
    public List<String> findAll() {
        System.out.println("OrderServiceImpl 查询所有订单。。。");
        return Arrays.asList("111", "222", "333");
    }
}

FinanceService :

public class FinanceService {
    
    public void addMoney(double money) {
        System.out.println("FinanceService 收钱 === " + money);
    }
    
    public double subtractMoney(double money) {
        System.out.println("FinanceService 付钱 === " + money);
        return money;
    }
    
    public double getMoneyById(String id) {
        System.out.println("FinanceService 查询账户,id为" + id);
        return Math.random();
    }
}

Logger 切面类:

public class Logger {
    
    public void beforePrint() {
        System.out.println("Logger beforePrint run ......");
    }
    
    public void afterPrint() {
        System.out.println("Logger afterPrint run ......");
    }
    
    public void afterReturningPrint() {
        System.out.println("Logger afterReturningPrint run ......");
    }
    
    public void afterThrowingPrint() {
        System.out.println("Logger afterThrowingPrint run ......");
    }
}

OK,有了这些,咱就可以开干了!

1. 基于xml的基本环境搭建【掌握】

环境搭建当然是简单的,咱先把该导入的依赖导入进去,该写的配置文件都写好。

1.1 导入Maven坐标依赖

既然是学习 SpringFramework 的 AOP ,那自然就要引入 Spring 的 AOP 模块对应的依赖:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>5.2.8.RELEASE</version>
</dependency>

注意,这里导入 aop 的依赖之后,借助 IDEA 的 Maven 窗口,可以发现 spring-aop 模块其实已经被 spring-context 模块依赖了:

Spring AOP基础-基于xml的aspect实现AOP

所以导不导 aop 的模块,当前工程中早就已经有 spring-aop 这个 jar 包的依赖啦。

1.2 编写配置文件

既然是基于 xml 配置文件的,那咱先把配置文件搞定。

在工程的 resources 目录下新建一个 xmlaspect.xml 文件,并首先把上面提到的几个类都注册进 IOC 容器中:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="financeService" class="com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService"/>

    <bean id="orderService" class="com.linkedbear.spring.aop.a_xmlaspect.service.impl.OrderServiceImpl"/>

    <bean id="logger" class="com.linkedbear.spring.aop.a_xmlaspect.component.Logger"/>
</beans>

1.3 测试运行

先不干任何多余的事情,直接编写启动类,驱动 IOC 容器并取出 FinanceService ,调用它的方法:

public class XmlAspectApplication {
    
    public static void main(String[] args) throws Exception {
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("aop/xmlaspect.xml");
        FinanceService financeService = ctx.getBean(FinanceService.class);
        financeService.addMoney(123.45);
        System.out.println(financeService.getMoneyById("abc"));
    }
}

运行 main 方法,控制台打印原生的对象输出的结果:

FinanceService 收钱 === 123.45
FinanceService 查询账户,id为abc
0.08681906193896294

至此,这些都是在前面 IOC 的基础内容了,接下来才是正儿八经的基于 xml 的 AOP 。

2. 基于xml的AOP实现【掌握】

要配置 xml 的 AOP ,需要几个步骤,咱一步一步来。

2.1 导入命名空间

要编写 AOP 的配置,需要在 xml 上导入命名空间:

<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.xsd 
                           http://www.springframework.org/schema/aop 
                           https://www.springframework.org/schema/aop/spring-aop.xsd">

如果小伙伴使用 IDEA 的话,该步骤可以跳过,IDEA 会帮我们加的。

然后,在配置文件中按提示键,会发现多了 3 个 aop 开头的标签:

Spring AOP基础-基于xml的aspect实现AOP

2.2 编写aop配置

接下来就要利用上面的这三个标签中的 <aop:config> 来配置 AOP 了。这个配置也比较简单,就两步。第一步要先声明一个切面:

<bean id="logger" class="com.linkedbear.spring.aop.a_xmlaspect.component.Logger"/>

<aop:config>
    <aop:aspect id="loggerAspect" ref="logger">
        
    </aop:aspect>
</aop:config>

一个 aspect 就是一个切面,id 随便起,只要是全局唯一即可;ref 跟 IOC 部分提到的 ref 一样,都是引用容器中的某个 bean ,这里咱要使用 Logger 作为切面类,所以 ref 就引用 logger 这个 bean 。

接下来,咱要配置一下通知类型。上一章咱说过了 Spring 一共有 5 种通知类型,这里咱先配置一个前置通知:

<bean id="logger" class="com.linkedbear.spring.aop.a_xmlaspect.component.Logger"/>

<aop:config>
    <aop:aspect id="loggerAspect" ref="logger">
        <aop:before method="beforePrint"
                    pointcut="??????"/>
    </aop:aspect>
</aop:config>

有了通知方法 method 了,切入点怎么搞定呢?哎,这里咱要学习一个新的知识点:切入点表达式

2.3 切入点表达式入门

最开始学习切入点表达式,咱先介绍最最常用的一种写法,而且这种写法刚好对标的就是 AOP 术语中的切入点

这样,小册先写一个,小伙伴们先瞅瞅这都什么含义:

execution(public void com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.addMoney(double))

是不是貌似还有点门道呢?下面咱来解释这个表达式的含义:

  • execution :以此法编写的切入点表达式,将使用方法定位的模式匹配连接点
    • 说白了,用 execution 写出来的表达式,都是直接声明到类中的方法的
  • public :限定只切入 public 类型的方法
  • void :限定只切入返回值类型为 void 的方法
  • com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService :限定只切入 FinanceService 这个类的方法
  • addMoney :限定只切入方法名为 addMoney 的方法
  • (double) :限定只切入方法的参数列表为一个参数,且类型为 double 的方法

所以,用这个表达式,就可以直接锁定到上面 FinanceService 的 addMoney 方法。

2.4 应用切入点表达式

接下来咱把上面写好的切入点表达式填到 pointcut 里:

<bean id="logger" class="com.linkedbear.spring.aop.a_xmlaspect.component.Logger"/>

<aop:config>
    <aop:aspect id="loggerAspect" ref="logger">
        <aop:before method="beforePrint"
                    pointcut="execution(public void com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.addMoney(double))"/>
    </aop:aspect>
</aop:config>

写完之后,使用 IDEA 的小伙伴会发现在 aop:before 的左边多了一个标识:

Spring AOP基础-基于xml的aspect实现AOP

点过去,会立马跳转到 FinanceService 的类中,并且 addMoney 方法的左边也有一个标识:

Spring AOP基础-基于xml的aspect实现AOP

这说明 IDEA 也知道这个切入点表达式的作用范围了。所以接下来咱即便不运行代码,光看这个标识也能知道切入点表达式的作用范围了。

2.5 测试运行

编写测试启动类,使用 xml 配置文件驱动 IOC 容器,并从 IOC 容器中取出 FinanceService ,分别执行它的三个方法:

public class XmlAspectApplication {
    
    public static void main(String[] args) throws Exception {
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("aop/xmlaspect.xml");
        FinanceService financeService = ctx.getBean(FinanceService.class);
        financeService.addMoney(123.45);
        financeService.subtractMoney(543.21);
        financeService.getMoneyById("abc");
    }
}

运行 main 方法,控制台打印了 Logger 的前置通知方法 beforePrint :

Logger beforePrint run ......
FinanceService 收钱 === 123.45
FinanceService 付钱 === 543.21
FinanceService 查询账户,id为abc

确实,上面编写的切入点表达式已经生效了,AOP 的效果得以体现。

3. 切入点表达式的多种写法【掌握】

咱继续讲解切入点表达式的编写方式哈。切入点表达式的写法比较多,咱先掌握 execution 风格写法,后面再学习更多的风格。

3.1 基本通配符

把上面的切入点表达式改一下,看看小伙伴们是否能猜得到它的含义:

execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.*(double))

还是很好猜的吧!这里有两个地方替换成了通配符 * ,咱解释一下它的含义:

  • void 的位置替换为 * ,代表不限制返回值类型,是什么都可以
  • FinanceService.*(double) 这里面的方法值替换为 * ,代表不限制方法名,什么方法都可以切入

所以,这样被切入的方法就变多了,除了 addMoney 方法之外,subtractMoney 也应该被切入了。

是不是这样呢,咱可以继续配置一个方法来检验一下。在 aop:config 中,继续添加后置通知:

<aop:config>
    <aop:aspect id="loggerAspect" ref="logger">
        <aop:before method="beforePrint"
                    pointcut="execution(public void com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.addMoney(double))"/>
        <aop:after method="afterPrint"
                   pointcut="execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.*(double))"/>
    </aop:aspect>
</aop:config>

其它的不需要任何改动,直接运行 main 方法,控制台会打印两次 afterPrint 方法,分别是 addMoney 与 subtractMoney 方法的调用,证明确实切到了两个方法。

Logger beforePrint run ......
FinanceService 收钱 === 123.45
Logger afterPrint run ......
FinanceService 付钱 === 543.21
Logger afterPrint run ......
FinanceService 查询账户,id为abc

注意:这个方法参数中,对于基本数据类型,直接声明即可;引用数据类型则要写类的全限定名

3.2 方法通配符

继续修改上面的切入点表达式:

execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.*(*))

这次的参数列表中标注了一个 * ,它代表方法的参数列表中必须有一个参数,至于类型那无所谓。

将 aop:after 的切入点表达式换为上面的写法,重新运行 main 方法,会发现 getMoneyById 方法也生效了:

Logger beforePrint run ......
FinanceService 收钱 === 123.45
Logger afterPrint run ......
FinanceService 付钱 === 543.21
Logger afterPrint run ......
FinanceService 查询账户,id为abc
Logger afterPrint run ......

3.3 类名通配符

咱继续变化切入点表达式:

execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.*.*(*))

这次连类名都任意了,所以这下 OrderService 接口也会被切入了。

咱继续编写一个 aop:after-returning 的通知:

<aop:config>
    <aop:aspect id="loggerAspect" ref="logger">
        <aop:before method="beforePrint"
                    pointcut="execution(public void com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.addMoney(double))"/>
        <aop:after method="afterPrint"
                   pointcut="execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.*(..))"/>
        <aop:after-returning method="afterReturningPrint"
                             pointcut="execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.*.*(..))"/>
    </aop:aspect>
</aop:config>

然后咱点击 aop:after-returning 标签左边的通知标识,发现 OrderService 的实现类也被切入了!

Spring AOP基础-基于xml的aspect实现AOP

所以我们又得知一个关键点:如果切入点表达式覆盖到了接口,那么如果这个接口有实现类,则实现类上的接口方法也会被切入增强

3.3 方法任意通配

如果我们重载一个 subtractMoney 方法,在方法的参数列表加上一个 id :

public double subtractMoney(double money, String id) {
    System.out.println("FinanceService 付钱 === " + money);
    return money;
}

注意写完这个方法后,IDEA 的左边并没有切入点的影响:

Spring AOP基础-基于xml的aspect实现AOP

说明 (*) 并不能切入两个参数的方法。那如果我想无论方法参数有几个,甚至没有参数,我都想切入,那该怎么写呢?

答案是换用 .. ,就像这样:

execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.*(..))

这样写完再切到 FinanceService 的类中,就发现所有方法都被切入了。

3.4 包名通配符

与类名、方法名的通配符一样,一个 * 代表一个目录,比如下面的这个切入点表达式:

execution(public * com.linkedbear.spring.aop.a_xmlaspect.*.*.*(..))

它代表的是切入 com.linkedbear.spring.aop.a_xmlaspect 包下的一级包下的任意类的任意方法(好绕。。。)。

注入 com.linkedbear.spring.aop.a_xmlaspect.controller 、com.linkedbear.spring.aop.a_xmlaspect.service 、com.linkedbear.spring.aop.a_xmlaspect.dao 等包下的所有类,都会被切到。

如果要切多级包怎么办呢?总不能一个 * 接着一个 * 写吧!所以方法参数列表中的 .. 在这里也能用:

execution(public * com.linkedbear.spring..*.*(..))

这个切入点表达式就代表 com.linkedbear.spring 包下的所有类的所有方法都会被切入。

最后多说一嘴,public 这个访问修饰符可以直接省略不写,代表切入所有访问修饰符的方法,那就相当于变成了这样:

execution(* com.linkedbear.spring..*.*(..))

3.5 抛出异常的切入

最后说下抛出异常的切入,对于某些显式声明了会抛出异常的方法,可以使用异常通知来切入这部分方法。

例如咱给 subtractMoney 方法添加一个 Exception 的抛出:

public double subtractMoney(double money, String id) throws Exception {
    System.out.println("FinanceService 付钱 === " + money);
    return money;
}

这样,在切入方法时,可以在类名方法名后面加上 throws 的异常类型即可:

execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.*(..) throws java.lang.Exception)

好了,到这里基本上 execution 风格的切入点表达式写法就差不多了,小伙伴们多多练习几个写法,并配合着 IDE 和测试代码,一定要掌握呀。

好,咱这一章就开始学习 AOP 的使用了,在开始之前呢,咱要先构建好一套测试代码。

本章源码均在 com.linkedbear.spring.aop.a_xmlaspect 目录。

0. 测试代码的搭建

为了接下来的演示更具有通用性,咱这里造一个 Service 层的接口,一个接口的实现类,一个普通的 Service 类,以及一个切面类 Logger :

OrderService :

public interface OrderService {
    
    void createOrder();
    
    void deleteOrderById(String id);
    
    String getOrderById(String id);
    
    List<String> findAll();
}

OrderServiceImpl :

public class OrderServiceImpl implements OrderService {
    
    @Override
    public void createOrder() {
        System.out.println("OrderServiceImpl 创建订单。。。");
    }
    
    @Override
    public void deleteOrderById(String id) {
        System.out.println("OrderServiceImpl 删除订单,id为" + id);
    }
    
    @Override
    public String getOrderById(String id) {
        System.out.println("OrderServiceImpl 查询订单,id为" + id);
        return id;
    }
    
    @Override
    public List<String> findAll() {
        System.out.println("OrderServiceImpl 查询所有订单。。。");
        return Arrays.asList("111", "222", "333");
    }
}

FinanceService :

public class FinanceService {
    
    public void addMoney(double money) {
        System.out.println("FinanceService 收钱 === " + money);
    }
    
    public double subtractMoney(double money) {
        System.out.println("FinanceService 付钱 === " + money);
        return money;
    }
    
    public double getMoneyById(String id) {
        System.out.println("FinanceService 查询账户,id为" + id);
        return Math.random();
    }
}

Logger 切面类:

public class Logger {
    
    public void beforePrint() {
        System.out.println("Logger beforePrint run ......");
    }
    
    public void afterPrint() {
        System.out.println("Logger afterPrint run ......");
    }
    
    public void afterReturningPrint() {
        System.out.println("Logger afterReturningPrint run ......");
    }
    
    public void afterThrowingPrint() {
        System.out.println("Logger afterThrowingPrint run ......");
    }
}

OK,有了这些,咱就可以开干了!

1. 基于xml的基本环境搭建【掌握】

环境搭建当然是简单的,咱先把该导入的依赖导入进去,该写的配置文件都写好。

1.1 导入Maven坐标依赖

既然是学习 SpringFramework 的 AOP ,那自然就要引入 Spring 的 AOP 模块对应的依赖:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>5.2.8.RELEASE</version>
</dependency>

注意,这里导入 aop 的依赖之后,借助 IDEA 的 Maven 窗口,可以发现 spring-aop 模块其实已经被 spring-context 模块依赖了:

Spring AOP基础-基于xml的aspect实现AOP

所以导不导 aop 的模块,当前工程中早就已经有 spring-aop 这个 jar 包的依赖啦。

1.2 编写配置文件

既然是基于 xml 配置文件的,那咱先把配置文件搞定。

在工程的 resources 目录下新建一个 xmlaspect.xml 文件,并首先把上面提到的几个类都注册进 IOC 容器中:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="financeService" class="com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService"/>

    <bean id="orderService" class="com.linkedbear.spring.aop.a_xmlaspect.service.impl.OrderServiceImpl"/>

    <bean id="logger" class="com.linkedbear.spring.aop.a_xmlaspect.component.Logger"/>
</beans>

1.3 测试运行

先不干任何多余的事情,直接编写启动类,驱动 IOC 容器并取出 FinanceService ,调用它的方法:

public class XmlAspectApplication {
    
    public static void main(String[] args) throws Exception {
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("aop/xmlaspect.xml");
        FinanceService financeService = ctx.getBean(FinanceService.class);
        financeService.addMoney(123.45);
        System.out.println(financeService.getMoneyById("abc"));
    }
}

运行 main 方法,控制台打印原生的对象输出的结果:

FinanceService 收钱 === 123.45
FinanceService 查询账户,id为abc
0.08681906193896294

至此,这些都是在前面 IOC 的基础内容了,接下来才是正儿八经的基于 xml 的 AOP 。

2. 基于xml的AOP实现【掌握】

要配置 xml 的 AOP ,需要几个步骤,咱一步一步来。

2.1 导入命名空间

要编写 AOP 的配置,需要在 xml 上导入命名空间:

<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.xsd 
                           http://www.springframework.org/schema/aop 
                           https://www.springframework.org/schema/aop/spring-aop.xsd">

如果小伙伴使用 IDEA 的话,该步骤可以跳过,IDEA 会帮我们加的。

然后,在配置文件中按提示键,会发现多了 3 个 aop 开头的标签:

Spring AOP基础-基于xml的aspect实现AOP

2.2 编写aop配置

接下来就要利用上面的这三个标签中的 <aop:config> 来配置 AOP 了。这个配置也比较简单,就两步。第一步要先声明一个切面:

<bean id="logger" class="com.linkedbear.spring.aop.a_xmlaspect.component.Logger"/>

<aop:config>
    <aop:aspect id="loggerAspect" ref="logger">
        
    </aop:aspect>
</aop:config>

一个 aspect 就是一个切面,id 随便起,只要是全局唯一即可;ref 跟 IOC 部分提到的 ref 一样,都是引用容器中的某个 bean ,这里咱要使用 Logger 作为切面类,所以 ref 就引用 logger 这个 bean 。

接下来,咱要配置一下通知类型。上一章咱说过了 Spring 一共有 5 种通知类型,这里咱先配置一个前置通知:

<bean id="logger" class="com.linkedbear.spring.aop.a_xmlaspect.component.Logger"/>

<aop:config>
    <aop:aspect id="loggerAspect" ref="logger">
        <aop:before method="beforePrint"
                    pointcut="??????"/>
    </aop:aspect>
</aop:config>

有了通知方法 method 了,切入点怎么搞定呢?哎,这里咱要学习一个新的知识点:切入点表达式

2.3 切入点表达式入门

最开始学习切入点表达式,咱先介绍最最常用的一种写法,而且这种写法刚好对标的就是 AOP 术语中的切入点

这样,小册先写一个,小伙伴们先瞅瞅这都什么含义:

execution(public void com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.addMoney(double))

是不是貌似还有点门道呢?下面咱来解释这个表达式的含义:

  • execution :以此法编写的切入点表达式,将使用方法定位的模式匹配连接点
    • 说白了,用 execution 写出来的表达式,都是直接声明到类中的方法的
  • public :限定只切入 public 类型的方法
  • void :限定只切入返回值类型为 void 的方法
  • com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService :限定只切入 FinanceService 这个类的方法
  • addMoney :限定只切入方法名为 addMoney 的方法
  • (double) :限定只切入方法的参数列表为一个参数,且类型为 double 的方法

所以,用这个表达式,就可以直接锁定到上面 FinanceService 的 addMoney 方法。

2.4 应用切入点表达式

接下来咱把上面写好的切入点表达式填到 pointcut 里:

<bean id="logger" class="com.linkedbear.spring.aop.a_xmlaspect.component.Logger"/>

<aop:config>
    <aop:aspect id="loggerAspect" ref="logger">
        <aop:before method="beforePrint"
                    pointcut="execution(public void com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.addMoney(double))"/>
    </aop:aspect>
</aop:config>

写完之后,使用 IDEA 的小伙伴会发现在 aop:before 的左边多了一个标识:

Spring AOP基础-基于xml的aspect实现AOP

点过去,会立马跳转到 FinanceService 的类中,并且 addMoney 方法的左边也有一个标识:

Spring AOP基础-基于xml的aspect实现AOP

这说明 IDEA 也知道这个切入点表达式的作用范围了。所以接下来咱即便不运行代码,光看这个标识也能知道切入点表达式的作用范围了。

2.5 测试运行

编写测试启动类,使用 xml 配置文件驱动 IOC 容器,并从 IOC 容器中取出 FinanceService ,分别执行它的三个方法:

public class XmlAspectApplication {
    
    public static void main(String[] args) throws Exception {
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("aop/xmlaspect.xml");
        FinanceService financeService = ctx.getBean(FinanceService.class);
        financeService.addMoney(123.45);
        financeService.subtractMoney(543.21);
        financeService.getMoneyById("abc");
    }
}

运行 main 方法,控制台打印了 Logger 的前置通知方法 beforePrint :

Logger beforePrint run ......
FinanceService 收钱 === 123.45
FinanceService 付钱 === 543.21
FinanceService 查询账户,id为abc

确实,上面编写的切入点表达式已经生效了,AOP 的效果得以体现。

3. 切入点表达式的多种写法【掌握】

咱继续讲解切入点表达式的编写方式哈。切入点表达式的写法比较多,咱先掌握 execution 风格写法,后面再学习更多的风格。

3.1 基本通配符

把上面的切入点表达式改一下,看看小伙伴们是否能猜得到它的含义:

execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.*(double))

还是很好猜的吧!这里有两个地方替换成了通配符 * ,咱解释一下它的含义:

  • void 的位置替换为 * ,代表不限制返回值类型,是什么都可以
  • FinanceService.*(double) 这里面的方法值替换为 * ,代表不限制方法名,什么方法都可以切入

所以,这样被切入的方法就变多了,除了 addMoney 方法之外,subtractMoney 也应该被切入了。

是不是这样呢,咱可以继续配置一个方法来检验一下。在 aop:config 中,继续添加后置通知:

<aop:config>
    <aop:aspect id="loggerAspect" ref="logger">
        <aop:before method="beforePrint"
                    pointcut="execution(public void com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.addMoney(double))"/>
        <aop:after method="afterPrint"
                   pointcut="execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.*(double))"/>
    </aop:aspect>
</aop:config>

其它的不需要任何改动,直接运行 main 方法,控制台会打印两次 afterPrint 方法,分别是 addMoney 与 subtractMoney 方法的调用,证明确实切到了两个方法。

Logger beforePrint run ......
FinanceService 收钱 === 123.45
Logger afterPrint run ......
FinanceService 付钱 === 543.21
Logger afterPrint run ......
FinanceService 查询账户,id为abc

注意:这个方法参数中,对于基本数据类型,直接声明即可;引用数据类型则要写类的全限定名

3.2 方法通配符

继续修改上面的切入点表达式:

execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.*(*))

这次的参数列表中标注了一个 * ,它代表方法的参数列表中必须有一个参数,至于类型那无所谓。

将 aop:after 的切入点表达式换为上面的写法,重新运行 main 方法,会发现 getMoneyById 方法也生效了:

Logger beforePrint run ......
FinanceService 收钱 === 123.45
Logger afterPrint run ......
FinanceService 付钱 === 543.21
Logger afterPrint run ......
FinanceService 查询账户,id为abc
Logger afterPrint run ......

3.3 类名通配符

咱继续变化切入点表达式:

execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.*.*(*))

这次连类名都任意了,所以这下 OrderService 接口也会被切入了。

咱继续编写一个 aop:after-returning 的通知:

<aop:config>
    <aop:aspect id="loggerAspect" ref="logger">
        <aop:before method="beforePrint"
                    pointcut="execution(public void com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.addMoney(double))"/>
        <aop:after method="afterPrint"
                   pointcut="execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.*(..))"/>
        <aop:after-returning method="afterReturningPrint"
                             pointcut="execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.*.*(..))"/>
    </aop:aspect>
</aop:config>

然后咱点击 aop:after-returning 标签左边的通知标识,发现 OrderService 的实现类也被切入了!

Spring AOP基础-基于xml的aspect实现AOP

所以我们又得知一个关键点:如果切入点表达式覆盖到了接口,那么如果这个接口有实现类,则实现类上的接口方法也会被切入增强

3.3 方法任意通配

如果我们重载一个 subtractMoney 方法,在方法的参数列表加上一个 id :

public double subtractMoney(double money, String id) {
    System.out.println("FinanceService 付钱 === " + money);
    return money;
}

注意写完这个方法后,IDEA 的左边并没有切入点的影响:

Spring AOP基础-基于xml的aspect实现AOP

说明 (*) 并不能切入两个参数的方法。那如果我想无论方法参数有几个,甚至没有参数,我都想切入,那该怎么写呢?

答案是换用 .. ,就像这样:

execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.*(..))

这样写完再切到 FinanceService 的类中,就发现所有方法都被切入了。

3.4 包名通配符

与类名、方法名的通配符一样,一个 * 代表一个目录,比如下面的这个切入点表达式:

execution(public * com.linkedbear.spring.aop.a_xmlaspect.*.*.*(..))

它代表的是切入 com.linkedbear.spring.aop.a_xmlaspect 包下的一级包下的任意类的任意方法(好绕。。。)。

注入 com.linkedbear.spring.aop.a_xmlaspect.controller 、com.linkedbear.spring.aop.a_xmlaspect.service 、com.linkedbear.spring.aop.a_xmlaspect.dao 等包下的所有类,都会被切到。

如果要切多级包怎么办呢?总不能一个 * 接着一个 * 写吧!所以方法参数列表中的 .. 在这里也能用:

execution(public * com.linkedbear.spring..*.*(..))

这个切入点表达式就代表 com.linkedbear.spring 包下的所有类的所有方法都会被切入。

最后多说一嘴,public 这个访问修饰符可以直接省略不写,代表切入所有访问修饰符的方法,那就相当于变成了这样:

execution(* com.linkedbear.spring..*.*(..))

3.5 抛出异常的切入

最后说下抛出异常的切入,对于某些显式声明了会抛出异常的方法,可以使用异常通知来切入这部分方法。

例如咱给 subtractMoney 方法添加一个 Exception 的抛出:

public double subtractMoney(double money, String id) throws Exception {
    System.out.println("FinanceService 付钱 === " + money);
    return money;
}

这样,在切入方法时,可以在类名方法名后面加上 throws 的异常类型即可:

execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.*(..) throws java.lang.Exception)

好了,到这里基本上 execution 风格的切入点表达式写法就差不多了,小伙伴们多多练习几个写法,并配合着 IDE 和测试代码,一定要掌握呀。

好,咱这一章就开始学习 AOP 的使用了,在开始之前呢,咱要先构建好一套测试代码。

本章源码均在 com.linkedbear.spring.aop.a_xmlaspect 目录。

0. 测试代码的搭建

为了接下来的演示更具有通用性,咱这里造一个 Service 层的接口,一个接口的实现类,一个普通的 Service 类,以及一个切面类 Logger :

OrderService :

public interface OrderService {
    
    void createOrder();
    
    void deleteOrderById(String id);
    
    String getOrderById(String id);
    
    List<String> findAll();
}

OrderServiceImpl :

public class OrderServiceImpl implements OrderService {
    
    @Override
    public void createOrder() {
        System.out.println("OrderServiceImpl 创建订单。。。");
    }
    
    @Override
    public void deleteOrderById(String id) {
        System.out.println("OrderServiceImpl 删除订单,id为" + id);
    }
    
    @Override
    public String getOrderById(String id) {
        System.out.println("OrderServiceImpl 查询订单,id为" + id);
        return id;
    }
    
    @Override
    public List<String> findAll() {
        System.out.println("OrderServiceImpl 查询所有订单。。。");
        return Arrays.asList("111", "222", "333");
    }
}

FinanceService :

public class FinanceService {
    
    public void addMoney(double money) {
        System.out.println("FinanceService 收钱 === " + money);
    }
    
    public double subtractMoney(double money) {
        System.out.println("FinanceService 付钱 === " + money);
        return money;
    }
    
    public double getMoneyById(String id) {
        System.out.println("FinanceService 查询账户,id为" + id);
        return Math.random();
    }
}

Logger 切面类:

public class Logger {
    
    public void beforePrint() {
        System.out.println("Logger beforePrint run ......");
    }
    
    public void afterPrint() {
        System.out.println("Logger afterPrint run ......");
    }
    
    public void afterReturningPrint() {
        System.out.println("Logger afterReturningPrint run ......");
    }
    
    public void afterThrowingPrint() {
        System.out.println("Logger afterThrowingPrint run ......");
    }
}

OK,有了这些,咱就可以开干了!

1. 基于xml的基本环境搭建【掌握】

环境搭建当然是简单的,咱先把该导入的依赖导入进去,该写的配置文件都写好。

1.1 导入Maven坐标依赖

既然是学习 SpringFramework 的 AOP ,那自然就要引入 Spring 的 AOP 模块对应的依赖:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>5.2.8.RELEASE</version>
</dependency>

注意,这里导入 aop 的依赖之后,借助 IDEA 的 Maven 窗口,可以发现 spring-aop 模块其实已经被 spring-context 模块依赖了:

Spring AOP基础-基于xml的aspect实现AOP

所以导不导 aop 的模块,当前工程中早就已经有 spring-aop 这个 jar 包的依赖啦。

1.2 编写配置文件

既然是基于 xml 配置文件的,那咱先把配置文件搞定。

在工程的 resources 目录下新建一个 xmlaspect.xml 文件,并首先把上面提到的几个类都注册进 IOC 容器中:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="financeService" class="com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService"/>

    <bean id="orderService" class="com.linkedbear.spring.aop.a_xmlaspect.service.impl.OrderServiceImpl"/>

    <bean id="logger" class="com.linkedbear.spring.aop.a_xmlaspect.component.Logger"/>
</beans>

1.3 测试运行

先不干任何多余的事情,直接编写启动类,驱动 IOC 容器并取出 FinanceService ,调用它的方法:

public class XmlAspectApplication {
    
    public static void main(String[] args) throws Exception {
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("aop/xmlaspect.xml");
        FinanceService financeService = ctx.getBean(FinanceService.class);
        financeService.addMoney(123.45);
        System.out.println(financeService.getMoneyById("abc"));
    }
}

运行 main 方法,控制台打印原生的对象输出的结果:

FinanceService 收钱 === 123.45
FinanceService 查询账户,id为abc
0.08681906193896294

至此,这些都是在前面 IOC 的基础内容了,接下来才是正儿八经的基于 xml 的 AOP 。

2. 基于xml的AOP实现【掌握】

要配置 xml 的 AOP ,需要几个步骤,咱一步一步来。

2.1 导入命名空间

要编写 AOP 的配置,需要在 xml 上导入命名空间:

<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.xsd 
                           http://www.springframework.org/schema/aop 
                           https://www.springframework.org/schema/aop/spring-aop.xsd">

如果小伙伴使用 IDEA 的话,该步骤可以跳过,IDEA 会帮我们加的。

然后,在配置文件中按提示键,会发现多了 3 个 aop 开头的标签:

Spring AOP基础-基于xml的aspect实现AOP

2.2 编写aop配置

接下来就要利用上面的这三个标签中的 <aop:config> 来配置 AOP 了。这个配置也比较简单,就两步。第一步要先声明一个切面:

<bean id="logger" class="com.linkedbear.spring.aop.a_xmlaspect.component.Logger"/>

<aop:config>
    <aop:aspect id="loggerAspect" ref="logger">
        
    </aop:aspect>
</aop:config>

一个 aspect 就是一个切面,id 随便起,只要是全局唯一即可;ref 跟 IOC 部分提到的 ref 一样,都是引用容器中的某个 bean ,这里咱要使用 Logger 作为切面类,所以 ref 就引用 logger 这个 bean 。

接下来,咱要配置一下通知类型。上一章咱说过了 Spring 一共有 5 种通知类型,这里咱先配置一个前置通知:

<bean id="logger" class="com.linkedbear.spring.aop.a_xmlaspect.component.Logger"/>

<aop:config>
    <aop:aspect id="loggerAspect" ref="logger">
        <aop:before method="beforePrint"
                    pointcut="??????"/>
    </aop:aspect>
</aop:config>

有了通知方法 method 了,切入点怎么搞定呢?哎,这里咱要学习一个新的知识点:切入点表达式

2.3 切入点表达式入门

最开始学习切入点表达式,咱先介绍最最常用的一种写法,而且这种写法刚好对标的就是 AOP 术语中的切入点

这样,小册先写一个,小伙伴们先瞅瞅这都什么含义:

execution(public void com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.addMoney(double))

是不是貌似还有点门道呢?下面咱来解释这个表达式的含义:

  • execution :以此法编写的切入点表达式,将使用方法定位的模式匹配连接点
    • 说白了,用 execution 写出来的表达式,都是直接声明到类中的方法的
  • public :限定只切入 public 类型的方法
  • void :限定只切入返回值类型为 void 的方法
  • com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService :限定只切入 FinanceService 这个类的方法
  • addMoney :限定只切入方法名为 addMoney 的方法
  • (double) :限定只切入方法的参数列表为一个参数,且类型为 double 的方法

所以,用这个表达式,就可以直接锁定到上面 FinanceService 的 addMoney 方法。

2.4 应用切入点表达式

接下来咱把上面写好的切入点表达式填到 pointcut 里:

<bean id="logger" class="com.linkedbear.spring.aop.a_xmlaspect.component.Logger"/>

<aop:config>
    <aop:aspect id="loggerAspect" ref="logger">
        <aop:before method="beforePrint"
                    pointcut="execution(public void com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.addMoney(double))"/>
    </aop:aspect>
</aop:config>

写完之后,使用 IDEA 的小伙伴会发现在 aop:before 的左边多了一个标识:

Spring AOP基础-基于xml的aspect实现AOP

点过去,会立马跳转到 FinanceService 的类中,并且 addMoney 方法的左边也有一个标识:

Spring AOP基础-基于xml的aspect实现AOP

这说明 IDEA 也知道这个切入点表达式的作用范围了。所以接下来咱即便不运行代码,光看这个标识也能知道切入点表达式的作用范围了。

2.5 测试运行

编写测试启动类,使用 xml 配置文件驱动 IOC 容器,并从 IOC 容器中取出 FinanceService ,分别执行它的三个方法:

public class XmlAspectApplication {
    
    public static void main(String[] args) throws Exception {
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("aop/xmlaspect.xml");
        FinanceService financeService = ctx.getBean(FinanceService.class);
        financeService.addMoney(123.45);
        financeService.subtractMoney(543.21);
        financeService.getMoneyById("abc");
    }
}

运行 main 方法,控制台打印了 Logger 的前置通知方法 beforePrint :

Logger beforePrint run ......
FinanceService 收钱 === 123.45
FinanceService 付钱 === 543.21
FinanceService 查询账户,id为abc

确实,上面编写的切入点表达式已经生效了,AOP 的效果得以体现。

3. 切入点表达式的多种写法【掌握】

咱继续讲解切入点表达式的编写方式哈。切入点表达式的写法比较多,咱先掌握 execution 风格写法,后面再学习更多的风格。

3.1 基本通配符

把上面的切入点表达式改一下,看看小伙伴们是否能猜得到它的含义:

execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.*(double))

还是很好猜的吧!这里有两个地方替换成了通配符 * ,咱解释一下它的含义:

  • void 的位置替换为 * ,代表不限制返回值类型,是什么都可以
  • FinanceService.*(double) 这里面的方法值替换为 * ,代表不限制方法名,什么方法都可以切入

所以,这样被切入的方法就变多了,除了 addMoney 方法之外,subtractMoney 也应该被切入了。

是不是这样呢,咱可以继续配置一个方法来检验一下。在 aop:config 中,继续添加后置通知:

<aop:config>
    <aop:aspect id="loggerAspect" ref="logger">
        <aop:before method="beforePrint"
                    pointcut="execution(public void com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.addMoney(double))"/>
        <aop:after method="afterPrint"
                   pointcut="execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.*(double))"/>
    </aop:aspect>
</aop:config>

其它的不需要任何改动,直接运行 main 方法,控制台会打印两次 afterPrint 方法,分别是 addMoney 与 subtractMoney 方法的调用,证明确实切到了两个方法。

Logger beforePrint run ......
FinanceService 收钱 === 123.45
Logger afterPrint run ......
FinanceService 付钱 === 543.21
Logger afterPrint run ......
FinanceService 查询账户,id为abc

注意:这个方法参数中,对于基本数据类型,直接声明即可;引用数据类型则要写类的全限定名

3.2 方法通配符

继续修改上面的切入点表达式:

execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.*(*))

这次的参数列表中标注了一个 * ,它代表方法的参数列表中必须有一个参数,至于类型那无所谓。

将 aop:after 的切入点表达式换为上面的写法,重新运行 main 方法,会发现 getMoneyById 方法也生效了:

Logger beforePrint run ......
FinanceService 收钱 === 123.45
Logger afterPrint run ......
FinanceService 付钱 === 543.21
Logger afterPrint run ......
FinanceService 查询账户,id为abc
Logger afterPrint run ......

3.3 类名通配符

咱继续变化切入点表达式:

execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.*.*(*))

这次连类名都任意了,所以这下 OrderService 接口也会被切入了。

咱继续编写一个 aop:after-returning 的通知:

<aop:config>
    <aop:aspect id="loggerAspect" ref="logger">
        <aop:before method="beforePrint"
                    pointcut="execution(public void com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.addMoney(double))"/>
        <aop:after method="afterPrint"
                   pointcut="execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.*(..))"/>
        <aop:after-returning method="afterReturningPrint"
                             pointcut="execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.*.*(..))"/>
    </aop:aspect>
</aop:config>

然后咱点击 aop:after-returning 标签左边的通知标识,发现 OrderService 的实现类也被切入了!

Spring AOP基础-基于xml的aspect实现AOP

所以我们又得知一个关键点:如果切入点表达式覆盖到了接口,那么如果这个接口有实现类,则实现类上的接口方法也会被切入增强

3.3 方法任意通配

如果我们重载一个 subtractMoney 方法,在方法的参数列表加上一个 id :

public double subtractMoney(double money, String id) {
    System.out.println("FinanceService 付钱 === " + money);
    return money;
}

注意写完这个方法后,IDEA 的左边并没有切入点的影响:

Spring AOP基础-基于xml的aspect实现AOP

说明 (*) 并不能切入两个参数的方法。那如果我想无论方法参数有几个,甚至没有参数,我都想切入,那该怎么写呢?

答案是换用 .. ,就像这样:

execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.*(..))

这样写完再切到 FinanceService 的类中,就发现所有方法都被切入了。

3.4 包名通配符

与类名、方法名的通配符一样,一个 * 代表一个目录,比如下面的这个切入点表达式:

execution(public * com.linkedbear.spring.aop.a_xmlaspect.*.*.*(..))

它代表的是切入 com.linkedbear.spring.aop.a_xmlaspect 包下的一级包下的任意类的任意方法(好绕。。。)。

注入 com.linkedbear.spring.aop.a_xmlaspect.controller 、com.linkedbear.spring.aop.a_xmlaspect.service 、com.linkedbear.spring.aop.a_xmlaspect.dao 等包下的所有类,都会被切到。

如果要切多级包怎么办呢?总不能一个 * 接着一个 * 写吧!所以方法参数列表中的 .. 在这里也能用:

execution(public * com.linkedbear.spring..*.*(..))

这个切入点表达式就代表 com.linkedbear.spring 包下的所有类的所有方法都会被切入。

最后多说一嘴,public 这个访问修饰符可以直接省略不写,代表切入所有访问修饰符的方法,那就相当于变成了这样:

execution(* com.linkedbear.spring..*.*(..))

3.5 抛出异常的切入

最后说下抛出异常的切入,对于某些显式声明了会抛出异常的方法,可以使用异常通知来切入这部分方法。

例如咱给 subtractMoney 方法添加一个 Exception 的抛出:

public double subtractMoney(double money, String id) throws Exception {
    System.out.println("FinanceService 付钱 === " + money);
    return money;
}

这样,在切入方法时,可以在类名方法名后面加上 throws 的异常类型即可:

execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.*(..) throws java.lang.Exception)

好了,到这里基本上 execution 风格的切入点表达式写法就差不多了,小伙伴们多多练习几个写法,并配合着 IDE 和测试代码,一定要掌握呀。

好,咱这一章就开始学习 AOP 的使用了,在开始之前呢,咱要先构建好一套测试代码。

本章源码均在 com.linkedbear.spring.aop.a_xmlaspect 目录。

0. 测试代码的搭建

为了接下来的演示更具有通用性,咱这里造一个 Service 层的接口,一个接口的实现类,一个普通的 Service 类,以及一个切面类 Logger :

OrderService :

public interface OrderService {
    
    void createOrder();
    
    void deleteOrderById(String id);
    
    String getOrderById(String id);
    
    List<String> findAll();
}

OrderServiceImpl :

public class OrderServiceImpl implements OrderService {
    
    @Override
    public void createOrder() {
        System.out.println("OrderServiceImpl 创建订单。。。");
    }
    
    @Override
    public void deleteOrderById(String id) {
        System.out.println("OrderServiceImpl 删除订单,id为" + id);
    }
    
    @Override
    public String getOrderById(String id) {
        System.out.println("OrderServiceImpl 查询订单,id为" + id);
        return id;
    }
    
    @Override
    public List<String> findAll() {
        System.out.println("OrderServiceImpl 查询所有订单。。。");
        return Arrays.asList("111", "222", "333");
    }
}

FinanceService :

public class FinanceService {
    
    public void addMoney(double money) {
        System.out.println("FinanceService 收钱 === " + money);
    }
    
    public double subtractMoney(double money) {
        System.out.println("FinanceService 付钱 === " + money);
        return money;
    }
    
    public double getMoneyById(String id) {
        System.out.println("FinanceService 查询账户,id为" + id);
        return Math.random();
    }
}

Logger 切面类:

public class Logger {
    
    public void beforePrint() {
        System.out.println("Logger beforePrint run ......");
    }
    
    public void afterPrint() {
        System.out.println("Logger afterPrint run ......");
    }
    
    public void afterReturningPrint() {
        System.out.println("Logger afterReturningPrint run ......");
    }
    
    public void afterThrowingPrint() {
        System.out.println("Logger afterThrowingPrint run ......");
    }
}

OK,有了这些,咱就可以开干了!

1. 基于xml的基本环境搭建【掌握】

环境搭建当然是简单的,咱先把该导入的依赖导入进去,该写的配置文件都写好。

1.1 导入Maven坐标依赖

既然是学习 SpringFramework 的 AOP ,那自然就要引入 Spring 的 AOP 模块对应的依赖:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>5.2.8.RELEASE</version>
</dependency>

注意,这里导入 aop 的依赖之后,借助 IDEA 的 Maven 窗口,可以发现 spring-aop 模块其实已经被 spring-context 模块依赖了:

Spring AOP基础-基于xml的aspect实现AOP

所以导不导 aop 的模块,当前工程中早就已经有 spring-aop 这个 jar 包的依赖啦。

1.2 编写配置文件

既然是基于 xml 配置文件的,那咱先把配置文件搞定。

在工程的 resources 目录下新建一个 xmlaspect.xml 文件,并首先把上面提到的几个类都注册进 IOC 容器中:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="financeService" class="com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService"/>

    <bean id="orderService" class="com.linkedbear.spring.aop.a_xmlaspect.service.impl.OrderServiceImpl"/>

    <bean id="logger" class="com.linkedbear.spring.aop.a_xmlaspect.component.Logger"/>
</beans>

1.3 测试运行

先不干任何多余的事情,直接编写启动类,驱动 IOC 容器并取出 FinanceService ,调用它的方法:

public class XmlAspectApplication {
    
    public static void main(String[] args) throws Exception {
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("aop/xmlaspect.xml");
        FinanceService financeService = ctx.getBean(FinanceService.class);
        financeService.addMoney(123.45);
        System.out.println(financeService.getMoneyById("abc"));
    }
}

运行 main 方法,控制台打印原生的对象输出的结果:

FinanceService 收钱 === 123.45
FinanceService 查询账户,id为abc
0.08681906193896294

至此,这些都是在前面 IOC 的基础内容了,接下来才是正儿八经的基于 xml 的 AOP 。

2. 基于xml的AOP实现【掌握】

要配置 xml 的 AOP ,需要几个步骤,咱一步一步来。

2.1 导入命名空间

要编写 AOP 的配置,需要在 xml 上导入命名空间:

<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.xsd 
                           http://www.springframework.org/schema/aop 
                           https://www.springframework.org/schema/aop/spring-aop.xsd">

如果小伙伴使用 IDEA 的话,该步骤可以跳过,IDEA 会帮我们加的。

然后,在配置文件中按提示键,会发现多了 3 个 aop 开头的标签:

Spring AOP基础-基于xml的aspect实现AOP

2.2 编写aop配置

接下来就要利用上面的这三个标签中的 <aop:config> 来配置 AOP 了。这个配置也比较简单,就两步。第一步要先声明一个切面:

<bean id="logger" class="com.linkedbear.spring.aop.a_xmlaspect.component.Logger"/>

<aop:config>
    <aop:aspect id="loggerAspect" ref="logger">
        
    </aop:aspect>
</aop:config>

一个 aspect 就是一个切面,id 随便起,只要是全局唯一即可;ref 跟 IOC 部分提到的 ref 一样,都是引用容器中的某个 bean ,这里咱要使用 Logger 作为切面类,所以 ref 就引用 logger 这个 bean 。

接下来,咱要配置一下通知类型。上一章咱说过了 Spring 一共有 5 种通知类型,这里咱先配置一个前置通知:

<bean id="logger" class="com.linkedbear.spring.aop.a_xmlaspect.component.Logger"/>

<aop:config>
    <aop:aspect id="loggerAspect" ref="logger">
        <aop:before method="beforePrint"
                    pointcut="??????"/>
    </aop:aspect>
</aop:config>

有了通知方法 method 了,切入点怎么搞定呢?哎,这里咱要学习一个新的知识点:切入点表达式

2.3 切入点表达式入门

最开始学习切入点表达式,咱先介绍最最常用的一种写法,而且这种写法刚好对标的就是 AOP 术语中的切入点

这样,小册先写一个,小伙伴们先瞅瞅这都什么含义:

execution(public void com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.addMoney(double))

是不是貌似还有点门道呢?下面咱来解释这个表达式的含义:

  • execution :以此法编写的切入点表达式,将使用方法定位的模式匹配连接点
    • 说白了,用 execution 写出来的表达式,都是直接声明到类中的方法的
  • public :限定只切入 public 类型的方法
  • void :限定只切入返回值类型为 void 的方法
  • com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService :限定只切入 FinanceService 这个类的方法
  • addMoney :限定只切入方法名为 addMoney 的方法
  • (double) :限定只切入方法的参数列表为一个参数,且类型为 double 的方法

所以,用这个表达式,就可以直接锁定到上面 FinanceService 的 addMoney 方法。

2.4 应用切入点表达式

接下来咱把上面写好的切入点表达式填到 pointcut 里:

<bean id="logger" class="com.linkedbear.spring.aop.a_xmlaspect.component.Logger"/>

<aop:config>
    <aop:aspect id="loggerAspect" ref="logger">
        <aop:before method="beforePrint"
                    pointcut="execution(public void com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.addMoney(double))"/>
    </aop:aspect>
</aop:config>

写完之后,使用 IDEA 的小伙伴会发现在 aop:before 的左边多了一个标识:

Spring AOP基础-基于xml的aspect实现AOP

点过去,会立马跳转到 FinanceService 的类中,并且 addMoney 方法的左边也有一个标识:

Spring AOP基础-基于xml的aspect实现AOP

这说明 IDEA 也知道这个切入点表达式的作用范围了。所以接下来咱即便不运行代码,光看这个标识也能知道切入点表达式的作用范围了。

2.5 测试运行

编写测试启动类,使用 xml 配置文件驱动 IOC 容器,并从 IOC 容器中取出 FinanceService ,分别执行它的三个方法:

public class XmlAspectApplication {
    
    public static void main(String[] args) throws Exception {
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("aop/xmlaspect.xml");
        FinanceService financeService = ctx.getBean(FinanceService.class);
        financeService.addMoney(123.45);
        financeService.subtractMoney(543.21);
        financeService.getMoneyById("abc");
    }
}

运行 main 方法,控制台打印了 Logger 的前置通知方法 beforePrint :

Logger beforePrint run ......
FinanceService 收钱 === 123.45
FinanceService 付钱 === 543.21
FinanceService 查询账户,id为abc

确实,上面编写的切入点表达式已经生效了,AOP 的效果得以体现。

3. 切入点表达式的多种写法【掌握】

咱继续讲解切入点表达式的编写方式哈。切入点表达式的写法比较多,咱先掌握 execution 风格写法,后面再学习更多的风格。

3.1 基本通配符

把上面的切入点表达式改一下,看看小伙伴们是否能猜得到它的含义:

execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.*(double))

还是很好猜的吧!这里有两个地方替换成了通配符 * ,咱解释一下它的含义:

  • void 的位置替换为 * ,代表不限制返回值类型,是什么都可以
  • FinanceService.*(double) 这里面的方法值替换为 * ,代表不限制方法名,什么方法都可以切入

所以,这样被切入的方法就变多了,除了 addMoney 方法之外,subtractMoney 也应该被切入了。

是不是这样呢,咱可以继续配置一个方法来检验一下。在 aop:config 中,继续添加后置通知:

<aop:config>
    <aop:aspect id="loggerAspect" ref="logger">
        <aop:before method="beforePrint"
                    pointcut="execution(public void com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.addMoney(double))"/>
        <aop:after method="afterPrint"
                   pointcut="execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.*(double))"/>
    </aop:aspect>
</aop:config>

其它的不需要任何改动,直接运行 main 方法,控制台会打印两次 afterPrint 方法,分别是 addMoney 与 subtractMoney 方法的调用,证明确实切到了两个方法。

Logger beforePrint run ......
FinanceService 收钱 === 123.45
Logger afterPrint run ......
FinanceService 付钱 === 543.21
Logger afterPrint run ......
FinanceService 查询账户,id为abc

注意:这个方法参数中,对于基本数据类型,直接声明即可;引用数据类型则要写类的全限定名

3.2 方法通配符

继续修改上面的切入点表达式:

execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.*(*))

这次的参数列表中标注了一个 * ,它代表方法的参数列表中必须有一个参数,至于类型那无所谓。

将 aop:after 的切入点表达式换为上面的写法,重新运行 main 方法,会发现 getMoneyById 方法也生效了:

Logger beforePrint run ......
FinanceService 收钱 === 123.45
Logger afterPrint run ......
FinanceService 付钱 === 543.21
Logger afterPrint run ......
FinanceService 查询账户,id为abc
Logger afterPrint run ......

3.3 类名通配符

咱继续变化切入点表达式:

execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.*.*(*))

这次连类名都任意了,所以这下 OrderService 接口也会被切入了。

咱继续编写一个 aop:after-returning 的通知:

<aop:config>
    <aop:aspect id="loggerAspect" ref="logger">
        <aop:before method="beforePrint"
                    pointcut="execution(public void com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.addMoney(double))"/>
        <aop:after method="afterPrint"
                   pointcut="execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.*(..))"/>
        <aop:after-returning method="afterReturningPrint"
                             pointcut="execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.*.*(..))"/>
    </aop:aspect>
</aop:config>

然后咱点击 aop:after-returning 标签左边的通知标识,发现 OrderService 的实现类也被切入了!

Spring AOP基础-基于xml的aspect实现AOP

所以我们又得知一个关键点:如果切入点表达式覆盖到了接口,那么如果这个接口有实现类,则实现类上的接口方法也会被切入增强

3.3 方法任意通配

如果我们重载一个 subtractMoney 方法,在方法的参数列表加上一个 id :

public double subtractMoney(double money, String id) {
    System.out.println("FinanceService 付钱 === " + money);
    return money;
}

注意写完这个方法后,IDEA 的左边并没有切入点的影响:

Spring AOP基础-基于xml的aspect实现AOP

说明 (*) 并不能切入两个参数的方法。那如果我想无论方法参数有几个,甚至没有参数,我都想切入,那该怎么写呢?

答案是换用 .. ,就像这样:

execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.*(..))

这样写完再切到 FinanceService 的类中,就发现所有方法都被切入了。

3.4 包名通配符

与类名、方法名的通配符一样,一个 * 代表一个目录,比如下面的这个切入点表达式:

execution(public * com.linkedbear.spring.aop.a_xmlaspect.*.*.*(..))

它代表的是切入 com.linkedbear.spring.aop.a_xmlaspect 包下的一级包下的任意类的任意方法(好绕。。。)。

注入 com.linkedbear.spring.aop.a_xmlaspect.controller 、com.linkedbear.spring.aop.a_xmlaspect.service 、com.linkedbear.spring.aop.a_xmlaspect.dao 等包下的所有类,都会被切到。

如果要切多级包怎么办呢?总不能一个 * 接着一个 * 写吧!所以方法参数列表中的 .. 在这里也能用:

execution(public * com.linkedbear.spring..*.*(..))

这个切入点表达式就代表 com.linkedbear.spring 包下的所有类的所有方法都会被切入。

最后多说一嘴,public 这个访问修饰符可以直接省略不写,代表切入所有访问修饰符的方法,那就相当于变成了这样:

execution(* com.linkedbear.spring..*.*(..))

3.5 抛出异常的切入

最后说下抛出异常的切入,对于某些显式声明了会抛出异常的方法,可以使用异常通知来切入这部分方法。

例如咱给 subtractMoney 方法添加一个 Exception 的抛出:

public double subtractMoney(double money, String id) throws Exception {
    System.out.println("FinanceService 付钱 === " + money);
    return money;
}

这样,在切入方法时,可以在类名方法名后面加上 throws 的异常类型即可:

execution(public * com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService.*(..) throws java.lang.Exception)

好了,到这里基本上 execution 风格的切入点表达式写法就差不多了,小伙伴们多多练习几个写法,并配合着 IDE 和测试代码,一定要掌握呀。

 

免责声明:
1.本站所有内容由本站原创、网络转载、消息撰写、网友投稿等几部分组成。
2.本站原创文字内容若未经特别声明,则遵循协议CC3.0共享协议,转载请务必注明原文链接。
3.本站部分来源于网络转载的文章信息是出于传递更多信息之目的,不意味着赞同其观点。
4.本站所有源码与软件均为原作者提供,仅供学习和研究使用。
5.如您对本网站的相关版权有任何异议,或者认为侵犯了您的合法权益,请及时通知我们处理。
火焰兔 » Spring AOP基础-基于xml的aspect实现AOP