关于Spring AOP的灵魂十问

简介: AOP全称是Aspect Oriented Programming,翻译过来是“面向切面”编程。在Java语言里,一切皆对象,所以我们通常说Java语言是一门“面向对象”编程的语言。而面向切面编程,不是要取代面向对象编程,而是对它的一种补充。

荒腔走板


今天没有这个环节。。。


什么是AOP?


AOP全称是Aspect Oriented Programming,翻译过来是“面向切面”编程。在Java语言里,一切皆对象,所以我们通常说Java语言是一门“面向对象”编程的语言。而面向切面编程,不是要取代面向对象编程,而是对它的一种补充。

AOP要解决的问题是用一个“横切面”的方式,来统一处理很多对象都需要的,相同或相似的功能,减少程序里面的重复代码,让代码变得更干净,更专注于业务。

网络异常,图片无法展示
|


AOP能做什么?

AOP适合用来做一些比较通用的、与业务关系不大的事情。比较常见的就是调用日志、权限控制、调用时间和性能统计、参数校验等等。

其实Spring基于AOP做了很多有用的东西,比如大家可能会经常使用的事务、缓存、重试、Validation等等,底层都是使用的AOP。

自己用AOP实现一些小功能的需求也比较多。比如之前我们团队就有这样一个需求,我们在服务与服务之间的调用,不使用枚举,而是换成了使用字符串,即枚举相应字段的name。但是这样就带来了一个问题,如果调用方输入了一个不属于这个枚举的字符串,那到后面使用就会抛异常。所以我们希望能够在Controller层就把这个校验做了,该抛异常抛异常。

这是一个参数校验的需求,然而Spring提供的Validation和自定义Validation都无法实现这个需求,因为字段是字符串,但每次需要校验的枚举类型是不同的。所以我们就基于注解和AOP自己开发了这样一个枚举参数校验器。


Spring AOP和AspectJ是什么关系?

其实AOP并不是Spring的专属,AOP最开始是一种编程模型,后来大佬们为了探讨AOP的标准化,统一AOP规范,成立了一个AOP联盟。除了Spring外,AOP的框架有很多,比如AspectJ, AspectWerkz, JBoss-AOP。

最开始,Spring AOP和AspectJ是完全独立的,Spring有自己的实现和使用语法。但是Spring的AOP使用起来太麻烦了,深受大家吐槽。于是Spring支持了广受大家好评的AspectJ语法,通过在配置类上添加@EnableAspectJAutoProxy这个注解来开启对AspectJ的语法。

但Spring仅仅是支持了AspectJ的部分语法(有些语法是不支持的),但底层实现还是自己的一套东西。而且两个框架的目标不同,AspectJ是一套完整的AOP解决方案,更为健壮,但使用起来比较复杂,还需要使用特殊的语法和编译器。而Spring的目的是想要把AOP和IoC框架结合起来,让Spring管理的Bean能够很方便地使用AOP的功能。

所以Spring AOP和AspectJ没啥关系,只是Spring借鉴了Aspect的声明语法。

这里说清楚了AOP、Spring AOP、AspectJ的关系,那下文中我就用AOP代替Spring AOP了,毕竟这是一个比较懒的作者。


如何使用Spring AOP?

学一门技术,首先要学会怎么用,然后再学会原理,最后深入思考,举一反三。而学习怎么用,最好的途径,就是看官方文档

有些同学可能会觉得英文的看起不太方便,所以会去看一些中文的文章或者书。但这些中文的文章或书其实就是把官方文档翻译了一遍,还有可能不全甚至是错误的。所以还是推荐直接看官方文档,哪怕是用自动翻译软件,也能看懂了。何况英语也算是程序员的必修技能之一。

网络异常,图片无法展示
|

AOP有一些专业的概念和术语。切面、连接点、通知、切点、引入、目标对象、代理对象、织入等。很多都是直接根据英文单词翻译过来的,这里我们就不详细介绍这些概念了,Spring官方文档AOP那一章节开头就有详细的介绍。但是这篇文章会从使用的视角来顺带介绍其中的一些术语。

打开AspectJ支持

本文就不介绍XML的配置了,实在是太古老了。。。

使用@EnableAspectJAutoProxy这个注解可以打开AspectJ支持,以后就可以愉快地在Spring中使用AspectJ语法了。需要注意的是,如果你使用的是SpringBoot,这个注解已经默认加上了,不需要再手动写在你的代码里。

声明一个切面

使用@Aspect注解可以声明一个切面。其实就是一个类,在这个类里面去定义切点、通知等东西。

声明一个切点

所谓“切点”,就是你要去切什么地方。Spring只支持去切被Spring管理的Bean的方法。声明一个切点也很简单,在我们上面声明的切面类里面,用下面这种形式创建一个方法就行了:

@Pointcut("execution(* transfer(..))")
private void anyOldTransfer() {}

使用@Pointcut注解,里面是切点的表达式。需要注意这个方法的返回值必须是void的。关于表达式,有一些Spring支持的关键字,这里就不一一细讲了,官方文档上Supported Pointcut Designators这一节有详细介绍。我们最常用的应该是execution@annotation这两个了。

其中也有一些通配符,包括*, ., (), (..), (*), (*, string)等等,都是有不同的含义和作用。具体可以在官方文档Examples这一节了解。

声明通知

“通知”,指的是在什么时候去执行我们定义的AOP逻辑。Spring提供了这样几种通知:

  • @Before
  • @AfterReturning
  • @AfterThrowing
  • @After,其实质是AfterFinally
  • @Around

大家看名字应该都知道是啥意思了吧。其中,@Around包含上面所有的功能,使用起来更强大、灵活。

一个完整的AOP定义大概长这样:

@Aspect
@Component
public class MyAspect {
    @Pointcut("within(com.example.springbase.dao..*)")
    private void myPointcut() {}
    @Before("myPointcut()")
    public void before() {
        System.out.println("before...");
    }
}

需要注意的是这里的@Component注解是必须要加的,不然Spring不会自动扫描这个类,那你定义的切面、切点和通知也就无效了。

一个切面里面可以有多个切点和多个通知,并且一个切点也可以被多个通知使用。


Spring用什么实现的AOP?

前面我们提到过AspectJ,AspectJ使用的是编译期和类加载时进行织入,而Spring AOP利用的是运行时织入。而如果使用运行时织入,就要用到“动态代理”的技术。

先来聊聊动态代理。AOP其实是设计模式中的“代理模式”的一种应用,那什么是代理模式呢?我们举个很常见的例子,就是游戏代练了。

游戏代练高手收了土豪的钱,登上土豪的账号,一路连胜打上最强王者,然后完成交易,功成身退。而游戏中的队友和对手,甚至游戏官方并不知道这是一个代练,还以为是土豪本人,私下里纷纷夸赞土豪技术了得,满足了土豪的虚荣心。。。

代理模式说简单点,就是通过把原本的对象进行包装,提供一些额外的能力,但是对外部而言是无感知的,并不知道或者并不在意这个对象是不是被代理了。

静态代理和动态代理的区别在于,静态代理把要代理的类型已经写死了,一个代理只能代理一种类型。而动态代理就不一样了,一个代理可以代理多种类型。还是拿游戏代练举例,静态代理可能就是这个代练只能代练LOL这一种游戏。而动态代理,这个代练可以代练所有游戏,是不是一听就是大高玩。

Spring底层使用了两种方式来实现动态代理,一种是Java自带的动态代理,另一种是CGLib。如果是使用JDK动态代理生成的代理对象,Debug可以看到JdkDynamicAopProxy,而如果是CGLib生成的对象,可以看到是EnhancerBySpringCGLIB

那Spring具体是使用的哪种方式呢?网上有很多文章说,Spring默认产生代理对象的行为是:如果你的Bean有对应的接口,是使用的基于JDK的动态代理,否则是使用CGLIB。但这样说其实不准确,Spring用了下面这个配置来控制它,如果这个配置是false,才是上面我们说的这个逻辑。而如果这个配置是true,则所有的要使用AOP的Bean都使用CGLIB代理,不管它是不是有接口。而我们使用最新版的SpringBoot的话,这个值默认就是true。

spring.aop.proxy-target-class=true

所以现在如果使用SpringBoot的话,我们的AOP代理对象都是用CGLIB生成的


JDK和CGLib动态代理有什么区别?

两者实现的原理不同,JDK动态代理是基于Java反射来实现的,而CGLIB动态代理是基于修改字节码,生成子类来实现的,底层是使用的asm开源库。

两者都有一些限制,JDK动态代理,Bean必须要有接口;CGLIB不能对final类或方法做代理。


哪些方法可以被代理?

如果是使用JDK动态代理,那只有public方法可以被代理。而如果使用CGLIB,除了private方法,都可以被代理。(当然,final方法除外)。

另一个比较有意思的问题是,如果两个方法在同一个类里面,一个方法调用另一个方法是不会走代理的。只有一个Bean调用另一个Bean的方法,才会走代理。

上面两个特性也就解释了为什么有时候你的@Transactional不生效的原因:

  • 在私有方法上不生效
  • 在final方法上不生效
  • 同一个类里面方法互相调用不生效


代理对象是什么时候生成的?

在上一篇文章中,我们了解了Spring的Bean是如何生成的。在Spring启动的时候,会去调用getBean方法,完成Bean的初始化工作。而在getBan里面,初始化Bean后,会去调用Bean的BeanPostProcessor。这个代码可以通过getBean方法Debug找进去。这里就不细讲Debug过程了,放一张调用栈的截图吧。

网络异常,图片无法展示
|

从Debug可以看到,其中有一个BeanPostProcessor是AnnotationAwareAspectJAutoProxyCreator类型的,继续Debug进去可以看到最终是用了CglibAopProxy类的getProxy()方法生成的代理对象。

网络异常,图片无法展示
|


同一个方法被多次代理怎么办?

一个方法是有可能被多次代理的。Spring AOP不仅仅是基于代理模式,还使用了“拦截器”模式。这个拦截器,有点像Web的拦截器一样,在目标对象上包了一层又一层,形成一个拦截器链。那它们的顺序是如何决定的呢?

我们在前面的源码解析中,有一个分支,逻辑是去除当前这个代理对象的所有“通知”,然后排序。代码在AspectJAwareAdvisorAutoProxyCreator类的方法里。调用栈:

网络异常,图片无法展示
|

这个方法内部先取出所有的通知,然后给它们都加上一个AspectJPrecedenceComparator。这个Comparator会取出通知所在的Bean的@Order注解定义的优先级,按照这个优先级来排序。其实我们有时候使用其它Bean也会用到这个注解。

所以如果你会对一个方法声明多个通知,那可以使用@Order注解来定义这多个通知的优先级。@Order定义的值越小,其内部定义的通知对应的拦截器就会在调用链的越外层。

注意,如果是同一个切面类里面定义的多个通知,会按照方法声明的先后顺序来排序。


AOP和循环依赖的那些事?

大家可能会遇到过或者听说过Spring的循环依赖的问题。Spring使用了“三级缓存”来解决Bean的循环依赖,但可能很多人不知道为什么要使用三级缓存,其实这个也跟AOP有关。

如果没有AOP,其实Spring使用二级缓存就可以解决循环依赖的问题。若使用二级缓存,在AOP情形下,注入到其他Bean的,不是最终的代理对象,而是原始目标对象。

因为Spring对Bean有一个生命周期的定义,而代理对象是在Bean初始化完成后,执行后置处理器的时候生成的。所以不能在二级缓存的时候就直接生成代理对象,放进缓存。


使用AOP有什么弊端?

AOP不是万能的,使用AOP也是有一些弊端的。个人觉得对大的弊端就是让代码可读性变差了,因为它并不是一个显式的调用,所以很有可能未来接手代码的人并不清楚这个方法被AOP代理了。

笔者之前遇到过一个项目,就是使用了AOP来做权限控制,但这个权限控制不是那种简单的Access,而是要去查数据库里面的一些字段,比如状态之类的,还有复杂的逻辑判断。这种情况下,如果使用AOP来做,代码的可读性就不强,出了问题比较难以排查。

目录
相关文章
|
3月前
|
Java
Spring5入门到实战------9、AOP基本概念、底层原理、JDK动态代理实现
这篇文章是Spring5框架的实战教程,深入讲解了AOP的基本概念、如何利用动态代理实现AOP,特别是通过JDK动态代理机制在不修改源代码的情况下为业务逻辑添加新功能,降低代码耦合度,并通过具体代码示例演示了JDK动态代理的实现过程。
Spring5入门到实战------9、AOP基本概念、底层原理、JDK动态代理实现
|
19天前
|
存储 缓存 Java
Spring高手之路23——AOP触发机制与代理逻辑的执行
本篇文章深入解析了Spring AOP代理的触发机制和执行流程,从源码角度详细讲解了Bean如何被AOP代理,包括代理对象的创建、配置与执行逻辑,帮助读者全面掌握Spring AOP的核心技术。
28 3
Spring高手之路23——AOP触发机制与代理逻辑的执行
|
5天前
|
Java Spring
[Spring]aop的配置与使用
本文介绍了AOP(面向切面编程)的基本概念和核心思想。AOP是Spring框架的核心功能之一,通过动态代理在不修改原代码的情况下注入新功能。文章详细解释了连接点、切入点、通知、切面等关键概念,并列举了前置通知、后置通知、最终通知、异常通知和环绕通知五种通知类型。
14 1
|
2月前
|
设计模式 Java 测试技术
spring复习04,静态代理动态代理,AOP
这篇文章讲解了Java代理模式的相关知识,包括静态代理和动态代理(JDK动态代理和CGLIB),以及AOP(面向切面编程)的概念和在Spring框架中的应用。文章还提供了详细的示例代码,演示了如何使用Spring AOP进行方法增强和代理对象的创建。
spring复习04,静态代理动态代理,AOP
|
30天前
|
Java 编译器 Spring
Spring AOP 和 AspectJ 的区别
Spring AOP和AspectJ AOP都是面向切面编程(AOP)的实现,但它们在实现方式、灵活性、依赖性、性能和使用场景等方面存在显著区别。‌
49 2
|
1月前
|
Java Spring 容器
Spring IOC、AOP与事务管理底层原理及源码解析
【10月更文挑战第1天】Spring框架以其强大的控制反转(IOC)和面向切面编程(AOP)功能,成为Java企业级开发中的首选框架。本文将深入探讨Spring IOC和AOP的底层原理,并通过源码解析来揭示其实现机制。同时,我们还将探讨Spring事务管理的核心原理,并给出相应的源码示例。
117 9
|
29天前
|
XML Java 数据格式
Spring的IOC和AOP
Spring的IOC和AOP
43 0
|
2月前
|
Java 数据库连接 数据库
Spring基础3——AOP,事务管理
AOP简介、入门案例、工作流程、切入点表达式、环绕通知、通知获取参数或返回值或异常、事务管理
Spring基础3——AOP,事务管理
|
3月前
|
XML Java 数据格式
Spring5入门到实战------11、使用XML方式实现AOP切面编程。具体代码+讲解
这篇文章是Spring5框架的AOP切面编程教程,通过XML配置方式,详细讲解了如何创建被增强类和增强类,如何在Spring配置文件中定义切入点和切面,以及如何将增强逻辑应用到具体方法上。文章通过具体的代码示例和测试结果,展示了使用XML配置实现AOP的过程,并强调了虽然注解开发更为便捷,但掌握XML配置也是非常重要的。
Spring5入门到实战------11、使用XML方式实现AOP切面编程。具体代码+讲解
|
3月前
|
缓存 Java 开发者
Spring高手之路22——AOP切面类的封装与解析
本篇文章深入解析了Spring AOP的工作机制,包括Advisor和TargetSource的构建与作用。通过详尽的源码分析和实际案例,帮助开发者全面理解AOP的核心技术,提升在实际项目中的应用能力。
42 0
Spring高手之路22——AOP切面类的封装与解析