最近跟着源码研究了下Spring使用三级缓存处理循环依赖的原理,里面涉及到Spring AOP的概念;本篇介绍AOP相关的知识点,AOP下的概念名词比较多,尽量使用通俗的概念来逐个解释,内容包括:对AOP(面向切面编程)的理解、AOP下的名词概念、Spring AOP与AspectJ的比较等;
这部分知识对于学习Spring框架或者面试,都非常重要,需要掌握;
乍一看AOP,上来就是一大堆术语,而且还有个拉风的名字,面向切面编程,都说是面向对象编程OOP的一种有益补充等等;这些概念一下子让人不知所措,会觉得AOP多难多难;当我看进去以后,我才发现:它就是一些Java基础上的朴实无华的应用,包括控制反转IOC,包括许许多多这样的名词,都是万变不离其宗而已;
面向切面的程序设计(Aspect-oriented programming,AOP,又译作面向切面的程序设计),是计算机科学中的一种程序设计思想,旨在将横切关注点与业务主体进行进一步分离,以提高程序代码的模块化程度。
通过在现有代码基础上增加额外的通知(Advice)机制,能够对被声明为“切点(Pointcut)”的代码块进行统一管理与装饰,比如说:“对所有方法名以set*开头的方法添加后台日志”。
该思想使得开发人员能够将与代码核心业务逻辑关系不那么密切的功能(如日志功能)添加至程序中,同时又不降低业务代码的可读性。
——《面向切面编程AOP-维基百科》
以上来自于WIKI中对AOP的解释;学习Java的时候,印象最深刻的就是它是一门面向对象的语言,也就是OOP(Object Oriented Programming),这里的对象包含两个含义,其中一个是数据(属性),另外一个是动作(方法),即OOP把相关的数据和动作当成一个整体来看待,从更高的层次来进行系统建模;那么AOP与OOP之间有什么关系呢?
OOP也就是我们熟悉的面向对象编程,将某个业务处理过程中涉及的属性和方法进行抽象封装,以获得职责更加清晰高效的逻辑单元划分;
举个例子,一个用户通知功能,通知方式上包括:短信、PUSH、邮件、AI电话、站内信;要实现上述功能,在OOP的思想下,我们发现每种通知方式都具备相同的行为:将通知发放给指定用户;此外通知对象具备相同的属性:用户标识、通知流水号(幂等)、通知时间、通知内容、通知重试次数等;并且通知结果也具备相同的属性:通知流水号、通知是否成功、通知到达时间;
于是,我们将发放用户通知的行为抽象成接口,接口方法为发放用户通知;将接口的入参/返参抽象,用通知类型来区分不同的参数含义;如此以来,带来的好处是:
1. 发放通知的行为被接口规范化,从接口定义上可以明确其方法的入参和返参;实现不同的通知方式时,提醒开发人员会去关注幂等、重试、结果回调等;
2. 出入参被抽象,因此通知记录数据都可以放在同一个数据表中,方便数据管理和检索;
3. 便于将公共的功能模块抽出,如异常处理、重试、入库时事务保证、后台查询;
4. 修改某个接口实现时,影响范围小,测试工作量少;如发送短信的第三方接口升级,仅需要修改短信发放这1个接口实现即可,其他实现都不会受影响;
5. 与设计模式原则契合,即依赖倒置原则 (DIP) 所表达的——程序不应依赖于实现,而应依赖于抽象;
但是OOP也有它的缺点;
再举一个例子,我们需要在多个Service的方法前后打印出入参,这个行为可以被看做成一个"关注点",因为它他不针对某一个类,而是面向多个类;如果使用OOP的思想,我们无能为力,需要手动的在每个类的方法上手动加上日志,最多能做的是将打印参数的方法抽象到某个工具类,这个类具备"打印参数"的能力,如:
/**
* 抽象出来的日志工具类
*/
public class LogUtil {
// 打印方法参数
public void printLog(Class clazz, String methodName, Object param) {
final Logger logger = LoggerFactory.getLogger(clazz);
logger.warn("call {}#{}. [param={}]", clazz.getSimpleName(), methodName, JSON.toString(param));
}
}
但是,当我们以AOP的思想来看问题,即面向"关注点",我们就会思考:是否有一种方式,能将"打印日志"这个步骤嵌入到各个类的方法里面去,也就是需要一种可以在不改变原来代码的基础上,通过“动态注入”代码,来改变原来执行结果的技术;这时,一个词从脑海中蹦了出来——代理!没错,代理就是AOP思想的一种实现;
于是,我们可以定义代理工厂,将原Service对象传入,返回其代理对象,在代理对象执行方法的时候,加上通用的日志打印逻辑,如下:
public class ProxyFactory {
// (原)目标对象
private Object target;
public ProxyFactory(Object target) {
this.target = target;
}
public Object getProxyInstance() {
// 为目标对象生成代理对象
return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("当前方法:" + method.getName() + "入参:" + JSON.toJsonString(args));
// 执行目标对象方法
Object returnValue = method.invoke(target, args);
return returnValue;
}
});
}
}
尽管Java是面向对象的语言,JDK中也提供了动态代理API;
可见,AOP和OOP各自作为一种编程思想,是互不冲突的,并且是互补的;
OOP与AOP的关系,小结如下:
1. OOP(面向对象编程)针对业务处理过程的实体,对其属性和方法进行抽象和封装,以获得更加清晰高效的逻辑单元划分;但是,当聚焦"关注点"时,面向对象无法简单的解决这个问题,因为一个关注点是面向所有的类而不是单一的类,因此OOP无法解决关注点聚焦的问题,它的实现只能分散到接口的各个子类中;
2. AOP(面向切面编程)则是针对业务处理过程中的某个步骤或阶段进行提取和抽象,以获得逻辑过程中各部分之间低耦合性的隔离效果;
3. 这两种设计思想在目标上有着本质的差异,AOP并不是与OOP对立的,而是为弥补OOP的不足;
AOP下有很多专业的名词,如"切入点"、"切面"、"织入"等;为了充分理解AOP下的各个名词的概念,先来弄清楚为什么需要AOP;我的理解就是:
- 说白了就是为了方便;编程的人都是“懒人”,因为他把自己做的事情都让程序做了;用了AOP能让你少写很多代码;
- 为了让业务逻辑更加清晰和纯粹,也就是让你的业务代码仅关注自己本身的业务逻辑,而不去想一些其他的事情,这些其他的事情包括:事物开启/提交、日志打印、统一入参校验、统一返参封装、全局异常捕获等;
这里继续用上面的例子,有A、B、C三个方法,要求:在调用每个方法之前,要求打印方法入参,在调用每个方法之后打印返参,在方法发生业务异常时打印异常信息;
小白的思路可能是在每个方法的第一行和最后一行添加一句日志打印,在整个代码块包裹上try/catch;在这样的思路下,如果方法很多,就需要每个方法挨个去复制这段代码,最终产生很多重复的代码;
换个思路试下,能不能把打印日志这个功能封装一下,然后让它能在指定的方法的某个指定位置(比如执行方法前,或者执行方法后)自动的去调用呢?如果可以的话,业务功能代码中就不会掺杂这些与业务无关的代码,显得清晰整洁;
而AOP就可以做这一类的工作,比如,日志输出,事务控制,异常的处理等;
如果把AOP当做成给我们写的“业务功能”增添一些"增强处理",就会有这么几个问题:
带上上面的疑问,下面来分别介绍下与AOP相关的一些术语;
通知Advice包括2个点:
(1)定义我们要做的"增强操作"是什么,就是上面描述的"关注点"——即"业务处理过程中的某个步骤或阶段",如日志输出、事务控制、异常处理等;
(2)以及使用"增强操作"的时机(方法前/后/异常时);这些时机围绕着目标方法执行过程中的各个阶段,对应的注解有@Before/@After/@Around/@AfterReturing/@AfterThrowing)
把这些步骤和时机(通知Advice)先定义好,然后在想用的地方(切点Pointcut)嵌入进去(织入weaving);
这个比较好解释,就是Spring理论上允许你使用通知Advice的地方,就是通知Advice和目标方法连接到一起的地方;基本每个方法的执行前/执行后/执行中,或抛出异常时都可以是连接点JoinPoint;
Spring只支持方法连接点,因为Spring的AOP是基于动态代理的;其他的AOP实现如AspectJ还可以让构造器执行时或属性注入时成为连接点;
连接点是在应用执行过程中能够插入切面的一个点;对于Spring AOP来说,一个方法的前前后后有多个地方可以织入通知,这些位置都可作为连接点;如下方代码中的ProceedingJoinPoint对象:
@Aspect
@Component
public class AccountValidateAspect {
@Around(value = "@annotation(com.XX.web.controller.handler.annotation.AccountCheck) && @annotation(accountCheck)")
public Object paramValidate(ProceedingJoinPoint joinPoint, AccountCheck accountCheck) throws Throwable {
// 连接点joinPoint执行前 做一些事情...
Object proceed;
try {
rerturn joinPoint.proceed();
}
// 连接点joinPoint执行时异常 打日志 封装方法返回参数
catch (IllegalArgumentException ie) {
log.error("paramValidate_error! [args={}]", joinPoint.getArgs(), e);
return BaseResponse.buildResult(ResultCodeEnum.PARAMS_ERROR, ie.getMessage());
}
// 连接点joinPoint执行后 做一些事情...
}
}
在上面说的连接点JoinPoint的基础上,来定义切入点Pointcut;
一个切面Aspect并不需要通知应用的所有连接点;连接点的数量太大了,而切点就是用来筛选连接点的;切点有助于缩小切面所通知的连接点的范围;
例如一个程序执行流程里,涉及多个Service类,而每个Service类里有多个方法,那么理论上执行流程中可能有几十甚至上百个连接点JoinPoint;但是你并不想在所有方法附近都使用通知Advice(使用通知这一行为被称为"织入"),你只想选中其中的几个类/几个方法来织入通知;而切点Pointcut就是用来标记的,用来选中那几个你想要的方法;
切点的定义会匹配通知所要织入的一个或多个连接点;可以通过正则表达式定义所匹配的类和方法名称来定义切点;
根据2.2中的代码,"execution(public * com.XX.web.controller..*(..)) && @annotation(com.XX.web.controller.handler.annotation.ResultHandler)"就是切入点,表示标记"com.XX.web.controller目录下且打了@ResultHandler注解"的方法",切点是可以单独拿出来定义的,方便复用,Spring中切点的定义示例如下:
@Aspect
@Component
public class ResultHandlerAspect {
// 标记"com.XX.web.controller目录下且打了@ResultHandler注解"的方法
@Pointcut("execution(public * com.XX.web.controller..*(..)) && @annotation(com.XX.web.controller.handler.annotation.ResultHandler)")
public void pointCut4ResultHandler(){}
@Around("pointCut4ResultHandler()")
public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {//...}
@Before("pointCut4ResultHandler()")
public Object beforeMethod(ProceedingJoinPoint joinPoint) throws Throwable {//...}
}
从上面代码的Aspect的定义就可以看出,切面Aspect是通知Advice和切点Pointcut的结合;
通知说明了干什么和什么时候干(通过注解的名称Before/After/Around等就能知道),而切入点说明了在哪干(指定到底是哪些类哪些方法,如类路径和方法签名的指定注解),这就是一个完整的切面定义;
织入是把切面应用到目标对象并创建新的代理对象的过程;切面Aspect在指定的连接点JointPoint被织入到目标对象Target中;
在目标对象的生命周期里有多个时期可以进行织入,包括:编译期、类加载期、运行时;
被AOP框架进行增强处理的对象,也就是要被通知的对象,也是真正的业务逻辑;通过织入切面,让目标专注于业务本身的逻辑;
对于Spring AOP来说,目标就是一个被代理的对象,在具体一点就,就是一个目标方法;
实现整套AOP机制的,都是通过代理;一个类被AOP织入增强后,就产生了一个结果类,它是融合了原目标类类和被增强后的代理类;
根据不同的代理方式,代理类既可能是和目标类实现相同接口的类(JDK动态代理),也可能是原类的子类(CGlib代理);类型相同,所以我们可以用代理类的对象替换目标对象;
Spring AOP用到的代理模式的实现包括JDK动态代理和CGlib代理,相关知识可以看看我之前写的一篇《Spring——AOP用到的代理模式》;
前面讲的通过织入切面,可以在原目标对象执行方法时,做一些额外的"增强操作",如日志打印;而引入(introduction)是一种特殊的增强,它可以为目标类添加一些属性和方法;
例如,即使一个目标类原本没有实现某个接口,通过AOP的引入Introduction功能,我们可以让目标类成为这个接口的实现类,从而获得接口的方法和属性;
相对于使用自定义切面Aspect,我在日常开发中几乎未使用过引入introduction,使用方法可以参考下面的文章:《Spring-AOP通过注解@DeclareParents引入新的方法》
AOP实现的关键就在于AOP框架自动创建的AOP代理;AOP代理则可分为静态代理和动态代理两大类,其中静态代理是指使用AOP框架提供的命令进行编译,从而在编译阶段就可生成 AOP 代理类,因此也称为编译时增强;而动态代理则在运行时借助于JDK动态代理、CGLIB等在内存中“临时”生成AOP动态代理类,因此也被称为运行时增强;
如今已经存在很多AOP相关的类库和框架,例如AspectJ、JAC、Nanning等,这些类库都有它们独特的目标和规范;
作为Java开发者,我们都很熟悉Spring这个框架,在Spring框架的核心思想之一就是面向切面编程AOP;Spring AOP旨在通过Spring IOC容器来提供一个简单的AOP实现,使用纯Java代码实现;在性能上,由于Spring AOP是基于动态代理来实现的,在容器启动时需要生成代理实例,并且在方法调用上也会增加栈的深度,使得Spring AOP的性能不如AspectJ的那么好;
AspectJ是AOP编程的完整的解决方案;AspectJ来自于Eclipse基金会,与Spring框架无关;AspectJ属于静态织入,通过修改代码来实现,在编译器生成代理;关于AspectJ的相关知识,这里不展开介绍,想了解更多AspectJ的知识可以参考《浅谈AOP以及AspectJ和Spring AOP 》和《AspectJ 简介》;
下面对AspectJ和Spring AOP从多个维度来作一个简单的比较;
Spring AOP和AspectJ有不同的目标:
Spring AOP旨在通过Spring IOC容器来提供一个简单的AOP实现,它并不是一个完整的AOP解决方案,它只能应用于由Spring容器管理的Bean。
另一方面,AspectJ是最初的AOP技术,旨在提供完整的AOP解决方案;它比Spring AOP更健壮,但也要复杂得多,AspectJ可以应用于所有域对象;
AspectJ和SpringAOP使用不同类型的织入方式,这会影响它们在性能和易用性方面的表现;
AspectJ使用三种不同的植入方式:
(1)Compile-time weaving(编译时织入):AspectJ编译期将切面和应用程序源代码作为输入,生成一个经过织入得类文件作为输出;
(2)Post-compile weaving(编译后织入):也称为二进制织入,它是把切面织入现有的类文件或者JAR文件;
(3)Load-time weaving(加载时织入):这与之前的二进制织入完全相同,区别在于织入过程延迟到类加载器加载类文件到JVM的时候;
AspectJ使用编译时织入和类加载时织入,而Spring AOP使用运行时织入(应用程序执行过程中使用目标对象的代理,包括使用JDK动态代理或CGLIB代理);
Spring AOP是一个基于代理的AOP框架,这意味着要将目标对象织入切面,它会创建该对象的代理,通过JDK动态代理和CGLIB代理这两种方式来实现;
AspectJ在运行时不做任何事情,它与Spring AOP不同,它不需要任何设计模式;为了将切面织入到代码中,它引入了一个AspectJ编译器(ajc),通过它编译程序,然后通过提供一个小的(<100K)运行库来运行程序;
上面我们提到Spring AOP是基于动态代理的,这需要代理类与目标类具备相同的类型(多态),无论是通过实现相同接口还是继承父类的方式;这也带来一些局限性,用到了继承,则目标类就不能被声明为final的类,因为声明为final的类不能被继承,因此Spring AOP只支持方法执行作为连接点;
然而,AspectJ在运行前的编译期将切面织入到代码中;与Spring AOP不同,它不要求对目标对象子类化,因此除了方法,还支持其他类型的连接点,如构造方法调用、静态代码块、字段赋值等;
值得注意的是,在Spring AOP中,切面不能应用于在同一个类中的方法调用,也就是说一个类中的方法中调用了这个类中的其他方法会导致切面失效,因为此时执行方法的是目标对象而非代理对象,想要拿到目标对象可以从ApplicationContext中直接获取,如下:
// (1) 开启EnableAspectJAutoProxy注解,设置代理暴露方式为true
/**
* EnableAspectJAutoProxy注解两个参数作用分别为:
* (1)proxyTargetClass: 控制aop的具体实现方式,为true的话使用cglib,为false的话使用java的Proxy,默认为false;
* (2)exposeProxy:控制代理的暴露方式,解决内部调用不能使用代理的场景,默认为false;
*/
@EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true)
@SpringBootApplication
public class SpringAopApplication {
public static void main(String[] args) {
SpringApplication.run(SpringAopApplication.class, args);
}
}
// (2) 调用法方时去拿当前类的代理对象
public class A {
@Transactional
public void doA() { // ... }
public void doB() {
// 这里显示的取A的代理对象
((A)AopContext.currentProxy()).doA();
// ...
}
}
Spring AOP显然更简单,因为是运行时织入,它没有在构建过程中引入任何额外的编译器或者织入器;但是它只适用由Spring容器管理的Bean;
相比之下,要使用AspectJ,需要引入引入AspectJ Java工具,包括:编译器(ajc),调试器(ajdb),文档生成器(ajdc),程序结构浏览器(ajbrowser),我们需要将这些工具与IDE或构件工具集成;
就性能而言,编译时织入比运行时织入更快;
Spring AOP是在IOC容器启动时生成代理实例,并且在方法调用上也会增加栈的深度,使得Spring AOP的性能不如AspectJ的那么好;
而AspectJ在应用程序执行前就将切面织入到代码中,因此运行时没有额外的开销;
下面的表格总结了Spring AOP和AspectJ之间的关键区别:
Spring AOP | AspectJ |
---|---|
纯Java实现 | 使用Java语言的扩展实现 |
不需要额外的编译过程 | 需要AspectJ编译器ajc(除非采用LTW加载时织入) |
仅支持使用运行时织入 | 不支持运行时织入,支持编译时、载入时织入 |
功能不强但是日常够用-仅支持方法级编织 | 功能更强大,支持字段、方法构造器、静态块、final类/方法等 |
只能在由Spring容器管理的Bean上实现 | 可以在所有域对象上实现 |
仅支持方法执行切点 | 支持所有切点 |
为目标对象创建代理对象,将切面应用到代理对象 | 程序执行前将切面植入到代码中 |
性能差一些,比AspectJ慢多了 | 性能更好 |
易于学习,Java后端开发经常使用 | 相对于Spring AOP来说更复杂 |
看起来从功能和性能上,AspectJ都强于Spring AOP,但是并不能说某一个框架一定完全优于另外一个,很大程度上还是要取决我们的需求;从以下几点来讨论:
(1)框架:如果应用程序不使用Spring框架,那么我们别无选择,只能放弃使用Spring AOP的想法,因为它无法管理任何超出spring容器范围的东西;实际上,作为Java后端开发,Spring框架目前还是主流框架;
(2)灵活性:由于仅支持方法执行切点,Spring AOP并不是一个完整的AOP解决方案,但它已经可以解决我们日常开发中常见问题,如事务、异常、日志;如果我们想用除了方法执行连接点以外的其他连接点,那么就使用AspectJ;
(3)性能:尽管编译时织入比运行时织入更快,但是如果我们使用少量的切面,那么二者性能差异很小;但是应用程序有大量的切面,那最好选择AspectJ;事实上,日常开发也用不到那么多切面的;
(4)共同优点:这两个框架其实是兼容的,我们可以使用Spring AOP,并仍然可以使用AspectJ来补充Spring AOP不支持的连接点;
(1)什么是AOP?包括的名词及含义?
AOP,Aspect-oriented programming,面向切面编程,是一种编程思想;包含的概念有:
通知(Advice):切点处被增强的行为,如日志、事务;
连接点(JoinPoint):是程序执行期间的一个点,如方法前后、异常、构造方法调用等;
切入点(Pointcut):用来筛选连接点,用表达式匹配;
切面(Aspect):切面Aspect是通知Advice和切点Pointcut的结合;
织入(weaving):把切面应用到目标对象并创建新的代理对象的过程;
目标(target):被AOP框架进行增强的对象,也就是要被通知的对象,是真正的业务逻辑;
代理(target):实现整套AOP机制的,都是通过代理;
引入(introduction):是一种特殊的增强,它可以为目标类添加一些属性和方法;
(2)AOP与OOP的关系?
OOP(面向对象编程)针对业务处理过程的实体,对其属性和方法进行抽象和封装,以获得更加清晰高效的逻辑单元划分;OOP无法解决关注点聚焦的问题,因为一个关注点是面向所有的类而不是单一的类;
AOP(面向切面编程)则是针对业务处理过程中的某个步骤或阶段进行提取和抽象,以获得逻辑过程中各部分之间低耦合性的隔离效果;
这两种设计思想在目标上有着本质的差异,AOP并不是与OOP对立的,而是为弥补OOP的不足;
(3)Spring AOP与AspectJ的区别?
Spring AOP纯Java实现,不需要额外的编译器,通过动态代理实现,只能在运行时织入;仅支持方法级编织,功能不强但是日常够用,性能比AspectJ差;易于学习,Java后端开发经常使用;
AspectJ使用Java语言的扩展实现,使用时需要AspectJ编译器ajc(除非采用LTW加载时织入);支持编译时、载入时织入;功能更强大,支持字段、方法构造器、静态块、final类/方法等,它提供完整的AOP解决方案;AspectJ在程序执行前将切面植入到代码中,性能更好,但是相对于Spring AOP来说更复杂;
参考:
Spring AOP面向切面编程:理解篇(一看就明白)- 华为云
SpringAOP基本用法(上) - 知乎
Spring AOP与AspectJ的比较 | WongLay's Blog
面试官:什么是AOP?Spring AOP和AspectJ的区别是什么? - SegmentFault 思否
浅谈AOP以及AspectJ和Spring AOP - SegmentFault 思否