目录
前言
本篇博客主要介绍Spring AOP的相关概念,Spring AOP的具体实现,Spring AOP中的切面,切点、通知连接点。以及简单介绍Spring AOP的实现原理。
一、Spring AOP是什么以及为什么要使用AOP
AOP是Aspect Oriented Programming的缩写,意思是面向切面编程。面向切面编程是一种思想,它的主要职责就是对某一方面的事情做统一的处理,这样子也可以实现类似代码复用的效果。比如在我们的一个任意网站中,有些页面是需要登录之后才有权限访问的,比如个人中心等等,那么如果没有AOP,那么我们就需要给每个需要登录验证的页面去写相应的登录验证方法,十分麻烦,然⽽有了 AOP 之后,我们只需要在某⼀处配置⼀下,所有需要判断⽤户登录⻚⾯就全部可以实现⽤户登录验证了,不再需要每个⽅法中都写相同的⽤户登录验证了。
AOP是一种思想,Spring AOP是一个框架,是对AOP思想的一种具体实现。
为什么要使用AOP:
我们使用AOP的主要目的就是为了实现统一功能处理,我们在做一个后台系统时,一般除了登录和注册功能的页面之外,都需要进行用户登录状态的校验。
如果不使用AOP的话就需要在每个controller中都去校验登录状态,而校验登录状态都是一样的代码,当功能增加时,每次都需要去增加这些固定的代码,这么多的冗余代码就会导致修改和维护的成本增加。所以对于这种统一的功能处理,我们就应该考虑AOP来进行处理了。
AOP的使用场景:
统⼀⽇志记录
统⼀⽅法执⾏时间统计
统⼀的返回格式设置
统⼀的异常处理
事务的开启和提交等
二、AOP的组成
AOP由切面、切点、通知、连接点组成,接下来我们就通过一般软件系统中都有的校验登录的拦截器为例子,来介绍AOP的组成。
2.1、切面(Aspect)
切⾯是包含了:通知、切点和切⾯的类,相当于 AOP 实现的某个功能的集合。
切面定义的是事件,(代表这个AOP是做啥的),切面对应的就是一个类,可以理解为我们一个系统中的定义拦截器的那个类就是切面。
2.2、切点(Pointcut)
切点相当于保存了众多连接点的⼀个集合(如果把切点看成⼀个表,⽽连接点就是表中⼀条⼀条的数据),
切点定义的是具体规则,例如我们系统中拦截器的规则就是如果用户在没有登录的情况下,只能访问我这个系统的登录和注册的页面,如果去访问其他页面的话就会被拦截。这个规则就是切点。
2.3、通知(Advice)
通知定义了切⾯是什么,何时使⽤,其描述了切⾯要完成的⼯作,还解决何时执⾏这个⼯作的问题。也就是通知就是AOP执行的具体方法。例如我们拦截器中,我们需要获取到用户的登录状态,进而根据用户登录状态来执行不同的操作。
通知还可以具体分为以下几个通知:
前置通知使⽤ @Before:通知⽅法会在⽬标⽅法调⽤之前执⾏。
后置通知使⽤ @After:通知⽅法会在⽬标⽅法返回或者抛出异常后调⽤。
返回之后通知使⽤ @AfterReturning:通知⽅法会在⽬标⽅法返回后调⽤。
抛异常后通知使⽤ @AfterThrowing:通知⽅法会在⽬标⽅法抛出异常后调⽤。
环绕通知使⽤ @Around:通知包裹了被通知的⽅法,在被通知的⽅法通知之前和调⽤之后执⾏⾃定义的⾏为。
2.4、连接点(Join Point)
连接点就是有可能触发切点(规则)的所有点,例如我们拦截器中所有的有可能被拦截的页面都是连接点,一般除了登录和注册的页面不会触发拦截器,其他基本都会触发拦截器所以就都是连接点。
三、Spring AOP的实现
接下来我们要使用Spring AOP来实现AOP的功能,具体要实现的就是:我们这里定义一个UserController类,接着定义一个AOP来拦截里面的所有方法,只要我们调用了UserController类中的方法,就会触发AOP执行相应的通知。
3.1、添加Spring Boot的AOP框架支持
在pom.xml中添加以下依赖(这里可以不加版本号)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
可以直接去MVN仓库找Spring Boot AOP的相关依赖:
☞Maven Repository: Central (mvnrepository.com)https://mvnrepository.com/repos/central
3.2、定义切面和切点
定义切面:切面这本质就是一个类,所以只要创建一个类并且加上@Aspect和@Componet(理论上五大类注解之一都可,但是加的注解要有词能达意):定义切面的代码如下:
定义切点:定义切点本质就是定义拦截的规则,定义拦截规则需要加@Pointcut注解,代表这是拦截规则,接着使用AspectJ的语法来定义拦截规则即可,代码如下:
这里的方法名主要用于后续通知中使用的注解需要指定定义拦截规则的方法名。 上面的代码的意思指的是我们拦截了UserController类中的所有方法。
这里对于定义拦截规则的语法等后面再介绍,这里先看代码即可。
3.3、定义连接点
定义连接点其实就是定义一个可以触发拦截规则的事件,上面我们的拦截规则是只要是UserController类中的方法都会触发拦截规则,所以这里我们只需要创建一个UserControler类,在其中定义一些方法,定义的方法就都会是连接点。
代码如下:
package com.example.blog_aop.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
@RequestMapping("/user")
@ResponseBody
public class UserController {
@RequestMapping("/sayhi")
public String sayHi(){
System.out.println("执行了sayhi方法");
return "sayhi";
}
@RequestMapping("/login")
public String login(){
System.out.println("执行了login方法");
return "login";
}
}
3.4、定义通知
各种通知的简介:
前置通知使⽤@Before:通知⽅法会在⽬标⽅法调⽤之前执⾏。
后置通知使⽤@After:通知⽅法会在⽬标⽅法返回或者抛出异常后调⽤。
返回之后通知使⽤@AfterReturning:通知⽅法会在⽬标⽅法返回后调⽤。
抛异常后通知使⽤@AfterThrowing:通知⽅法会在⽬标⽅法抛出异常后调⽤。
环绕通知使⽤@Around:通知包裹了被通知的⽅法,在被通知的⽅法通知之前和调⽤之后执 ⾏⾃定义的⾏为
前置通知和后置通知
前置通知和后置通知分别使用@Before和@After注解来实现。具体实现代码如下:
代码测试结果:
当我们去访问UserController类中的sayHi方法,我们定义的AOP就会执行对应的前置通知和后置通知了,这里我们只是让其将执行的通知打印出来而已,我们也可以在前置方法中进行别的判断操作等等。
环绕通知
环绕通知类似于是前置通知和后置通知的结合体,就是在一个方法中既会执行前置通知,也会执行后置通知,这样就有利于我们去统计一个方法的执行时间,我们可以在方法执行前去记录开始时间,方法结束时记录结束时间,进而得到方法的具体执行时间。环绕通知主要借助@Around注解来实现。实现代码如下:
关于环绕通知的一些说明:当环绕通知被触发时,它会包裹着目标方法,并提供了一个ProceedingJoinPoint
对象,通过调用proceed()
方法,可以显式地触发目标方法的执行。并且proceed()方法会返回目标方法的返回值,如果不调用proceed方法,那么目标方法就不会被执行。
代码测试结果:
环绕通知和前后置通知的对比
我们前面的代码都只是只有环绕通知或者前置通知后置通知单独执行,但是当这三个通知一起使用时,到底是环绕通知中的前置通知先执行呢?还是@Before的前置通知先执行呢?还有后置通知到底是哪个会后执行呢?这里我们直接来看代码运行的结果:
通过结果我们可以看出环绕通知总是会在最前面和最后面执行,这是由AOP框架决定的,我们只需要知道环绕通知总是在最前和最后执行的这个结论就可以了。
返回通知和异常通知的写法和上面类似,这里就不再演示,完整代码如下:
完整的AOP代码:
package com.example.blog_aop.config;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
//定义切面,以下注解缺一不可,一个是伴随着项目启动,一个是Spring Boot的注解
@Component
@Aspect
public class UserAspect {
//定义拦截规则(就是切点)
@Pointcut("execution(public * com.example.blog_aop.controller.UserController.*(..))")
public void pointcut(){
}
//定义通知(前置和后置通知)
@Before("pointcut()")
public void before(){
System.out.println("执行了前置通知");
}
@After("pointcut()")
public void after(){
System.out.println("执行了后置通知");
}
//环绕通知
@Around("pointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
Object obj = null;
System.out.println("around通知执行之前");
//执行方法
obj = joinPoint.proceed();
System.out.println("around通知执行之后");
return obj;
}
@AfterThrowing("pointcut()")
public void throwAdvice(){
System.out.println("执行了异常通知");
}
@AfterReturning("pointcut()")
public void returnAdvice(){
System.out.println("执行了返回通知");
}
}
3.5、切点表达式说明
切点表达式的组成:
切点表达式中通配符的说明:
* :匹配任意字符,只匹配⼀个元素(包,类,或⽅法,⽅法参数)
.. :匹配任意字符,可以匹配多个元素 ,在表示类时,必须和 * 联合使⽤。
+ :表示按照类型匹配指定类的所有类,必须跟在类名后⾯,如 com.cad.Car+ ,表示继承该类的 所有子类包括本身。
*与..的区别:*只能够匹配一个元素,比如我们修饰符有public、private、protected等,这些其实就是一个元素,修饰符也只要一个,所以修饰符就是使用*,使用*就代表匹配任意一个修饰符都可。
..就表示匹配多个元素,比如我们在方法的参数列表中,就可以使用..,代表的就是匹配多个参数,且参数的类型是任意的。这里如果使用*的话就代表匹配一个参数,参数类型任意。
切点表达式的一些使用案例:
包:com.example.demo.*.service 代表的是demo包下面的任意子包下的service包
com.example.demo.. 代表的是demo下的所有子包,含自己
com.example.demo.*.service.. 代表的是demo包下的任意子包,任意子包的service目录下的任意包
类:
UserController 代表指定类*com 代表以com结尾的类
User* 代表以User开头的类
* 代表任意类
参数:
() 代表的是无参
(int) 代表的是一个整型参数
(..) 代表参数任意
四、Spring AOP的实现原理
Spring AOP 是构建在动态代理基础上,因此 Spring 对 AOP 的⽀持局限于方法级别的拦截。
Spring AOP ⽀持 JDK Proxy 和 CGLIB ⽅式实现动态代理。默认情况下,实现了接⼝的类,使⽤ AOP 会基于 JDK ⽣成代理类,没有实现接⼝的类,会基于 CGLIB ⽣成代理类。
织⼊(Weaving):代理的生成时机
织⼊是把切⾯应⽤到⽬标对象并创建新的代理对象的过程,切⾯在指定的连接点被织⼊到目标对象中。
在⽬标对象的⽣命周期⾥有多个点可以进⾏织⼊:
编译期:切⾯在⽬标类编译时被织⼊。这种⽅式需要特殊的编译器。AspectJ的织⼊编译器就是以这种⽅式织⼊切⾯的。
类加载期:切⾯在⽬标类加载到JVM时被织⼊。这种方式需要特殊的类加载器 (ClassLoader),它可以在⽬标类被引⼊应⽤之前增强该目标类的字节码。AspectJ5的加载时织⼊(load-time weaving. LTW)就⽀持以这种⽅式织⼊切⾯。
运行期:切⾯在应⽤运⾏的某⼀时刻被织⼊。⼀般情况下,在织⼊切⾯时,AOP容器会为目标对象动态创建⼀个代理对象。SpringAOP就是以这种⽅式织⼊切⾯的。
实现原理简单概括:
Spring AOP实现是基于JDK Proxy和CGLIB这两动态代理来实现的,JDK Proxy是通过反射来实现的,性能比较高,JDK Proxy要求被代理的类一定要实现接口,因为这个只能代理接口中的方法。
CGLIB是通过代理类的子类来实现动态代理,它则要求被代理类不能是final修饰的。因为final修饰的类是不允许被继承,也就是不能有子类。
这两个动态代理的代理生成时期都是在运行期生成的。