Spring AOP

Spring AOP 学习大纲

一.AOP概述

AOP是Spring框架的第二大核心(第一大核心是IoC)

什么是AOP?

Aspect Oriented Programming(面向切面编程)

什么是面向切面编程呢?切面就是指某一类特定问题,所以AOP也可以理解为面向特定方法编程

拦截器就是AOP的一种应用,AOP是一种思想,拦截器是AOP思想的一种实现,spring框架实现了这种思想,提供了拦截器技术的相关接口

同样的,统一数据返回格式和统一异常处理,也是AOP思想的一种实现

简单来说:AOP是一种思想,是对某一类事情的集中处理

什么是Spring AOP?

AOP是一种思想,他的实现方法有很多,有springAOP,也有Aspect J,CGLIB等。SpringAOP是其中的一种实现方式

学会了统一功能之后,是不是就学会了SpringAOP呢?当然不是

拦截器作用的维度是URL(一次请求和响应),@ControllerAdvice应用场景主要是全局异常处理(配合自定义异常效果更佳),数据绑定,数据预处理。AOP作用的维度更加细致(可以根据包,类,方法名,参数等进行拦截),能够实现更加复杂的业务逻辑

AOP可以在不改动原有程序的基础上,针对特定的方法进行功能的增强(AOP的作用:在程序运行期间在不修改源代码的基础上对已有方法进行增强(无侵入性:解耦)

二.SpringAOP快速入门

1.引入AOP依赖

复制代码
        <dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-aop</artifactId>
		</dependency>

2.编写AOP程序

记录Controller中每个方法的执行时间

java 复制代码
@Slf4j
@Aspect
@Component
public class TimeAspect {
    /**
     * 记录方法耗时
     */
    @Around("execution(*com.july.aop.controller.*.*(..))")
    public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
        long begin = System.currentTimeMillis();
        Object result = pjp.proceed();
        long end = System.currentTimeMillis();
        log.info(pjp.getSignature() + "执行耗时:{}ms",end - begin);
        return result;
    }
}

对程序进行简单的讲解:

1.@Aspect:标识这是一个切面类

2.@Around:环绕通知,在目标方法的前后都会被执行,后面的表达式表示对哪些方法进行增强

3.ProceedingJoinPoint.proceed()让原始方法执行

通过上述程序,我们可以感受到AOP的一些优势:

1.代码无侵入

2.减少了重复代码

3.提高了开发效率

4.维护方便

三.SpringAOP详解

1.Spring AOP核心概念

1.切点(PointCut)

切点(PointCut),也叫做"切入点",PointCut的作用就是提供一组规则,告诉程序员对哪些方法来进行功能增强

上述代码中@Around("execution(*com.july.aop.controller.*.*(..))")就是切点表达式

2.连接点(Join Point)

满足切点表达式规则的方法,就是连接点,也就是可以被AOP控制的方法

以入门AOP程序举例,所有com.july.aop.controller路径下的方法就是连接点

切点和连接点的关系:

连接点是满足切点表达式的元素,切点可以看作是保存了众多连接点的一个集合

3.通知(Advice)

通知就是具体要做的工作,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)

在AOP面向切面编程中,我们把这部分重复的代码逻辑抽取出来单独定义 ,这部分代码就是通知的内容

4.切面(Aspect)

切面(Aspect) = 切点(CutPoint)+ 通知(Advice)

切面既包含了通知逻辑的定义,也包含了连接点的定义

2.通知类型

上述我们了解了什么是通知,接下来我们学习通知的类型

@Around就是一种通知类型,表示环绕通知

Spring中AOP的通知类型有以下几种:

@Around:环绕通知,此注解标注的通知方法在目标方法前后都执行

@Before:前置通知,此注解标注的通知方法在目标方法前执行

@After:后置通知,此注解标注的通知方法在目标方法后执行,无论是否有异常都会被执行

@AfterReturing:返回后通知,此注解标注的通知方法在目标方法后执行,有异常不执行

@AfterThrowing:异常后通知,此注解标注的通知方法发送异常后执行

程序正常运行情况下:

@Around前->@Before->@AfterReturning->@After->@Around后

程序发生异常的情况下:

@AfterReturning 标识的通知方法不会执行,@AfterThrowing标识的通知方法执行了

@Around环绕通知中原始方法调用时有异常,通知中的环绕后的代码逻辑也不会在执行了(因为原始方法调用出现异常了)

@Around前--->@Before->@After Throwing->@After

注意事项:

1.@Around环绕通知需要调用@ProceedingJoinPoint.proceed()来让原方法执行,其他通知不需要考虑目标方法执行

2.@Around环绕通知方法的返回值,必须指定为Object,来接收原始方法的返回值,否则原始方法执行完毕,是获取不到返回值的

3.一个切面类可以有多个切点

3.@PointCut

Spring提供了@PointCut注解,把公共的切点表达式提取出来,需要时引用该切入点表达式即可

java 复制代码
package com.july.aop.aspect;
import lombok.extern.slf4j.Slf4j;
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;
@Slf4j
@Aspect
@Component
public class AspectDemo {
//定义切点(公共的切点表达式)
@Pointcut("execution(com.july.aop.controller..*(..))")
private void pt(){}
//前置通知
@Before("pt()")
public void doBefore(){
}
//后置通知
@After("pt()")
public void deAfter(){

}
//返回后通知
@After("pt()")
public void deAfterReturning(){

}
//抛出异常后通知
@After("pt()")
public void deAfterThrowing(){

}
//环绕通知
@After("pt()")
public void deAround(){

}
}

当切点用private修饰的时候,仅能在当前切面类中使用,当其他切面类也要使用当前切点定义的时候,就需要把private改为public,引用方式为:全限定名.方法名()

java 复制代码
@Slf4j
@Aspect
@Component
public class AspectDemo2 {
@Before("execution(* com.bit.aop.controller.*.*(..))")
public void doBefore() {
    log.info("AspectDemo2 doBefore....");

}
}

4.切面优先级@Order

当存在多个切面类的时候,默认按照切面类的类名字母排序:

1.@Before通知:字母排名靠前的先执行

2.@After通知:字母排名靠后的先执行

但是这种方式并不便于管理,Spring提供了一个新的注解,来控制切面通知的顺序:@Order

@Order注解标识的切面类,执行顺序如下:

1.@Before通知:数字越小先执行

2.@After通知:数字越大先执行

5.切点表达式

切点表达式常见有两种表达方式:

1.execution(...):根据方法的签名来匹配

2.@Annotation(...):根据注解匹配

1.execution表达式

execution()是最常用的切点表达式,用来匹配方法,语法为:

execution(<访问修饰符><返回类型><包名.类名.方法(方法参数)><异常>

切点表达式支持通配符表达:

1.*:匹配任意字符,只匹配一个元素(返回类型,包,类名,方法或者方法参数)

  1. ..:匹配多个连续的任意符号,可以统配任意层级的包,或任意类型,任意个数的参数

2.@annotation

execution表达式更适合有规则的,如果要匹配多个无规则的方法呢,这个时候我们可以借助自定义注解的方式以及另一种切点表达式@annotaion来描述这一类的切点

实现步骤:

1.编写自定义注解

2.使用@annotion表达式来描述切点

3.在连接点的方法上添加自定义注解

java 复制代码
@Slf4j
@Component
@Aspect
public class MyAspectDemo {
    //前置通知
    @Before("@annotation(com.july.aop.aspect.MyAspect")
    public void doBefore(){
        log.info("MyAspect  ->before");
    }
    //后置通知
    @After("@annotation(com.july.aop.aspect.MyAspect")
    public void deAfter(){
        log.info("MyAspect  ->after");
    }
}

总结:SpringAOP的实现方式(常见的面试题)

1.基于注解@Aspect

2.基于自定义注解@annotation

3.基于SpringAPI(通过xml配置的方式,在springboot广泛使用后,这种方法几乎看不到了)

4.基于代理的方式实现(更加古老笨重,不建议使用)

四.SpringAOP原理

Spring是如何实现AOP的呢?

SpringAOP是基于动态代理来实现AOP的,下面我们先了解代理模式,再对SpringAOP源码进行分析

1.代理模式

代理模式也叫做委托模式

定义:为其他对象提供一种代理以控制这个对象的访问。他的作用就是通过一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用

在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用

代理模式的主要角色:

1.Subject:业务接口类,可以是抽象类或者接口(不一定有)

2.ReadSubject:业务实现类,具体的业务执行,也就是被代理对象

3.Proxy:代理类,RealSubject的代理

代理模式可以在不修改被代理对象的基础上,通过拓展代理类,进行一些功能的附加与增强

根据代理的创建时期,代理模式分为静态代理和动态代理

静态代理:由程序员创建代理类或特定工具自动生成源代码再对其编译,在程序运行前代理类的.class文件就已经存在了

动态代理:在程序运行时,运用反射机制动态创建而成

1.静态代理

静态代理在程序运行前就把代码写好了,对目标对象的每个方法的增强都是手动完成的,非常不灵活,在日常开发中,几乎看不到静态代理的场景

我们通过代码来加深对静态代理模式的理解,以租房为例

1.定义接口(定义房东要做的时,也是中介要做的事情)

java 复制代码
public interface HouseSubject {
    void rentHouse();
}

2.实现接口(房东出租房子)

java 复制代码
public class RealHouseSubject implements HouseSubject{
    @Override
    public void rentHouse() {
        System.out.println("我是房东,我出租房子");
    }
}

3.代理(中介,帮忙出租房子)

java 复制代码
public class HouseProxy implements HouseSubject{
    //将被代理对象声明成成员变量
    private HouseSubject houseSubject;
    public HouseProxy(HouseSubject houseSubject){
        this.houseSubject = houseSubject;
    }
    @Override
    public void rentHouse() {
        //开始代理
        System.out.println("我是中介,开始代理");
        houseSubject.rentHouse();
        //结束代理
        System.out.println("我是中介,代理结束");
    }
}

4.使用

java 复制代码
public class StaticMain {
public static void main(String[] args) {
HouseSubject subject = new RealHouseSubject();
//创建代理类
HouseProxy houseProxy = new HouseProxy(subject);
//通过代理访问目标方法
houseProxy.rentHouse();
}
}

上面这个代理实现方式就是静态模式,从上述程序可以看出,虽然静态代理也完成了目标对象的代理,但代码写死了,对目标对象的增强都是手动完成的,非常不灵活,所以日常开发几乎看不到静态代理的场景

接下来又新增需求,出售房子

我们需要对上述代码进行修改

1.接口定义修改

java 复制代码
public interface HouseSubject {
    void rentHouse();
    void saleHouse();
}

2.接口实现修改

java 复制代码
public class RealHouseSubject implements HouseSubject{
    @Override
    public void rentHouse() {
        System.out.println("我是房东,我出租房子");
    }
@Override
public void saleHouse() {
    System.out.println("我是房东,我出售房子");
}
}

3.代理类修改

java 复制代码
public class HouseProxy implements HouseSubject{
//将被代理对象声明成成员变量
private HouseSubject houseSubject;
public HouseProxy(HouseSubject houseSubject){
this.houseSubject = houseSubject;
}
@Override
public void rentHouse() {
//开始代理
System.out.println("我是中介,开始代理");
houseSubject.rentHouse();
//结束代理
System.out.println("我是中介,代理结束");
}
@Override
public void saleHouse() {
    //开始代理
    System.out.println("我是中介,开始代理");
    houseSubject.rentHouse();
    //结束代理
    System.out.println("我是中介,代理结束");
}
}

从上述代码可以看出,我们修改接口(subject)和业务实现类(realSubject)时还需要修改代理类

同样的,如果有新增接口(subject)和业务实现类(realSubject),也需要对每一个业务实现类新增代理类(proxy)

2.动态代理

相比于静态代理来说,动态代理更加灵活

我们不需要针对每个目标对象都单独创建一个 代理对象,而是把这个创建代理对象的工作推迟到程序运行时由JVM实现,也就是说动态代理在程序运行时,根据需要动态创建生成

Java也对动态代理进行了实现,并给我们提供了一些API,常见的实现方式有两种:

1.JDK动态代理

JDK动态代理类实现步骤

1.自定义一个接口及其实现类

2.自定义InvocationHandler并 重写invoke方法,在invoke方法中我们会调用目标方法(被代理的方法)并自定义一些处理逻辑

3.通过Proxy.newProxyInstance(ClassLoader loader,Class<?>\[\] interfacces,InvocationHandler h)方法创建代理对象

实现JDK动态代理类

1,实现InvocationHandler接口

java 复制代码
public class JDKInvocationHandler implements InvocationHandler {
    //目标对象就是被代理对象
    private Object target;
public JDKInvocationHandler(Object target){
    this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    //代理增强内容
    System.out.println("我是中介,开始代理");
    //通过反射调用被代理类方法
    Object retVal = method.invoke(target,args);
    //代理增强内容
    System.out.println("我是中介,结束代理");
    return  retVal;
}
}

2.创建一个代理对象并使用

java 复制代码
public class JDKInvocationHandler implements InvocationHandler {
    //目标对象就是被代理对象
    private Object target;
public JDKInvocationHandler(Object target){
    this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    //代理增强内容
    System.out.println("我是中介,开始代理");
    //通过反射调用被代理类方法
    Object retVal = method.invoke(target,args);
    //代理增强内容
    System.out.println("我是中介,结束代理");
    return  retVal;
}
}

下面我们了解一下这部分代码

1.InvocationHandler

InvocationHandler是Java动态代理的关键接口之一,他定义了一个单一方法invoke(),用于处理被代理对象的方法调用

通过实现InvocationHandler接口,可以对被代理对象的方法进行功能增强

2.Proxy

Proxy类中使用频率最高的方法是newProxyInstance(),这个方法主要用来生成一个代理对象

这个方法一共三个参数:1.Loader:类加载器,用于加载代理对象 2.interfaces:被代理类实现的一些接口(这个参数的定义,也决定了JDK动态代理只能代理实现了接口的一些类) h:实现了InvocationHandler接口的对象

ps:上述代码只是伪代码,我们要知道JDK动态代理只能代理接口

2.CGLIB动态代理

JDK动态代理有一个致命的缺点就是只能代理实现了接口的类,有一些场景下业务代码是直接实现的,JDK动态代理无法解决,我们可以使用CGLIB动态代理机制来解决

CGLIB(Code Generation Libary)是一个基于ASM的字节码生成库,它允许我们在运行时对于字节码进行修改和动态生成.CGLIB通过继承的方式实现代理,很多知名的开源框架都使用到了CGLIB,例如Spring中的AOP模块中:如果目标对象实现了接口,默认使用JDK动态代理,否则采用CGLIB动态代理

CGLIB动态代理类实现步骤:

1.定义一个类(被代理类)

2.自定义MethodInterceptor并重写Interceptor方法,intercept用于增强目标方法,和JDK动态代理中的invoke()方法类似

3.通过enhancer类的create()创建代理类

实现:

1.添加依赖

CGLIB实际上属于一个开源项目,如果要使用,要添加依赖

java 复制代码
        <dependency>
			<groupId>cglib</groupId>
			<artifactId>cglib</artifactId>
			<version>3.3.0</version>
		</dependency>

2.自定义MethodInterceptor(方法拦截器)

实现MethodInterceptor接口

java 复制代码
public class CGlibMethodInterceptor implements MethodInterceptor {
    private Object target;
public CGlibMethodInterceptor(Object target) {
    this.target = target;
}

@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
    System.out.println("我开始代理");
    Object invoke = method.invoke(target, args);
    System.out.println("我结束代理");
    return invoke;
}
}

3.创建代理类,并使用

java 复制代码
 HouseSubject houseProxy = (HouseSubject) Enhancer.create(target.getClass(), new CGlibMethodInterceptor(target));
        houseProxy.rentHouse();

代码介绍:

1.MethodInterceptor

MethodInterceptor和JDK动态代理中的InvocationHandler类似,他只是定义了一个方法intercept(),用于增强目标方法

2.Enhancer.create()用来创建要给代理对象

2.SpringAOP源码分析(了解)

springAOP主要是基于两种方式实现的:JDK以及CGLIB的方式

Spring 对AOP的实现,基本上都是靠AnnotationAwareAspectJAutoProxyCreator去完成

生成代理对象的逻辑在父类AbstractAutoProxyCreator中

java 复制代码
private Class<?> createProxyClass(Class<?> beanClass, @Nullable String beanName,
			@Nullable Object[] specificInterceptors, TargetSource targetSource) {
	return (Class&lt;?&gt;) buildProxy(beanClass, beanName, specificInterceptors, targetSource, true);
}

private Object buildProxy(Class&lt;?&gt; beanClass, @Nullable String beanName,
		@Nullable Object[] specificInterceptors, TargetSource targetSource, boolean classOnly) {

	if (this.beanFactory instanceof ConfigurableListableBeanFactory clbf) {
		AutoProxyUtils.exposeTargetClass(clbf, beanName, beanClass);
	}
	//创建代理工厂
	ProxyFactory proxyFactory = new ProxyFactory();
	proxyFactory.copyFrom(this);
	/**
	 *检查proxyTargetClass属性值,spring默认为false
	 *proxyTargetClass检查接口是否对类代理,而不是接口代理
	 *如果代理对象为类,设置为true,使用cglib代理
	 *
	 */
	if (proxyFactory.isProxyTargetClass()) {
		//
		// Explicit handling of JDK proxy targets and lambdas (for introduction advice scenarios)
		if (Proxy.isProxyClass(beanClass) || ClassUtils.isLambdaClass(beanClass)) {
			// Must allow for introductions; can't just set interfaces to the proxy's interfaces only.
			for (Class&lt;?&gt; ifc : beanClass.getInterfaces()) {
				proxyFactory.addInterface(ifc);
			}
		}
	}
	else {
		// No proxyTargetClass flag enforced, let's apply our default checks...
		if (shouldProxyTargetClass(beanClass, beanName)) {
			proxyFactory.setProxyTargetClass(true);
		}
		else {
			evaluateProxyInterfaces(beanClass, proxyFactory);
		}
	}

	Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
	proxyFactory.addAdvisors(advisors);
	proxyFactory.setTargetSource(targetSource);
	customizeProxyFactory(proxyFactory);

	proxyFactory.setFrozen(this.freezeProxy);
	if (advisorsPreFiltered()) {
		proxyFactory.setPreFiltered(true);
	}

	// Use original ClassLoader if bean class not locally loaded in overriding class loader
	ClassLoader classLoader = getProxyClassLoader();
	if (classLoader instanceof SmartClassLoader smartClassLoader &amp;&amp; classLoader != beanClass.getClassLoader()) {
		classLoader = smartClassLoader.getOriginalClassLoader();
	}
	return (classOnly ? proxyFactory.getProxyClass(classLoader) : proxyFactory.getProxy(classLoader));
}</code></pre>
代理工厂有一个重要的属性:proxyTargetClass,默认值为false,也可以通过程序设置
proxyTargetClass
目标对象
代理方式
false
实现了接口
jdk代理
false
未实现接口(只有实现类)
cglib代理
true
实现了接口
cglib代理
true
未实现接口(只有实现类)
cglib代理
CGLIB和JDK代理的区别
1.JDK只能代理接口
2.CGLIB能代理接口和类
SpringFrameWork proxyTargetClass默认为false
spring2.x默认为false
spring3.x默认为true
大部分情况下springframework和springboot是保持一致的,但是spring aop是不一致的
相关推荐
特立独行的猫A14 分钟前
Ionic + 仓颉语言的 cj-tauri 开发框架实战:开发精美移动端 APP
java·后端·python
nice_lcj52026 分钟前
LRU 缓存入门:它是什么、怎么用、面试怎么手写
java·数据结构·缓存·面试·lru
文丑颜不良啊1 小时前
LeetCode2741~2760题目解析
java·leetcode
金銀銅鐵11 小时前
带您看懂字节码指令(二):空循环里有什么
java·后端·ai编程
吴名氏.12 小时前
Java转AI Agent教程(第十三周)混合检索与重排
java·数据库·人工智能·ai·ai agent
.痞老板.13 小时前
java ee--多线程(进阶)
java·java-ee
深入云栈15 小时前
Netty 4.2.x 源码深度解析 (二十三):ByteBuf 核心设计 —— 索引、类型与零拷贝
java·后端·架构
wang_shu_mo_ran16 小时前
MyBatis 从入门到进阶(一)
java·spring·mybatis
量化分析码农17 小时前
股票历史数据的复权参数对照:多级别返回的字段与数值差异
java·javascript·后端