一. 什么事AOP
AOP(Aspect Oriented Programming):面向切面编程,它是一种思想,它是对某一类事务的集中处理。比如用户登录权限的校验,再没学AOP之前,我们所有需要判断用户登录的界面(中的方法),都要各自实现或调用用户验证的方法,然而有了AOP之后,我们只需要再某一处配置一下,所有需要判断用户登录页面就都可以实现用户登录验证了,不需要每个方法都写相同的用户登录验证了。
而AOP 是一种思想,而Spring AOP 是一个框架,提供了一种对AOP 思想的实现,它们的关系和IoC 与 DI 类似
二. AOP 的组成
2.1 切面(Aspect)
切面由切点和通知组成,它既包含了横切逻辑的定义,也包含了连接点的定义
切面是包含了:通知,切点和切面的类,相当于 AOP 实现了某个功能的集合(定义一个事件:用户登录校验)
2.2 连接点(Join Point)
应用执行过程中能够插入切面的一个点,这个点可以是方法调用时,抛出异常,甚至修改字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。
连接点相当于需要被增强的某个 AOP 功能的所有方法(有可能触发切点的所有点:所有接口)
2.3 切点(Pointcut)
Pointcut 的作用就是提供一组规则(使用 AspectJ pointcut expression language 来描述)来匹配 Join Point , 给满足规则的 Join Point 添加 Advice.
切点相当于保存了众多连接点的一个集合(定义用户登录拦截规则,哪些接口需要判断用户登录,哪些不判断)
2.4 通知(Advice)
AOP 执行的具体方法。定义了切面是什么,何时使用,其描述了切面要完成的工作。
Spring 切面类中,可以在方法上使用以下注解,会设置方法为通知方法,在满足条件后会通知本方法进行调用:
- 前置通知:@Before: 通知方法会在目标方法调用之前执行
- 后置通知:@After:通知方法会在目标方法返回或者抛出异常后调用
- 返回通知:@AfterReturning:通知方法会在目标方法返回后调用
- 异常通知:@AfterThrowing:通知方法会在目标方法抛出异常后调用
- 环绕通知:@Around:通知包裹了被通知的方法,在被通知的方法通知之前和调用之后执行自定义的行为。
三. Spring AOP 实现
Spring AOP 的实现步骤如下:
3.1 添加Spring AOP 框架支持
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
3.2 定义切面和切点
//表明此类为一个切面 @Aspect @Component public class UserAspect { //定义一个切点,这里使用AspectJ 表达式语法 @Pointcut("execution(* com.example.demo.controller.UserController.*(..))") public void poingcut(){} }
其中pointcut 为空方法,它不需要有方法体,此方法名就是起到一个“标识”的作用,标识下面的通知方法具体指的是哪个切点(因为切点可能会有很多个)
切点表达式支持三种通配符:
- *:匹配任意字符,只匹配一个元素(包类,或方法,方法参数)
- ..:匹配任意字符,可以匹配多个元素,在表示类时,必须和 * 联合使用
- +:表示按照类型匹配指定类的所有类,必须跟在类名后面,如 com.cad.Car+,表示继承该类的所有子类包括本身
切点表达式由切点函数组成,其中 execution() 是最常用的切点函数,用来匹配方法,语法为:
execution(<修饰符><返回类型><包.类.方法(参数)><异常>)
修饰符和异常通常可以省略,具体含义如下:
修饰符,一般省略
public | 公共方法 |
* | 任意 |
返回值,不能省略
void | 没有返回值 |
String | 返回值字符串 |
* | 任意 |
包
com.gyf.crm | 固定包 |
com.gyf.crm.*.service | crm包下面子包任意(ex:com.guf.crm.staff.service) |
com.gyf.crm.. | crm包下面任意子包(包括自己) |
com.gyf.crm.*.service.. | crm包下任意子包,固定目录service,service目录任意包 |
类
UerService | 指定类 |
*lmpl | 以 lmpl 结尾 |
User* | 以 User 开头 |
* | 任意 |
方法名,不能省略
addUser | 固定方法 |
add* | 以 add 开头 |
*Do | 以 Do 结尾 |
* | 任意 |
参数
() | 无参 |
(int) | 一个整型 |
(int,int) | 两个整型 |
(..) | 任意参数 |
表达式示例
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 |
3.4 定义通知
//前置通知 @Before("pointcut()") public void doBefore(){ System.out.println("执行了 doBefore 方法"); } //后置通知 @After("pointcut()") public void doAfter(){ System.out.println("执行了 doAfter 方法"); } //返回之后通知 @AfterReturning("pointcut()") public void doAfterReturning(){ System.out.println("执行了 doAfterReturning 方法"); } //抛出异常之后通知 @AfterThrowing("pointcut()") public void doAfterThrowing(){ System.out.println("执行了 doAfterThrowing 方法"); } //环绕通知(传入的参数类型 ProceedingJoinPoint 是固定的) @Around("pointcut()") public Object doAround(ProceedingJoinPoint joinPoint){ Object result=null; System.out.println("doAround 方法开始执行"); try { result=joinPoint.proceed(); } catch (Throwable e) { e.printStackTrace(); } System.out.println("doAround 方法结束执行"); return result; }
@RequestMapping("/login.html") public String login(HttpServletRequest request){ HttpSession session=request.getSession(); session.setAttribute("username","张三"); System.out.println("登录成功"); return "login"; }
访问后的打印结果:
doAround 方法开始执行
执行了 doBefore 方法
登录成功
执行了 doAfterReturning 方法
执行了 doAfter 方法
doAround 方法结束执行
四. Spring AOP 实现原理
Spring AOP 是构建在动态代理基础上,因此 Spring 对 SOP 的支持局限于方法级别的拦截
Spring AOP 支持 JDK Proxy 和 CGLIB 方式实现动态代理.默认情况下,实现了接口的类,使用AOP 会基于JDK 生成代理类,没有实现接口的类,会基于 CGLIB 生成代理类.二者都是基于反射实现的。
JDK 动态代理实现:
import org.example.demo.service.AliPayService; import org.example.demo.service.PayService; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; //动态代理:使⽤JDK提供的api(InvocationHandler、Proxy实现),此种⽅式实现,要求被代理类必须实现接⼝ public class PayServiceJDKInvocationHandler implements InvocationHandler { //⽬标对象即就是被代理对象 private Object target; public PayServiceJDKInvocationHandler( Object target) { this.target = target; } //proxy代理对象 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //1.安全检查 System.out.println("安全检查"); //2.记录⽇志 System.out.println("记录⽇志"); //3.时间统计开始 System.out.println("记录开始时间"); //通过反射调⽤被代理类的⽅法 Object retVal = method.invoke(target, args); //4.时间统计结束 System.out.println("记录结束时间"); return retVal; } public static void main(String[] args) { PayService target= new AliPayService(); //⽅法调⽤处理器 InvocationHandler handler = new PayServiceJDKInvocationHandler(target); //创建⼀个代理类:通过被代理类、被代理类实现的接⼝、⽅法调⽤处理器来创建 PayService proxy = (PayService) Proxy.newProxyInstance( target.getClass().getClassLoader(), new Class[]{PayService.class}, handler ); proxy.pay(); } }
CGLIB 动态代理实现:
import org.springframework.cglib.proxy.Enhancer; import org.springframework.cglib.proxy.MethodInterceptor; import org.springframework.cglib.proxy.MethodProxy; import org.example.demo.service.AliPayService; import org.example.demo.service.PayService; import java.lang.reflect.Method; public class PayServiceCGLIBInterceptor implements MethodInterceptor { //被代理对象 private Object target; public PayServiceCGLIBInterceptor(Object target){ this.target = target; } @Override public Object intercept(Object o, Method method, Object[] args, Method Proxy methodProxy) throws Throwable { //1.安全检查 System.out.println("安全检查"); //2.记录⽇志 System.out.println("记录⽇志"); //3.时间统计开始 System.out.println("记录开始时间"); //通过cglib的代理⽅法调⽤ Object retVal = methodProxy.invoke(target, args); //4.时间统计结束 System.out.println("记录结束时间"); return retVal; } public static void main(String[] args) { PayService target= new AliPayService(); PayService proxy= (PayService) Enhancer.create(target.getClass(), new PayServiceCGLIBInterceptor(target)); proxy.pay(); } }
区别:
- 出身不同,JDK Proxy 是来自于原生的jdk,而 CGLIB 是第三方库的
- JDK Proxy 要求被代理类必须实现接口,CGLIB 可以不实现接口,可以通过实现代理类的子类来实现动态代理
- CGLIB 动态代理无法代理 final 类和 final 方法;JDK 动态代理可以代理任意类
- JDK 动态代理性能相对较高,生成代理对象速度较快;CGLIB 动态代理性能相对较低,生成代理对象速度较慢。