Spring AOP实现方式(xml&注解)

1. 基于xml的aspect实现AOP

1.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

Logger 切面类:

1.1 基于xml的基本环境搭建

1.1.1 导入Maven坐标依赖

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

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

img

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

1.1.2 编写配置文件

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

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

1.1.3 测试运行

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

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

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

1.2 基于xml的AOP实现

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

1.2.1 导入命名空间

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

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

img

1.2.2 编写aop配置

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

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

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

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

1.2.3 切入点表达式入门

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

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

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

  • execution :以此法编写的切入点表达式,将使用方法定位的模式匹配连接点

    • 说白了,用 execution 写出来的表达式,都是直接声明到类中的方法的

  • public :限定只切入 public 类型的方法

  • void :限定只切入返回值类型为 void 的方法

  • com.linkedbear.spring.aop.a_xmlaspect.service.FinanceService :限定只切入 FinanceService 这个类的方法

  • addMoney :限定只切入方法名为 addMoney 的方法

  • (double) :限定只切入方法的参数列表为一个参数,且类型为 double 的方法

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

1.2.4 应用切入点表达式

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

1.2.5 测试运行

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

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

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

1.3 切入点表达式的多种写法

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

1.3.1 基本通配符

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

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

  • void 的位置替换为 * ,代表不限制返回值类型,是什么都可以

  • FinanceService.*(double) 这里面的方法值替换为 * ,代表不限制方法名,什么方法都可以切入

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

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

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

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

1.3.2 方法通配符

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

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

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

1.3.3 类名通配符

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

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

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

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

img

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

1.3.4 方法任意通配

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

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

img

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

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

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

1.3.5 包名通配符

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

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

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

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

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

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

1.3.6 抛出异常的切入

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

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

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

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

2. 基于AspectJ实现AOP

2.1 Spring AOP与AspectJ

在 SpringFramework 的官方文档中,AOP 的介绍下面有一个段落,它说明了 Spring AOP 与 AspectJ 的关系:

docs.spring.io/spring-fram…

Spring provides simple and powerful ways of writing custom aspects by using either a schema-based approach or the @AspectJ annotation style. Both of these styles offer fully typed advice and use of the AspectJ pointcut language while still using Spring AOP for weaving.

Spring 通过使用基于模式的方法或 @AspectJ 注解样式,提供了编写自定义切面的简单而强大的方法。这两种样式都提供了完全类型化的通知,并使用了 AspectJ 切入点表达式语言,同时仍使用 Spring AOP 进行通知的织入。

由此可知,SpringFramework 实现注解配置 AOP ,是整合了 AspectJ 完成的。在第 41 章中,小册也提到了 SpringFramework 中的通知类型就是基于 AspectJ 制定的:

  • Before 前置通知:目标对象的方法调用之前触发

  • After 后置通知:目标对象的方法调用之后触发

  • AfterReturning 返回通知:目标对象的方法调用完成,在返回结果值之后触发

  • AfterThrowing 异常通知:目标对象的方法运行中抛出 / 触发异常后触发

  • Around 环绕通知:编程式控制目标对象的方法调用

2.2 基于注解的AOP配置

2.2.1 标注@Component注解

上一章中咱注册 Bean 是使用 <bean> 标签的方式注册,这一章咱使用注解驱动,那就在两个 Service 类上标注 @Component 注解:

2.2.2 修改Logger切面类

这次使用 AspectJ 注解配置,切面类上也得做改动了。

首先,在 Logger 上标注 @Component 注解,将其注册到 IOC 容器中。然后还得标注一个 **@Aspect** 注解,代表该类是一个切面类:

接下来,就是给这些方法标注通知注解了。小册先写一个,小伙伴们一下子就知道了:

嚯,这也太简单了是吧!那前置通知叫 @Before ,那后置通知就是 @After 咯?当然啦,相应的,返回通知 @AfterReturning ,异常通知 @AfterThrowing ,环绕通知 @Around

这些编写思路都是一样的,所以咱可以简单的这样写一下:

2.2.3 编写配置类

配置类中,无需做任何多余的操作,只需要几个注解即可:

注意这里用了一个新的注解:**@EnableAspectJAutoProxy** ,是不是突然产生了一点亲切感(模块装配 + 条件装配)!用它可以开启基于 AspectJ 的自动代理,简言之,就是开启注解 AOP

如果要使用 xml 配置文件开启注解 AOP ,则需要添加一个 <aop:aspectj-autoproxy/> 的标签声明(它等价于 @EnableAspectJAutoProxy 注解)。

2.2.4 测试运行

好啦,编写测试启动类的步骤已经很简单了吧,咱就不啰嗦了,直接上代码:

运行 main 方法,控制台打印出了 Logger 的前置 、后置 、返回通知:

2.3 环绕通知的编写

除了前面提到的 4 种基本的通知类型之外,还有环绕通知没有说。环绕通知的编写其实在第 40 章回顾动态代理的时候就已经写过了,对,InvocationHandlerMethodInterceptor 的编写本身就是环绕通知的体现。换做使用 AspectJ 的写法,又要如何来编写呢?咱也要来学习一下。

2.3.1 添加新的环绕通知方法

Logger 类中,咱添加一个 aroundPrint 方法:(切入的方法就不覆盖那么多了,一个就好)

然后咱回想一下,InvocationHandler 的结构是什么来着?得有入参,里面有对应的方法、参数,还得有返回值 Object 。。。可是这里啥也没有呀,这咱怎么写呢?

所以我们要先学习一个通知方法中的特殊参数:ProceedingJoinPoint

aroundPrint 方法的参数中添加 ProceedingJoinPoint ,并把方法的返回值类型改为 Object

然后来看,ProceedingJoinPoint 有一个 proceed 方法,执行了它,就相当于之前咱在动态代理中写的 method.invoke(target, args); 方法了:

之后剩下的部分,咱就很熟悉了,快速的来编写一下吧:

仔细观看小册的这种写法,是不是刚刚好就是上面 4 种通知的结合呀!

2.3.2 测试运行

直接重新运行 main 方法,可以发现在控制台有同时打印环绕通知和前置通知:

由此也得出了一个小小的结论:同一个切面类中,环绕通知的执行时机比单个通知要早

2.4 切入点表达式的更多使用方法

上一章咱只是在切入点表达式的学习中接触了一些比较简单的写法和用法,这一章咱继续学习更多的使用方法。

2.4.1 抽取通用切入点表达式

注意上面咱在 Logger 类中标注的切入点表达式:

这两个切入点表达式是一样的,如果这种同样的切入点表达式一多起来,回头修改起来那岂不是太费劲了?Spring 当然也为我们考虑到这一点了,所以它分别就 xml 和注解的方式提供了抽取通用表达式的方案。

2.4.1.1 AspectJ注解抽取

在注解 AOP 切面中,定义通用的切入点表达式只需要声明一个空方法,并标注 @Pointcut 注解即可:

其它的通知要引用这个切入点表达式,只需要标注方法名即可,效果是一样的。

2.4.1.2 xml抽取

相应的,在 xml 配置文件中,也有一个专门的标签来抽取,那就是 aop:pointcut

注意,要引用 xml 的切入点表达式,需要使用 pointcut-ref 而不是 pointcut 属性!

2.4.2 @annotation的使用

除了 execution 之外,还有一种切入点表达式也比较常用:@annotation()

它的使用方式就非常简单了,只需要在括号中声明注解的全限定名即可。下面咱简单演示一下。

咱还是使用 Logger 作为切面类,这次咱声明一个 @Log 注解,用于标注要打印日志的方法:

然后,切入点表达式中只需要以下声明即可:

以此法声明的切入点表达式会搜索整个 IOC 容器中标注了**@Log**注解的所有 bean 全部增强

接下来咱就在 FinanceServicesubtractMoney 方法中标注一个 @Log 注解:

重新运行 main 方法,发现只有 subtractMoney 方法有打印后置通知的日志了:

最后更新于

这有帮助吗?