在 Spring AOP 中,有这几种概念通常令人晦涩难懂,但又必须理解,下面将用通俗易懂的方式介绍它们:
切面(Aspect):
可以将切面理解为一个横切关注点的模块化单元。它是一个包含了一组横切关注点的类,它定义了在哪里和何时应该执行这些横切关注点。例如,日志记录、事务管理等都可以作为一个切面。
切点(Pointcut):
切点就是在应用程序中定义的一个或多个特定的方法,它们被用来确定切面在何处使用。切点可以通过表达式或者正则表达式等方式来定义。例如,当方法名以"save"开头时,就可以作为一个切点。(后面介绍)
连接点(Joinpoint):
连接点是在切面中可能触发切点的点。它代表了在程序执行过程中,切面可以插入的位置。例如,方法的调用、方法的执行、异常的抛出等都可以作为连接点。
通知(Advice):
通知是切面在特定连接点上执行的动作。它定义了在切点处执行的代码逻辑。通知可以在连接点之前、之后或者环绕连接点执行。例如,前置通知在方法执行之前执行,后置通知在方法执行之后执行。
可以通过一个简单的例子来说明这些概念:假设有一个日志切面,它在程序中的所有方法执行之前和之后记录日志。那么,切面就是这个日志切面,它定义了在何处和何时记录日志。切点就是指定了哪些方法需要被记录日志,例如所有的Service层方法。连接点就是这些方法的调用或执行的位置。通知就是在连接点之前或之后执行的记录日志的动作。
下面将实现一个AOP的逻辑,主要功能为:当访问登录、注册等功能时,某些方法属于切点方法,有的不是切点方法,通过不同的通知方法,将需要进行验证的方法拦截下来,并观察执行的结果有何不同。
目标:学会切面、切点、连接点、通知的使用;
理解AspectJ 支持的三种通配符,以及五类通知的执行逻辑;
简单了解 AOP 实现原理;
注意:这里添加的一定是 boot 的 aop依赖,而不是 spring 的 aop 依赖;
org.springframework.boot
spring-boot-starter-web
创建切面需要引入 @Aspect 注解,以及容器相关五大类注解之一;
- 用于标识一个类为切面类,Spring框架会将该类识别为一个切面,并根据其中的通知和切点等定义,自动将其织入到目标对象的方法中;
- 定义切点和通知:切面类中可以定义切点和通知等相关内容,通过@Pointcut、@Before、@After等注解来实现。切点用于指定在哪些连接点上应用通知;
当前这个 UserAOP 类就作为一个切面类,在该切面内,将实现注册拦截规则,通知方法等功能;
在切面 UserAOP 中创建切点:
切点用来定义拦截规则,写一个空方法,不实现,加上注解,实现在通知中,拦截规则精确到参数部分;
说明:
execution 表示执行;
这里 execution 里面的路径表示要拦截的是UserController类里所有的方法,如果要拦截具体的方法,就注明其方法名;
*(..) 表示参数是什么:
如果是 .. 表示动态参数,要拦截的方法传参是任意参数的;
如果写作 *(int,String) 就表示拦截传参顺序为先 int 后 String 的方法;
pointcut() 方法说明了后续只要调用这个方法,就要按照注解逻辑执行拦截操作;
关于这里具体参数的规则,将在下面 理解切点表达式的规则 中详细介绍。
通知用来拦截下来具体要做什么,比如可以验证当前用户是否登录、验证身份信息是否正确、验证用户状态是否安全等等。就好比交警在路上拦下人,要检查一下驾驶证一样。
通知的类型有五种:
前置通知(@Before):在目标方法执行之前执行的通知。可以在前置通知中进行一些准备工作,例如参数校验、权限验证等。
后置通知(@After):在目标方法执行之后执行的通知。无论目标方法是否抛出异常,后置通知都会执行。可以在后置通知中进行一些清理工作,例如释放资源、记录日志等。
返回通知(@AfterReturning):在目标方法正常返回后执行的通知。可以获取目标方法的返回值,并对返回值进行处理或记录。
异常通知(@AfterThrowing):在目标方法抛出异常后执行的通知。可以捕获目标方法抛出的异常,并进行相应的处理,例如发送告警、记录日志等。
环绕通知(@Around):在目标方法执行前后都执行的通知。可以完全控制目标方法的执行过程,包括是否调用目标方法、修改参数、修改返回值等。
这五种通知类型可以根据需要选择使用,用于在目标方法的不同阶段执行相应的操作。可以根据具体的业务需求,选择合适的通知类型来实现横切关注点的功能。
下面我们按照:前置通知 —— 后置通知 —— 环绕通知 —— 返回通知 —— 异常通知 的顺序,验证它们各自的逻辑,以及相互的关系。
(1)继续在刚才 UserAOP 切面中添加一个前置通知:
前置方法使用 @Before 注解,注解里注明切点方法;
因为连接点是会触发拦截规则的,也就是触发切点方法,因此根据先前设定好的要拦截的方法,添加一个类 UserController,这个类中的所有方法都将被拦截到:
显而易见,该类中有两个方法:sayHi() 和 login() 都作为连接点;
另外我们在创建一个非连接点,用于对比验证:
添加一个类 ArticleController,也实现一个方法 sayHi() :
到此,就可以执行对比结果了:
这里要提醒的是,由于本篇只是介绍和了解AOP思想及执行逻辑,所以通知方法中不涉及到具体的业务功能,只是一个简单的日志看一下它是如何执行的。
继续在切面类 UserAOP 中添加一个后置通知,使用 @After() 注解:
如下图:由于此时前置通知和后置通知都存在,所以会按照:【前置通知 —— 目标方法 —— 后置通知】 的顺序走一遍:
使用环绕通知,可以在目标方法执行之前和之后都执行自定义的逻辑
在环绕通知方法中,通过ProceedingJoinPoint 来给环绕通知方法传入目标方法本身。
可以使用 proceed() 方法来调用目标方法,也可以选择不调用,从而实现拦截目标方法的效果。
在调用 proceed() 方法之后,可以获取目标方法的返回值,并进行相应的处理或记录。
如果目标方法抛出异常,环绕通知方法可以捕获并进行相应的处理,例如发送告警、记录日志等。
继续在切面类 UserAOP 中添加一个环绕通知方法,使用 @Around 注解:
此时,前置通知、后置通知、环绕通知三者同时存在的情况下,看如何执行的:
按照:开始环绕通知 —— 前置通知 —— 目标方法 —— 后置通知 —— 结束环绕通知 的顺序执行
如果去掉前置通知合成后置通知,只有环绕通知的情况下:
在上面环节中,有一个环节是创建切点,它在定义拦截规则时,execution 后面的参数是一个长表达式,这里就会展开介绍。
切点表达式由切点函数组成,execution、within、this、target、args等,其中execution()是最常用的切点函数,用来匹配方法,语法为:
execution(<修饰符><返回类型><包.类.方法(参数)><异常>)
修饰符和异常一般可以省略:
* :匹配任意字符,只匹配⼀个元素(包,类,或方法,方法参数);
.. :匹配任意字符,可以匹配多个元素 ,在表示类时,必须和 * 联合使用;
+ :表示按照类型匹配指定类的所有子类(包括自身),必须跟在类名后面,如 com.cad.Car+ ,表示继承该类的 所有子类包括本身;
下面用具体例子分析:
- execution(* com.cad.demo.User.*(..)) :匹配 User 类里的所有方法。
- execution(* com.cad.demo.User+.*(..)) :匹配该类的子类包括该类的所有方法。
- execution(* com.cad.*.*(..)) :匹配 com.cad 包下的所有类的所有方法。
- execution(* com.cad..*.*(..)) :匹配 com.cad 包下、子孙包下所有类的所有方法。
- execution(* addUser(String, int)) :匹配 addUser 方法,且第⼀个参数类型是 String,第⼆个 参数类型是 int。
Spring AOP的实现原理可以用一个生活中的例子来解释:
假设你是一个家庭主妇,每天要做很多家务,比如做饭、洗衣服、打扫卫生等。但是你发现每次做饭的时候,都需要先洗菜、切菜、炒菜等一系列的操作,这些操作其实是和每一顿饭都有关系的,而不仅仅是某一顿饭。
于是你决定引入一个帮手来帮你处理这些和做饭相关的操作,这个帮手就是Spring AOP。它的实现原理就是在你做饭的过程中,帮你自动完成洗菜、切菜、炒菜等操作,而不需要你每次都亲自去做。
通过引入帮手,你不仅解放了自己的双手,还能更专注于做好饭菜本身。同样地,Spring AOP通过动态生成代理对象,在目标方法执行前、执行后或抛出异常时,自动插入相应的通知,帮助开发者更专注于业务逻辑的实现,而不需要关心横切关注点的处理。
具体来说,Spring AOP实现原理包括以下几个步骤:
定义切面:通过编写切面类,使用注解或XML配置来定义通知和切点表达式。
创建代理对象:当Spring容器启动时,会扫描并解析切面类,根据切面类中的通知和切点表达式,动态生成代理对象。
代理对象的生成方式有两种:
基于JDK动态代理:如果目标对象实现了接口(因为JDK动态代理是基于接口的代理方式,使用目标对象至少要实现一个接口,这个接口可以是自定义的接口,也可以是Spring提供的AOP相关接口),Spring AOP会使用JDK动态代理来生成代理对象。在运行时,通过实现InvocationHandler接口,通过反射,将目标对象的方法调用委托给通知对象。
基于CGLIB动态代理:如果目标对象没有实现接口,Spring AOP会使用CGLIB动态代理来生成代理对象。在运行时,通过继承目标对象的子类,并重写其方法,将方法调用委托给通知对象。
代理对象的应用:当目标对象的方法被调用时,代理对象会拦截方法调用,并根据切点表达式判断是否应用通知。如果切点表达式匹配成功,则会在目标方法执行前、执行后或抛出异常时,调用相应的通知方法。
总之,Spring AOP的实现原理是基于动态代理的方式。通过动态生成代理对象,并在目标方法调用时拦截并应用通知,实现对目标对象的增强。这种方式使得开发人员可以将横切关注点与业务逻辑分离,提高了代码的可维护性和可重用性。