JaneRoad

V1

2022/07/26阅读:8主题:全栈蓝

最近老被问到AOP

简述AOP

AOP又叫面向切面编程,旨在通过允许横切关注点的分离,提高模块化。通俗理解就是,将那些与业务无关,却为业务模块所共同调用的逻辑代码封装起来,形成一个切面,使原来的业务功能更加强大,即增强,并减少重复代码,降低模块间的耦合度,方便后期操作和维护

AOP相关概念

切面(Aspect)

  • 切面是一个横切关注点的模块化,一个切面能够包含同一个类型的不同增强方法,比如说事务处理和日志处理可以理解为两个切面。
  • 切面由切入点和通知组成,它既包含了横切逻辑的定义,也包括了切入点的定义。
  • Spring AOP就是负责实施切面的框架,它将切面所定义的横切逻辑织入到切面所指定的连接点中。
@Component
@Aspect
public class LogAspect {
}

目标对象(Target)

目标对象指将要被增强的对象,即包含主业务逻辑的类对象。或者说是被一个或者多个切面所通知的对象

连接点(JoinPoint)

  • 程序执行过程中明确的点,如方法的调用或特定的异常被抛出。连接点由两个信息确定

    • 方法(表示程序执行点,即在哪个目标方法)
    • 相对点(表示方位,即目标方法的什么位置,比如调用前,后等)
  • 简单来说,连接点就是被拦截到的程序执行点,因为Spring aop只支持方法类型的连接点,所以在Spring中连接点就是被拦截到的方法。

@Before("pointcut()")
public void log(JoinPoint joinPoint) //这个JoinPoint参数就是连接点
}

切入点(PointCut)

  • 切入点是对连接点进行拦截的条件定义。切入点表达式如何和连接点匹配是AOP的核心,Spring缺省使用AspectJ切入点语法。
  • 一般认为,所有的方法都可以认为是连接点,但是我们并不希望在所有的方法上都添加通知,而切入点的作用就是提供一组规则(使用 AspectJ pointcut expression language 来描述) 来匹配连接点,给满足规则的连接点添加通知。

通知(Advice)

  • 通知是指拦截到连接点之后要执行的代码,包括了around、before和after等不同类型的通知。
  • Spring AOP框架以拦截器来实现通知模型,并维护一个以连接点为中心的拦截器链。
// @Before说明这是一个前置通知,log函数中是要前置执行的代码,JoinPoint是连接点,
@Before("pointcut()")
public void log(JoinPoint joinPoint) 
}

织入(Weaving)

  • 织入是将切面和业务逻辑对象连接起来, 并创建通知代理的过程。
  • 织入可以在编译时,类加载时和运行时完成。
  • 在编译时进行织入就是静态代理,而在运行时进行织入则是动态代理。

增强器(Advisor)

  • Advisor是切面的另外一种实现,能够将通知以更为复杂的方式织入到目标对象中,是将通知包装为更复杂切面的装配器。
  • Advisor由切入点和Advice组成。
  • Advisor这个概念来自于Spring对AOP的支撑,在AspectJ中是没有等价的概念的。Advisor就像是一个小的自包含的切面,这个切面只有一个通知。切面自身通过一个Bean表示,并且必须实现一个默认接口。
// AbstractPointcutAdvisor是默认接口
public class LogAdvisor extends AbstractPointcutAdvisor {
 private Advice advice; // Advice
 private Pointcut pointcut; // 切入点

 @PostConstruct
 public void init() {
 // AnnotationMatchingPointcut是依据修饰类和方法的注解进行拦截的切入点。
 this.pointcut = new AnnotationMatchingPointcut((Class) null, Log.class);
 // 通知
 this.advice = new LogMethodInterceptor();
 }
}

AOP常用注解

  • @Aspect :作用是把当前类标识为一个切面供容器读取
  • @Pointcut : 切入点是对连接点进行拦截的条件定义,在程序中主要体现为书写切入点表达式
  • @Before :标识一个前置增强方法,相当于BeforeAdvice的功能
  • @AfterReturning :后置增强,相当于AfterReturningAdvice,方法退出时执行
  • @AfterThrowing :异常抛出增强,相当于ThrowsAdvice
  • @After :final增强,不管是抛出异常或者正常退出都会执行
  • @Around :环绕增强,相当于MethodInterceptor

AOP执行顺序

AOP执行顺序-来源掘金
AOP执行顺序-来源掘金

静态代理和动态代理

代理是一种设计模式。

  • 静态代理:代理类和委托类在代码运行前关系就确定了,也就是说在代理类的代码一开始就已经存在了。

对应到AOP实现中指使用AOP框架提供的命令进行编译,从而在编译阶段就可生成 AOP 代理类,因此也称为编译时增强

  • 动态代理:动态代理类的字节码在程序运行时的时候生成。

对应到AOP实现中在运行时借助于JDK动态代理、CGLIB等在内存中“临时”生成AOP动态代理类,因此也被称为运行时增强

JDK动态代理

JDK会帮我们在运行时生成一个代理类,这个代理类实际上就是我们需要代理的接口的实现类。

实现的方法里面会调用InvocationHandler类中的invoke方法,并且同时传入自身被调用的方法的的Method对象和参数列表方便我们编码实现方法的调用。

比如我们调用reduce方法,那么我们就可以通过Method.Invoke(Object obj, Object... args)调用我们具体的实现类,再在周围做一些代理做的事儿。就实现了动态代理。

  • JDK动态代理只能代理有实现接口的类,并且是能代理接口方法,不能代理一般的类中的方法
  • 提供了一个使用InvocationHandler作为参数的构造方法。在代理类中做一层包装,业务逻辑在invoke方法中实现
  • 重写了Object类的equals、hashCode、toString,它们都只是简单的调用了InvocationHandler的invoke方法,即可以对其进行特殊的操作,也就是说JDK的动态代理还可以代理上述三个方法
  • invoke方法中我们甚至可以不用Method.invoke方法调用实现类就返回。这种方式常常用在RPC框架中,在invoke方法中发起通信调用远端的接口等

CGLIB动态代理

利用ASM框架,对代理对象类生成的class文件加载进来,通过修改其字节码生成子类来处理

  • CGlib可以传入接口也可以传入普通的类,接口使用实现的方式,普通类使用会使用继承的方式生成代理类。
  • 由于是继承方式,如果是 static、private、final 修饰的方法是不能被代理的
  • 做了方法访问优化,使用建立方法索引的方式避免了传统Method的方法反射调用.
  • 提供callback filter设计,可以灵活地给不同的方法绑定不同的callback。编码更方便灵活。
  • CGLIB会默认代理Object中finalizeequalstoStringhashCodeclone等方法。比JDK代理多了finalizeclone

AspectJ静态编译织入

静态代理唯一的缺点就是我们需要对每一个方法编写我们的代理逻辑,造成了工作的繁琐和复杂。AspectJ就是为了解决这个问题,在编译成class字节码的时候在方法周围加上业务逻辑。复杂的工作由特定的编译器帮我们做。

  • AspectJ并不是动态的在运行时生成代理类,而是在编译的时候就植入代码到class文件
  • 由于是静态织入的,所以性能相对来说比较好
  • AspectJ不受类的特殊限制,不管方法是private、或者static、或者final的,都可以代理
  • AspectJ不会代理除了限定方法之外任何其他诸如toString(),clone()等方法

Spring AOP中的代理

Spring代理实际上是对JDK代理和CGLIB代理做了一层封装,并且引入了AOP概念:Aspectadvicejoinpoint等等

同时引入了AspectJ中的一些注解@pointCut@after@before等等

Spring AOP 严格的来说都是动态代理,所以实际上Spring代理和AspectJ的关系并不大

在使用Spring AOP的时候通常会在XML配置文件中设置<aop:aspectj-autoproxy proxy-target-class="true"> 来使用CGlib代理

DefaultAopProxyFactory#createAopProxy方法中有一段判断

//optimize 优化,如上列代码编程true会默认进入if
//ProxyTargetClass 是否对具体类进行代理
//判断传入的class 是否有接口。如果没有也会进入选择
if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config))

Spring AOP封装了JDK和CGLIB的动态代理实现,同时引入了AspectJ的编程方式和注解。使得可以使用标准的AOP编程规范来编写代码外,还提供了多种代理方式选择,可以根据需求来选择最合适的代理模式。同时Spring也提供了XML配置的方式实现AOP配置。

CGLIB和JDK动态代理的区别

1、底层实现不同

CGLIB 利用ASM框架加载代理对象的字节码,修改其字节码生成子类来完成方法的增强;JDK动态代理利用拦截器实现 InvocationHandler 加上反射机制生成一个代理接口的匿名类,在调用具体方法前调用 InvokeHandler 来处理

2、实现代理的要求不同

JDK动态代理只能对实现了接口的类生成代理,而不能针对类;CGLIB对接口和类都可以代理,但是因为采用的是继承,所以该类或方法最好不要生成final,对于final类或方法,是无法继承的

3、代理使用场景不同

目标对象生成了接口默认用JDK动态代理,如果Spring配置了可以强制使用CGLIB;如果目标对象没有实现接口,必须采用cglib库,Spring会自动在JDK动态代理和cglib之间转换

4、代理效率不同

CGLIB 效率高于JDK

区别 CGLIB JDK动态代理
原理 动态生成一个要代理的子类,子类重写要代理的类的所有不是final的方法。在子类中采用方法拦截技术拦截所有的父类方法的调用,顺势织入横切逻辑,它比Java反射的jdk动态代理要快 JDK中的动态代理是通过反射类Proxy以及InvocationHandler回调接口实现的,但是JDK中所有要进行动态代理的类必须要实现一个接口,也就是说只能对该类所实现接口中定义的方法进行代理,这在实际编程中有一定的局限性,而且使用反射的效率较慢
是否提供子类代理
是否提供接口代理 是(可不用接口) 是(必须)

Spring AOP和AspectJ 的区别

1、代理类型不同

Spring AOP是在运行期间通过代理生成目标类,属于动态代理。AspectJ是在编译期间将切面代码编译到目标代码的,属于静态代理

2、AspectJ更强大

  • Spring AOP仅仅是方法织入,AspectJ支持编译期织入、编译后织入、类加载后织入,支持所有切入点,不仅仅是方法织入。
  • Spring AOP需要依赖IOC容器来管理,并且只能作用于Spring容器
  • AspectJ在实际运行之前就完成了织入,所以说它生成的类是没有额外运行时开销的,Spring AOP需要生成代理实例,在方法调用上也会增加栈的深度。Spring AOP的性能较AspectJ的差
Spring AOP AspectJ
在纯 Java 中实现 使用 Java 编程语言的扩展实现
不需要单独的编译过程 除非设置 LTW,否则需要 AspectJ 编译器 (ajc)
只能使用运行时织入 运行时织入不可用。支持编译时、编译后和加载时织入
功能不强-仅支持方法级编织 更强大 - 可以编织字段、方法、构造函数、静态初始值设定项、最终类/方法等。
只能在由 Spring 容器管理的 bean 上实现 可以在所有域对象上实现
仅支持方法执行切入点 支持所有切入点
代理是由目标对象创建的, 并且切面应用在这些代理上 在执行应用程序之前 (在运行时) 前, 各方面直接在代码中进行织入
比 AspectJ 慢多了 更好的性能
易于学习和应用 相对于 Spring AOP 来说更复杂

误区

AspectJ是Spring AOP一部分

错误!Spring AOP只是使用了AspectJ的Annotation。,比如@Aspect,@Pointcut,@Before等。但是并没有使用它的编译器和织入器。其实现原理是JDK动态代理和CGLIB,在运行时生成代理类。

注意

为了启用 Spring 对 @AspectJ 方面配置的支持,并保证 Spring 容器中的目标 Bean 被一个或多个方面自动增强,必须在 Spring 配置文件中添加配置<aop:aspectj-autoproxy/>

在高版本的springboot中已默认开启支持,并不需要配置。

JDK动态代理,CGLIB,Spring AOP和AspectJ关系

1、Spring AOP和AspectJ 是两种实现 AOP 的框架,一个是动态代理一个是静态代理

2、Spring AOP动态代理有两种底层技术实现:

  • JDK 动态代理(默认有接口的目标类使用jdk动态代理)

  • CGLIB(没有接口或有接口的目标类使用)

3、Spring AOP采用了AspectJ包提供的注解,但是底层编译器和织入器并不是AspectJ

分类:

后端

标签:

后端

作者介绍

JaneRoad
V1