SpringAOP:代码增强术
今天小编来介绍下SpringAOP。
AOP:
Aspect Oriented Programming(⾯向切⾯编程)
面向切面编程它是一种编程范式,旨在通过分离横切关注点来提高软件模块性。
这里的切面代表的是一类特定的问题。
举例:
比如在Controller层某个类中,我想对此类下的方法进行一些功能实现,比如日志记录、方法耗时、权限认证等
那么此时我又不想对该类上的方法进行代码侵入,此时呢,很适合用到AOP思想来做这样的事。
所以简单来说,AOP它就是一种思想,他就是对某一类事情的集中处理。
对于SprinAOP来说,,它是实现了AOP了,所以SpringAOP是一种实现方式,
除了SpringAOP,那么还有AspectJ,CGLIB等,它们也是实现了AOP。
AOP核心概念:
切面(Aspect)、连接点(Join Point)、通知 (Advice)、切入点(Pointcut)
引入(Introduction)、目标对象(Target Object)、AOP代理(Proxy)
这些概念会在代码引入中,进行详细解释。
快速上手:
一:项目准备:
1.创建一个Springboot的项目,创建时,引入相关依赖,比如spring-boot-starter-web、lombok等,按需引入即可
2.pom.xml添加配置:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>3.创建controller包、aspect包,然后再创建以下这几个类在controller包中
Test1Controller:
import com.nanxi.springaop.aspect.TimeRecord;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/test")
public class Test1Controller {
@RequestMapping("/t1")
public String t1(){
System.out.println("执行t1方法……");
return "t1返回";
}
@RequestMapping("/t2")
public String t2(){
System.out.println("执行t2方法……");
return "t2返回";
}
@RequestMapping("/t3")
public int t3(){
System.out.println("执行t3方法……");
int ret=10 / 0;
return ret;
}
}Test2Controller:
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class Test2Controller {
@RequestMapping("/u1")
public String u1(){
System.out.println("执行u1方法");
return "u1返回";
}
@RequestMapping("/u2")
public String u2(){
System.out.println("执行u2方法");
return "u2返回";
}
}二:使用AOP
1.在aspect包中创建TestRecordAspect类
@Slf4j
@Component
@Aspect
public class TestRecordAspect {
//方法写好后,要明确告诉给谁用
@Around("execution(* com.nanxi.springaop.controller.*.*(..))")
public Object timeRecord(ProceedingJoinPoint point) throws Throwable {
/**
* 实现一个计算耗时功能,但对代码不进行侵入,适应AOP切面编程
* 1.记录开始时间
* 2.执行目标代码
* 3.记录结束时间
* 4.返回结果
*/
long start=System.currentTimeMillis();
Object o=null;
o=point.proceed();
log.info(point.getSignature()+"耗时:"+(System.currentTimeMillis()-start)+"ms");
return o;
}
}通过访问URL:http://127.0.0.1:8080/user/u1
获取结果如下:

代码详解:
@Aspect:标识这是一个切面类
@Around:环绕通知,在目标方法前后都会执行,里面是一个切面表达式,表达了这个切面类对哪些方法进行增强。
o=point.proceed();:这个是让被增强的方法去执行。
切面:整个TestRecordAspect方法就是切面
切面、连接点、切入点、通知

切点表达式:
execution(* com.nanxi.springaop.controller.*.*(..))
com.nanxi.springaop.controller..*(..)这一坨东西就是切点表达式
常见的有这以下两种:
1.execution(……):根据方法的签名来匹配
2.@annotation(……):根据注解匹配
先介绍execution类型的表达式: 该表达式是用来匹配方法的,具体语法如下:
execution(<访问修饰符> <返回类型> <包名.类名.方法(方法参数)> <异常>)
*与..解释:
*:匹配任意字符,只匹配一个元素(返回类型、包、类名,方法活着方法参数)
包名使用*表示任意包名(一层包使用一个*)
类名使用*表示任意类
返回值使用*表示任意返回类型
方法名使用*表示任意方法
参数使用*表示一个任意的参数
.. :
使用该符号,代表匹配多个连续的任意符号,可以统配任意层级的包,或任意类型,任意个数的参数
使用..配置包名,标识此包下及其所有子包
使用..配置参数,表示任意个类型的参数
值得注意的是,切点表达式中返回参数是指,可以指定该方法的返回类型是什么,从而达到控制范围。
对于execution表达式而言,它更适合有规则的,那么对于无规则,比如,在Test1Controller下的u1方法
以及UserController下的c1方法下,且这两个方法写上了注解(比如RequestMapping),那么此时使用@annotation表达式去做。
举个例子,我们去增强只带有RequestMapping注解的方法
@annotation("org.springframework.web.bind.annotation.RequestMapping")@annotation的内容可以填写自定义注解。
除了这个注解表示式,还有其他注解表达式:
表格 还在加载中,请等待加载完成后再尝试复制
在介绍自定义注解之前,先来介绍Poincut注解,该注解是为了复用里面的切点表达式。
Pointcut:
@Pointcut("execution(* com.nanxi.springaop.controller.*.*(..))")
public void p1(){}
//调用该表达式
@Around("p1()")
public int test(){
……………………
}自定义注解:
首先定义一个注解类:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface TimeRecord {
}@Retention(RetentionPolicy.RUNTIME)
这个是说明该注解会在运行时起效 @Target({ElementType.METHOD})
这个是说明该注解对方法起效
实现注解:
注解方法任是实现方法耗时:
@Slf4j
@Component
@Aspect
public class ACTimeRecord {
//方法写好后,要明确告诉给谁用
@Around("@annotation(com.nanxi.springaop.aspect.TimeRecord)")
public Object timeRecord(ProceedingJoinPoint point) throws Throwable {
/**
* 实现一个计算耗时功能,但对代码不进行侵入,适应AOP切面编程
* 1.记录开始时间
* 2.执行目标代码
* 3.记录结束时间
* 4.返回结果
*/
long start=System.currentTimeMillis();
Object o=null;
o=point.proceed();
log.info(point.getSignature()+"耗时:"+(System.currentTimeMillis()-start)+"ms");
return o;
}
}如何调用:
在需要统计方法上加上@TimeRecord即可
@RestController
@RequestMapping("/test")
public class Test1Controller {
@TimeRecord
@RequestMapping("/t1")
public String t1(){
System.out.println("执行t1方法……");
return "t1返回";
}
@TimeRecord
@RequestMapping("/t2")
public String t2(){
System.out.println("执行t2方法……");
return "t2返回";
}
@RequestMapping("/t3")
public int t3(){
System.out.println("执行t3方法……");
int ret=10 / 0;
return ret;
}
}通知(Advice)
通知类型如下:
表格 还在加载中,请等待加载完成后再尝试复制
代码演示:
新建一个AspectDemo1在aspect包下
@Component
@Slf4j
@Aspect
public class AspectDemo1 {
@Pointcut("execution(* com.nanxi.springaop.controller.*.*(..))")
public void p1(){}
@Before("p1()")
public void doBefore(){
log.info("AspectDemo1 DoBefore……");
}
@After("p1()")
public void doAfter(){
log.info("AspectDemo1 doAfter……");
}
@AfterReturning("p1()")
public void doAfterReturning(){
log.info("AspectDemo1 doAfterReturning……");
}
@AfterThrowing("p1()")
public void doAfterThrowing(){
log.info("AspectDemo1 doAfterThrowing……");
}
@Around("p1()")
public Object doAround(ProceedingJoinPoint point) {
log.info("Around前处理");
Object ret=null;
try {
ret=point.proceed();
} catch (Throwable e) {
log.error("Around异常处理");
}
log.info("Around后处理");
return ret;
}
}访问Test1Controller下的t1方法:http://127.0.0.1:8081/test/t1,访问结果如下:

显然是没有看到doThrowing返回的,这是因为,t1方法中没有出现异常,同时也可以观察到,Around通知级别是最高的。
接下来访问http://127.0.0.1:8081/test/t3(已把端口号修改),该方法会出现报错

此时就会看到这个AspectDemo1 doAfterThrowing……
切面优先级:
准备以下这几个类来测试切面优先级:
@Component
@Slf4j
@Aspect
public class AspectDemo2 {
@Pointcut("execution(* com.nanxi.springaop.controller.*.*(..))")
public void p1(){}
@Before("p1()")
public void doBefore(){
log.info("AspectDemo2 DoBefore……");
}
@After("p1()")
public void doAfter(){
log.info("AspectDemo2 doAfter……");
}
}@Component
@Slf4j
@Aspect
public class AspectDemo3 {
@Pointcut("execution(* com.nanxi.springaop.controller.*.*(..))")
public void p1(){}
@Before("p1()")
public void doBefore(){
log.info("AspectDemo3 DoBefore……");
}
@After("p1()")
public void doAfter(){
log.info("AspectDemo3 doAfter……");
}
}@Component
@Slf4j
@Aspect
public class AspectDemo4 {
@Pointcut("execution(* com.nanxi.springaop.controller.*.*(..))")
public void p1(){}
@Before("p1()")
public void doBefore(){
log.info("AspectDemo4 DoBefore……");
}
@After("p1()")
public void doAfter(){
log.info("AspectDemo4 doAfter……");
}
}访问:http://127.0.0.1:8081/test/t1

由此可以观察出先进来的后出去。

默认优先级是按照包中,类的首字母按字典顺序排列。
但我们也可以设置它的优先级。
使用@Order注解
在AspectDemo4、AspectDemo3、AspectDemo2分别加上@Order(1)、@Order(2)、@Order(3)注解
再次访问URL,得到结果如下:

此时我们发现顺序颠倒过来,由此得出结论,@Order注解中的值,数字越大,级别越低,反之亦然。
上面分享到了SpringAOP的一些应用,那么它是如何实现这个SpringAOP的呢?
SpringAOP原理:
它的实现本质是基于代理模式。而且还是基于动态代理实现的。
代理模式:
它是一种结构性设计模式,它允许你提供一个代替对象(即代理对象),来控制对某个其他对象(即目标对象)的访问,代理模式可以用来执行额外的操作,比如延迟初始化,访问控制,日志记录,而不需要改变原始对象的行为或接口。
代理也分为静态代理和动态代理:
表格 还在加载中,请等待加载完成后再尝试复制
JDK 动态代理
只能对接口进行代理。
优点:JVM 原生支持,性能较好。
缺点:不能代理没有实现接口的类。
CGLIB 代理
可以对类进行代理(不需要接口)。
原理:通过继承目标类并重写方法的方式实现代理。
优点:适用于所有类,包括没有接口的类。
缺点:生成代理类需要更多内存和初始化时间
使用代理前:

使用代理后:

代理有角色划分,
Subject:业务接口类,可以是抽象类
RealSubject:业务实现类,具体业务执行,也就是被代理对象
Proxy:RealSubject的代理
举个例子:
现实生活中大多数去工作,那么避免不了租房。
那么这个租房就是业务(Subject),那么谁来实现这个业务呢,显然就是房东(RealSubject),毕竟是房东才有房子出租
那么房东此时觉得不想自己来做这些出租工作了,此时就会交给中介(Proxy)来做,此时中介就是代理
静态代理演示:
我们把例子转换成代码:
新建一个proxy包,新建HouseSubject接口、RealSubject类、HouseProxy类、TestProxy类
HouseSubject:
public interface HouseSubject {
void rent();
void sale();
}RealSubject:
public class RealHouseSubject implements HouseSubject{
@Override
public void rent() {
System.out.println("我是房东,有房屋出租");
}
@Override
public void sale() {
System.out.println("我是房东,有房屋出售");
}
}HouseProxy类:
public class HouseProxy implements HouseSubject{
private RealHouseSubject subject;
public HouseProxy(RealHouseSubject subject){
this.subject=subject;
}
@Override
public void sale() {
System.out.println("我是中介,开始代理房屋出售");
subject.sale();
System.out.println("我是中介,结束代理房屋出售");
}
@Override
public void rent() {
System.out.println("我是中介,开始代理房屋租聘");
subject.rent();
System.out.println("我是中介,结束代理房屋出租聘");
}
}TestProxy:
public class TestProxy {
public static void main(String[] args) {
// 静态代理
HouseProxy proxy=new HouseProxy(new RealHouseSubject());
proxy.rent();
proxy.sale();
}结果展示:

以上是展示静态代理。
那么接下来演示下动态代理
JDK动态代理:
//使用静态代理,要实现下InvocationHanlder接口
public class JDKProxy implements InvocationHandler {
//创建一个要代理的对象
private Object target;
public JDKProxy(HouseSubject target){
this.target=target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//before
System.out.println("JDK动态代理:开始代理");
//通过反射去执行目标方法
Object ret = method.invoke(target, args);
//after
System.out.println("JDK动态代理:结束代理");
return ret;
}
}invoke方法参数解释:
object proxy:当前被代理的对象(即通过 Proxy.newProxyInstance(...) 创建出来的代理对象本身)。
Metho method:你要调用的目标方法(即客户端调用的那个方法)的 Method 对象。
object[] args:调用目标方法时传入的参数数组
TestProxy:
//方式一:
HouseSubject target=new RealHouseSubject();
HouseSubject proxy= (HouseSubject)Proxy.newProxyInstance(target.getClass().getClassLoader(),
new Class[]{HouseSubject.class},new JDKProxy(target));
System.out.println(proxy.getClass());
proxy.rent();
proxy.sale();
方式二:
// HouseSubject target=new RealHouseSubject();
// HouseSubject proxy= (HouseSubject) Proxy.newProxyInstance(target.getClass().getClassLoader(),
// target.getClass().getInterfaces(),new JDKProxy(target));
// System.out.println(proxy.getClass());
// proxy.rent();
// proxy.sale();结果:

代码解释:
Proxy.newProxyInstance:
这是 Java 提供的一个静态方法,用于在运行时动态创建一个代理对象。这个代理对象实现了指定的接口,并将所有方法调用转发给一个 InvocationHandler 对象(也就是你写的 JDKProxy 类)。
1. target.getClass().getClassLoader()
获取目标对象的类加载器;
这是为了让生成的代理类能被正确加载进 JVM。
2. new Class[]{HouseSubject.class}
表示你要为
HouseSubject接口生成代理;如果有多个接口,可以传入多个接口类。
3. new JDKProxy(target)
创建了一个
InvocationHandler实现类的实例;当代理对象的方法被调用时,就会进入你自定义的
invoke()方法中。
调用链条:
客户端调用 rent()
↓
$Proxy0.rent()
↓
invoke(proxy, Method("rent"), null)
↓
前置增强:开始代理
↓
method.invoke(target, null) → RealHouseSubject.rent()
↓
后置增强:结束代理
↓
返回结果CGLIB动态代理
CGLIB(Code Generation Library):
CGLIB(Code Generation Library)是一个强大的、高性能的代码生成库,它广泛应用于Java编程中。与JDK动态代理不同,CGLIB通过继承的方式实现代理对象,即它会生成目标类的子类来覆盖目标类的方法,以此达到增强目的方法的功能。这意味着即使目标对象没有实现任何接口,CGLIB也能为其创建代理对象。
若是使用该库需要引入依赖
引入依赖:
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>新建一个CGLIBProxy类(注意导入的包是来自cglib的)
public class CGLIBProxy implements MethodInterceptor {
//新建一个对象
private Object target;
public CGLIBProxy(Object target){
this.target=target;
}
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
//before
System.out.println("CGLIB动态代理:开始代理");
Object ret = method.invoke(target, objects);
//after
System.out.println("CGLIB动态代理:结束代理!");
return ret;
}
}新建一个HouseSubject2:
public class HouseSubject2 {
public void rent(){
System.out.println("我是房东,有房屋出租");
}
public void sale(){
System.out.println("我是房东,有房屋出售");
}
}TestProxy
HouseSubject2 target=new HouseSubject2();
//运行时创建代理对象
//运行CGLIB要添加vm option
HouseSubject2 proxy=(HouseSubject2) Enhancer.create(target.getClass(), new CGLIBProxy(target));
System.out.println(proxy.getClass());
proxy.rent();值得注意的是,允许该方法前,需要配置vm option

点击modify options

然后在出现的框上写入:--add-opens java.base/java.lang=ALL-UNNAMED
为什么要加上该参数:
从 Java 9 之后引入了 Jigsaw 模块系统,它默认禁止某些模块内部使用反射访问其他模块的内部类或方法
参数意义拆解:
表格 还在加载中,请等待加载完成后再尝试复制
结果:

调用链图:
客户端调用 rent()
↓
HouseSubject2$$EnhancerByCGLIB...rent()
↓
intercept(o, Method("rent"), args, methodProxy)
↓
前置增强:开始代理
↓
method.invoke(target, null) → HouseSubject2.rent()
↓
后置增强:结束代理
↓
返回结果那么除了原生带的cglib库,spring它也提供自身的cglib库,其演示效果是一模一样的
那么它们有什么区别呢?
原生cglib与Springcglib相比
表格 还在加载中,请等待加载完成后再尝试复制
那么对于Spring framework以及Spring boot而言,它们是使用cglib还是JDK呢?
那么这里就涉及到一个重要的熟悉,来自代理工厂的proxyTargetClass,通过程序设置其值,是的代理方式变样
Spring framework:
变更设置,该变更设置适合于非Spring boot项目
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AppConfig {
}意义:设置为true时,强制对类进行代理(即使目标类实现了接口),使用CGLIB 代理
Springboot2.x:
变更设置:
在application.yml中写入即可
spring:
aop:
proxy-target-class: false