AOP这样理解更简单
# AOP 这样理解更简单
原文参考自这里 https://www.zhihu.com/question/24863332/answer/863736101,非常小白友好向,不涉及源码和底层,只从最上层的应用层面来解释 AOP 是个什么东西,我做了一些修改并重新配了图,帮助小伙伴更好理解 AOP~
老规矩,背诵版在文末。点击阅读原文可以直达我收录整理的各大厂面试真题
先来看一个简单的小例子,体会一下 AOP 能解决什么问题:
假设有这么三个类 A B C,它们都拥有各自实现的 do 方法:
如果我们要在 A,B,C 三个类的 do 方法中的最后执行一个名为 log
方法来打印日志,最简单的,我们可以这样写:
这样做可以解决问题,但是总感觉有些别扭,每个类的 do 方法中都调用了打印日志的方法,但是,打印日志其实并不是我们的核心业务,我们却要去花费大力气去处理它
随着系统越来越完善,类似这样的非核心业务也会越来越多,比如权限,异常处理,性能监控等
这样的功能出现在很多类的很多方法中干扰了我们的核心业务代码,怎么解决呢?
AOP 就是为此而生:
看看 AOP 是如何解决的?
从上图可以看出日志记录,性能监控,异常处理这样的非核心功能被单独抽取出来了,与业务代码分离,横切在核心业务代码之上
这就是我们通常所说的面向切面编程(AOP),通过一个例子看看他是如何实现的
创建一个 UserDao 类:
创建一个切面类:
当我们创建 UserDao 的对象并调用 addUser 方法的时候会打印出如下两条记录:
添加用户
打印日志
So,很神奇吧~ 究竟发生了什么?
明明 addUser 方法里面只有打印 “添加用户” 啊
这就是 Spring AOP 的强大之处,在运行时通过动态代理技术对 UserDao 的 addUser 方法进行了增强,添加了打印日志的功能。
动态代理其实就是在运行时动态的生成目标对象的代理对象,在代理对象中对目标对象的方法进行增强,之前文章 字节一面 • 作文题 • 动态代理具体是怎么实现 (opens new window) 有过详细的介绍,小伙伴们可以直接公众号里搜一下看看,下面来解释下 AOP 中几个重要的概念。
# 通知 Advice
就是会在目标方法执行前后执行的方法,定义增强的逻辑
上面这个例子中:
这个 log 方法就是通知,目标方法是 UserDao 类的 addUser()
在 addUser 执行之后执行了 log 方法,所以 log 方法是后置通知,通过在方法上加上 @After
注解来表示。
通过通知和目标方法的执行顺序我们可以把通知分为五种:
- 前置通知(before):在目标方法执行之前执行
- 后置通知(after):在目标方法执行之后执行
- 后置返回通知(after returning):在目标方法返回之后执行,先执行后置通知再执行后置返回通知
- 异常通知(after throwing):在目标方法抛出异常时执行
- 环绕通知(around): 功能最强大的Advice,可以在目标函数执行中执行,可以自定义执行顺序
这几种通知的执行顺序如下:
# 连接点 Join point
连接点就是可以应用通知进行增强的方法
因为 Spring Aop 只能针对方法进行增强,所以这里的连接点指的就是需要被增强的方法
如上例中的:
这三个方法都可以是连接点
# 切入点 Pointcut
应用通知进行增强的目标方法
现在面临的问题是如何去描述这个需要被增强的目标方法(Target object)(换句话说,如何去描述和定位连接点),如果只是一个具体的方法需要增强那简单,通过类名和方法名找到它就可以了,但是往往真实的需求中很多方法需要同样的通知进行增强,Spring AOP 为我们提供了一个描述方法的语法比如上例中的:
execution(* cn.xh.dao.UserDao.addUser(..)
就是用来描述需要应用通知的方法的,这里的含义是com.veal.dao 包 UserDao 类中的参数任意,返回值任意的 addUser 方法,这个表达式称之为切入点表达式。
so,切入点表达式其实就是一个匹配连接点的断言或者表达式。
而应用通知进行增强后的连接点,就称为切入点。
# 切面 Advisor
切入点和通知的结合
通知定义了需要做什么,切入点定义了在哪些类的哪些方法中执行通知,那么需要将他们 2 个组合起来才有效啊。
切面就是做这个事情的,可以用切面类来表示,通常有2个关键信息:
- 需要增强的目标方法列表,这个通过切入点 (Pointcut) 来指定
- 需要在目标方法中增强的逻辑,这个通过 (Advice) 通知来指定
在 MyAspectLog 这个切面类中,通过切入点表达式指定了切入点 addUser ,并且又包含了通知 log()
# 织入 Weaving
就是通过动态代理对目标对象方法进行增强的过程
这就是个名词,没什么特殊意义,很多文章和文档中都会用这个词,防止各位小伙伴不清楚,这里提一嘴~
最后放上 AOP 的背诵版(这里不扯源码,多扯点动态代理,后面会有扯源码的背诵版~):
🥸 面试官:讲一下你对 AOP 的理解
😎 小牛肉:AOP 就是在运行时通过动态代理技术对目标方法进行增强,可以在目标方法的调用前后或者调用过程中执行其他额外的逻辑。
动态代理它其实是代理模式的一种,所谓代理模式就是,使用代理对象来代替对真实对象的访问,这样就可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。代理模式有三大角色:
- Real Subject:真实类,也就是被代理类、委托类。用来真正完成业务服务功能
- Proxy:代理类。将自身的请求用 Real Subject 对应的功能来实现,代理类对象并不真正的去实现其业务功能
- Subject:定义 RealSubject 和 Proxy 角色都应该实现的接口
代理模式分为静态代理和动态代理。
先来说静态代理:
静态代理就是,对于你想要增强的委托类,我们需要新建一个代理类,这两个类实现一个同样的接口,然后将委托类注入进代理类中,在代理类的方法中调用委托类中的对应方法。这样,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情。
从 JVM 层面来说, 静态代理就是在编译时就将接口、委托类、代理类这些都变成了一个个实际的
.class
文件。静态代理的弊端很明显,一个委托类对应一个代理类,多个委托类就需要新建多个代理类,我们能不能将代理类做成一个通用的呢?
为此,动态代理应用而生。
动态代理的实现方式有很多种,Spring 中使用了 JDK 动态代理和 CGLIB 动态代理
先来说 JDK 动态代理:
同样的,JDK 动态代理需要委托类实现一个接口,不过代理类就不需要也实现同样的接口了,但是,JDK 动态代理机制中添加了一个新的角色,那就是处理类。具体来说,我们需要新建一个处理类,然后将委托类注入处理类,另外,这个处理类需要实现
InvocationHandler
接口,并重写其invoke
方法,在invoke
方法中可以利用反射机制调用委托类的方法,并可以在其前后添加一些额外的处理逻辑。最后,我们定义一个创建代理对象的工厂类(代理类),通过Proxy.newProxyInstance()
创建委托类对象的代理对象
JDK 动态代理有一个最致命的问题是它只能代理实现了某个接口的实现类,并且代理类也只能代理接口中实现的方法,要是实现类中有自己私有的方法,而接口中没有的话,该方法就不能进行代理调用。
为了解决这个问题,我们可以用 CGLIB 动态代理机制,CGLIB(Code Generation Library)其实就是一个基于 ASM 的 Java 字节码生成框架。
解释一下什么是字节码生成框架:
一个
Class
类对应一个.class
字节码文件,对吧,也就是说字节码文件中存储了一个类的全部信息。字节码其实是二进制文件,内容是只有 JVM 能够识别的机器码。JVM 解析字节码文件也就是加载类的过程是这样的:JVM 读取
.class
字节码文件,取出二进制数据,加载到内存中,解析字节码文件内的信息,然后生成对应的Class
类对象。显然,这个过程是在编译期就发生的。
那如果我们在运行期遵循 Java 编译系统组织
.class
字节码文件的格式和结构,生成相应的二进制数据(这就是字节码工具做的事情),然后再把这个二进制数据加载转换成对应的类。这样,我们不就完成了在运行时动态的创建一个类吗。这个思想其实也就是动态代理的思想。简单来说,动态代就是通过字节码技术生成一个子类,并在子类中拦截父类方法的调用(这也就是为什么说 CGLIB 是基于继承的了),织入额外的业务逻辑。关键词就是拦截,CGLIB 引入一个新的角色方法拦截器,让其实现接口
MethodInterceptor
,并重写intercept
方法,这里的intercept
用于拦截并增强委托类的方法(和 JDK 动态代理InvocationHandler
中的invoke
方法类似),最后,通过Enhancer.create()
创建委托类对象的代理对象。
如果不去说 AOP 的其他源码的话,扯了这么多动态代理我觉得其实也已经够了,如果后面还想继续装波杯说一说源码,记得关注小牛肉的后续文章哦~