目录
一、什么是Spring AOP
AOP(Aspect Oriented Programming),直译过来就是面向切面编程,AOP是一种编程思想,是面向对象编程(OOP)的一种补充。Spring AOP是AOP思想的一种实现,就像DI一样是IoC的一种实现。
AOP的主要作用就是分离功能性需求和非功能性需求,使开发人员可以集中处理某一个关注点,减少对业务代码的侵入。增强代码的可读性和可维护性。简单来所,AOP的作用就是保证开发者在不修改业务代码的前提下,位系统中的业务组件添加某种通用功能。
就比如实现一个用户登录权限的校验功能,就比如我们使用的博客,在要进入博客编辑的页面时,需要对你是否登录进行校验,如果已经登录,那么就可以进入编辑页,如果没有那么就需要在登录页面登录之后在进入。像这样需要登录校验的页面,我们使用AOP思想,只需要在某一处配置以下,所有的需要判断用户登录的页面就可以实现用户登录验证了。这样每个页面就只关注具体的业务逻辑了。
二、AOP的使用场景
就像上面举的例子,当你的程序中实现的页面越来越多,那么你要 写的登录验证也越来越多,⽽这些⽅法⼜是相同的,这么多的⽅法就会提高代码修改和维护的成本。我们对这种功能统一并且使用地方较多的功能,就可以考虑使用AOP来统一处理。当然AOP可以使用的场景还有很多。
- 统一日志记录
- 统一方法执行时间统计
- 统一的返回格式设置
- 统一的异常处理
- 事务的开启和提交等
如果没有使用AOP思想来写代码,用户发送的请求直接被业务代码控制层接收到,请求访问的页面来校验用户是否登录。使用了AOP思想的代码,用户发送方的请求被AOP这里的代码先进行登录校验,如果登录,将请求传给控制层,如果没有登录就会被拦截。
三、AOP组成
1️⃣切面(Aspect):表示当前AOP是针对那些事件做处理的,用来登录的还是记录日志的。切面就是通知和切点的结合,通知和切点共同定义了切面的全部内容,他是干什么的,什么时候在哪里执行。通常以类的形式表示。
2️⃣切点(Pointcut):表示定义具体规则。切点其实就是筛选出的连接点,一个类中的所有方法都是连接点,但又不全需要,会筛选出某些作为连接点作为切点,如果说通知定义了切面的动作后者执行时机的话,切点则定义了执行的地点。
3️⃣通知(Advice):AOP执行的具体方法。有的地方叫增强。
4️⃣连接点(Join point):就是有可能触发切点的所有点。应用执行过程中能够插入切面的一个点,这个点可以是方法调用时,异常抛出时,甚至修改字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。
四、Spring AOP的实现
1、添加Spring AOP依赖
在创建好的Spring Boot项目的pom.xml中添加Spring AOP的依赖,我们可以从中央仓库中下载
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
然后点击刷新,触发下载。
2、定义切面和切点
这里使用注解@Aspect表示定义切面,即UserAserAspect类为切面,使用@Component注解表示让切面随着框架的启动而启动,这样切面中的切点定义的拦截规则才能生效。
package com.example.demo.common;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect//定义切面
@Component//让切面随着框架的启动而启动
public class UserAspect {
//定义切点,@Pointcut注解的参数中定义了具体的拦截规则。参数中使用AspectJ表达式语法
@Pointcut("execution(* com.example.demo.controller.UserController.*(..))")
public void pointcut(){}
}
上述代码中,pointcut方法为空方法,它不需要又方法体,此方法名就是起到一个"标识"的作用,标识下面的通知方法具体指的是那个切点。因为一个切面中有很多切点。
上述pointcut方法上添加的@Pointcut注解的参数中使用切点表达式定义了具体的拦截规则。
execution(* com.example.demo.controller.UserController.*(..))
切点表达的意思是:拦截UserContrller类中的所有方法其参数为任意参数并且返回值为任意类型的返回值。
- execution表示的意思为执行,执行的是后面跟的()中的规则。
- *表示的多个部分组成的,有修饰符和返回值类型。
- com.example.demo.controller.UserController表示要拦截com.example.demo.controller包中的UserController类
- 类后面跟的*表示UserController类中的所有方法。
- ..表示的不定式传参
切点表达式由切点函数组成,其中execution()是最常见的切点函数用来匹配方法,语法为:
execution(<修饰符><返回值类型><包.类.方法(参数)><异常>)
常见表达式示例
- execution(* com.example.demo.User.*(..)):匹配User类中的所有方法。
- execution(* com.example.demo.User+.*(..)):匹配该类的子类包括该类的所有方法
- execution(* com.example.*.*(..)):匹配com.example包下的所有类的所有方法
- execution(* com.example..*.*(..)):匹配com.example包下,子孙包下所有类的所有方法
- execution(* addUser(String,int)):匹配addUser方法,其第一个参数类型是String,第二个参数类型是int。
创建UserController类,这个类中的方法哪一个要被执行(目标方法)哪一个就是连接点
package com.example.demo.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/getuser")
public String getUser(){
System.out.println("do getUser");
return "get user";
}
@RequestMapping("/deluser")
public String delUser(){
System.out.println("do delUser");
return "del user";
}
}
3、定义相关通知
通知定义的是被拦截的方法具体要执行的业务。比如用户登录权限验证方法就是具体要执行的业务。
Spring AOP中,可以在方法上使用以下注解,会设置方法为通知方法,在满足条件后会通知本方法进行调用:
- 前置通知使用@Before:通知方法会在目标方法(连接点)调用之前执行
- 后置通知使用@After:通知方法会在目标方法(连接点)返回或者抛出异常后调用
- 返回之后通知使用@AfterReturning:通知方法会在目标方法(连接点)返回后调用
- 抛异常后通知使用@AfterThrowing:通知方法会在目标方法(连接点)抛出异常后调用
- 环绕通知使用@Around:通知包裹了被通知的方法,在被通知的方法之前和调用之后执行自定义的行为。
1️⃣前置通知和后置通知的实现
package com.example.demo.common;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect//定义切面
@Component//让切面随着框架的启动而启动
public class UserAspect {
//定义切点,@Pointcut注解的参数中定义了具体的拦截规则
@Pointcut("execution(* com.example.demo.controller.UserController.*(..))")
public void pointcut(){}
//定义前置通知
@Before("pointcut()")//表示这个通知是针对pointcut方法的
public void doBefore(){
System.out.println("执行了前置通知");
}
//定义后置通知
@After("pointcut()")
public void doAfter(){
System.out.println("执行了后置通知");
}
}
当我们在前端页面中访问UserController类的方法时,后端程序的控制台上每次出现的结果是先执行前置通知,在执行目标方法(连接点),然后执行后置通知。
2️⃣环绕通知的具体实现
环绕通知方法是具有Object类型的返回值,需要把方法执行结果返回给框架,框架拿到对象继续执行。
package com.example.demo.common;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect//定义切面
@Component//让切面随着框架的启动而启动
public class UserAspect {
//定义切点,@Pointcut注解的参数中定义了具体的拦截规则
@Pointcut("execution(* com.example.demo.controller.UserController.*(..))")
public void pointcut(){}
//定义前置通知
@Before("pointcut()")//表示这个通知是针对pointcut方法的
public void doBefore(){
System.out.println("执行了前置通知");
}
//定义后置通知
@After("pointcut()")
public void doAfter(){
System.out.println("执行了后置通知");
}
//定义环绕通知
@Around("pointcut()")
//环绕通知方法的参数为要执行的连接点,也就是我们在前端访问的目标方法
public Object doAround(ProceedingJoinPoint joinPoint){
System.out.println("环绕通知之前");
Object result = null;
try {
//执行目标方法,它的目标方法就是我们在前端访问的方法
result = joinPoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
System.out.println("环绕通知之后");
return result;
}
}
从执行结果中可以看到环绕通知的执行范围,可以环绕执行通知是最先执行的,然后是执行前置通知,然后再执行目标方法,然后执行后置通知,最后所有的方法执行完成了,环绕通知方法才会执行完成。
五、 AOP的实现原理
Spring AOP是建立再动态代理的基础上的,Spring对AOP的支持局限于方法级别的拦截。
Spring AOP使用两种混合的实现方式:JDK动态代理和CGLib动态代理。
- JDK动态代理:如果目标对象实现了InvocationHandler接口,Spring将使用JDK动态代理来创建代理对象。
- CGLib动态代理:如果目标对象没有实现InvocationHandler接口,Spring将使用CGLib代理,通过继承目标对象来创建代理对象。
1、什么是动态代理
代理可以看作是对调用目标的一个包装,这样我们对目标代理的调用不是直接发生的,而是通过代理完成。
当想要给实现了某个接口的类中的方法,加一些额外的处理,比如加日志,加事务等。可以给这个类创建一个代理,也就是创建一个新的类,这个类不仅包含原来类方法的功能,而且还在原来的基础上添加了额外处理的新方法,这个代理类并不是定义好的,而是动态生成的,具有解耦意义,灵活、扩展性强。
在Java中,动态代理通常使用Java.lang.reflect.Proxy类和Java.lang.reflect.InvocationHandler接口来实现。
2、 JDK代理和CGLIB代理的区别
- 接口要求:JDK动态代理只能对实现了接口的类生成代理;而CGLIB代理可以没有实现接口的类,是通过继承被代理类,在运行时动态的生成代理对象。
- 生成方式:JDK代理使用Java的反射机制来完成代理对象,而CGLIB代理使用CGLIB库生成代理对象,通过修改目标类的字节码来实现。所以该类不能被final修饰
如果代理类没有实现 InvocationHandler 接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成指定类的一个子类对象,并覆盖其中特定方法并添加增强代码,从而实现AOP。CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。