Spring-AOP
前言
- Spring 框架通过定义切面, 通过拦截切点实现了不同业务模块的解耦,这个就叫面向切面编程 - Aspect Oriented Programming (AOP)
- 为什么@Aspect注解使用的是aspectj的jar包呢?这就引出了Aspect4J和Spring AOP的历史渊源,只有理解了Aspect4J和Spring的渊源才能理解有些注解上的兼容设计
- 如何支持更多拦截方式来实现解耦, 以满足更多场景需求呢? 这就是@Around, @Pointcut… 等切面通知和切点的设计
- 那么Spring框架又是如何实现AOP的呢? 这就引入代理技术,分静态代理和动态代理,动态代理又包含JDK代理和CGLIB代理等
如何理解AOP
AOP的本质也是为了解耦,它是一种设计思想; 在理解时也应该简化理解。
AOP是什么
AOP为Aspect Oriented Programming的缩写,意为:面向切面编程。
AOP最早是AOP联盟的组织提出的,制定的一套规范。Spring将AOP的思想引入框架之中,通过预编译方式和运行期间动态代理实现程序的统一维护。
将记录日志功能解耦为日志切面,它的目标是解耦。进而引出AOP的理念:就是将分散在各个业务逻辑代码中相同的代码通过横向切割的方式抽取到一个独立的模块中。
OOP面向对象编程,针对业务处理过程的实体及其属性和行为进行抽象封装,以获得更加清晰高效的逻辑单元划分。
OOP面向对象,允许开发者定义纵向的关系,但并不适用于定义横向的关系,导致了大量代码的重复,而不利于各个模块的重用。
而AOP则是针对业务处理过程中的切面进行提取,它所面对的是处理过程的某个步骤或阶段,以获得逻辑过程的中各部分之间低耦合的隔离效果。
AOP,一般称为面向切面,作为面向对象的一种补充,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块,这个模块被命名为“切面”(Aspect),减少系统中的重复代码,降低了模块间的耦合度,同时提高了系统的可维护性。可用于权限认证、日志、事务处理。
这两种设计思想在目标上有着本质的差异。
AOP术语
首先从一些重要的AOP概念和术语开始,这些术语不是Spring特有的。
Spring AOP和AspectJ是什么关系
AspectJ是什么?
AspectJ是一个java实现的AOP框架,它能够对java代码进行AOP编译(一般在编译期进行),让java代码具有AspectJ的AOP功能(当然需要特殊的编译器)。
可以这样说AspectJ是目前实现AOP框架中最成熟,功能最丰富的语言,更幸运的是,AspectJ与java程序完全兼容,几乎是无缝关联,因此对于有java编程基础的工程师,上手和使用都非常容易。
了解AspectJ应用到java代码的过程(这个过程称为织入),对于织入这个概念,可以简单理解为aspect(切面)应用到目标函数(类)的过程。
对于这个过程,一般分为动态织入和静态织入:
- 动态织入
动态织入的方式是在运行时动态将要增强的代码织入到目标类中,这样往往是通过动态代理技术完成的。
如Java JDK的动态代理(Proxy,底层通过反射实现)或者CGLIB的动态代理(底层通过继承实现),Spring AOP采用的就是基于运行时增强的代理技术。 - 静态织入
ApectJ采用的就是静态织入的方式。ApectJ主要采用的是编译期织入,在这个期间使用AspectJ的acj编译器(类似javac)把aspect类编译成class字节码后,在java目标类编译时织入,即先编译aspect类再编译目标类。
为什么需要理清楚Spring AOP和AspectJ的关系?
我们看下@Aspect以及增强的几个注解,为什么不是Spring包,而是来源于aspectJ呢?
Spring AOP和AspectJ是什么关系?
AspectJ可以做Spring AOP干不了的事情,它是AOP编程的完全解决方案。
Spring AOP则致力于解决企业级开发中最普遍的AOP(方法织入)。
Spring AOP | AspectJ |
---|---|
在纯 Java 中实现 | 使用 Java 编程语言的扩展实现 |
不需要单独的编译过程 | 除非设置 LTW,否则需要 AspectJ 编译器 (ajc) |
只能使用运行时织入 | 运行时织入不可用。支持编译时、编译后和加载时织入 |
功能不强-仅支持方法级编织 | 更强大 - 可以编织字段、方法、构造函数、静态初始值设定项、最终类/方法等……。 |
只能在由 Spring 容器管理的 bean 上实现 | 可以在所有域对象上实现 |
仅支持方法执行切入点 | 支持所有切入点 |
代理是由目标对象创建的, 并且切面应用在这些代理上 | 在执行应用程序之前 (在运行时) 前, 各方面直接在代码中进行织入 |
比 AspectJ 慢多了 | 更好的性能 |
易于学习和应用 | 相对于 Spring AOP 来说更复杂 |
代理模式
AOP实现的关键在于代理模式,AOP代理主要分为静态代理和动态代理。
静态代理的代表为AspectJ;动态代理则以Spring AOP为代表。
静态代理 - AspectJ
AspectJ是静态代理的增强。
所谓静态代理,就是AOP框架会在编译阶段生成AOP代理类,因此也称为编译时增强,他会在编译阶段将AspectJ(切面)织入到Java字节码中,运行的时候就是增强之后的AOP对象。
动态代理 - Spring AOP
所谓的动态代理就是说AOP框架不会去修改字节码,而是每次运行时在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。
Spring AOP中的动态代理主要有两种方式:
- JDK动态代理
- CGLIB动态代理
Spring 5.x中AOP默认依旧使用JDK动态代理
SpringBoot在2.x默认使用Cglib动态代理 (1.x中默认使用JDK动态代理)
JDK动态代理
只提供接口的代理,不支持类的代理。
核心InvocationHandler接口和Proxy类,InvocationHandler 通过invoke()方法反射来调用目标类中的代码,动态地将横切逻辑和业务编织在一起;接着,Proxy利用 InvocationHandler动态创建一个符合某一接口的的实例, 生成目标类的代理对象。
1 | package com.feng.proxy.dynamic.jdk.demo2; |
CGLIB动态代理
如果目标类没有实现接口,那么Spring AOP会选择使用CGLIB
来动态代理目标类。
CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成指定类的一个子类对象,并覆盖其中特定方法并添加增强代码,从而实现AOP。
CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。
1 | package com.feng.proxy.dynamic.cglib.demo1; |
其他代理方式 - Java字节码操作框架
ASM
Javassist
Byte-Buddy
Spring动态代理
相关使用场景
AOP切面 @Aspect
为被切点切到的bean生成代理对象。@Configuration
在Spring中只要被@Configuration
注解修饰的类,Spring就会为其生成代理对象,至于这样做的主要原因就是为了解决生成对象的单例问题。
取决于@Configuration注解
的proxyBeanMethods属性。
如果是true的话,那么这个配置类就会被代理了,如果是false的话,那么就不会被代理。1
2
3
4
5
6
7
8
9
10
11
12// @Configuration注解 proxyBeanMethods属性
public Configuration {
String value() default "";
// proxyBeanMethods代表的是,如果是true的话,那么这个配置类就会被代理了,如果是false的话,那么就不会被代理。
boolean proxyBeanMethods() default true;
}@Lazy
@Async
@Transactional
Spring事务
Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的。
真正的数据库层的事务提交和回滚是通过binlog或者redo log/undo log实现的。
Spring的核心事务管理抽象是TransactionManager
,管理封装了一组独立于技术的方法,无论使用Spring的哪种事务管理策略(编程式或声明式),事务管理器都是必须的。
事务的种类
编程式事务
通过 TransactionTemplate或者TransactionManager手动管理事务,实际应用中很少使用,但是对于你理解 Spring 事务管理原理有帮助。
声明式事务
推荐使用(代码侵入性最小),实际是通过 AOP 实现(基于@Transactional 的全注解方式使用最多)。
事务的属性
- 隔离级别
- 传播行为
- 回滚规则
- 是否只读
- 事务超时
@Transactional 注解使用详解
作用范围
- 方法 :推荐将注解使用于方法上,不过需要注意的是:该注解只能应用到 public 方法上,否则不生效。
- 类 :如果这个注解使用在类上的话,表明该注解对该类中所有的 public 方法都生效。
- 接口 :不推荐在接口上使用。
常用配置参数
- propagation 事务的传播行为,默认值为 REQUIRED
- isolation 事务的隔离级别,默认值采用 DEFAULT
- timeout 事务的超时时间,默认值为-1(不会超时)。如果超过该时间限制但事务还没有完成,则自动回滚事务。
- readOnly 指定事务是否为只读事务,默认值为 false。
- rollbackFor 用于指定能够触发事务回滚的异常类型,并且可以指定多个异常类型。
@Transactional事务注解原理
@Transactional
的工作机制是基于AOP实现的,Spring AOP又是使用动态代理实现的。
如果目标对象实现了接口,默认情况下会采用JDK的动态代理,如果目标对象没有实现接口,会使用CGLIB动态代理。
createAopProxy()
决定了是使用 JDK 还是 CGLIB 来做动态代理。
1 | public class DefaultAopProxyFactory implements AopProxyFactory, Serializable { |
如果一个类或者一个类中的 public 方法上被标注 @Transactional 注解的话,Spring 容器就会在启动的时候为其创建一个代理类,在调用被@Transactional 注解的 public 方法的时候,实际调用的是,TransactionInterceptor 类中的 invoke()方法。
这个方法的作用就是在目标方法之前开启事务,方法执行过程中如果遇到异常的时候回滚事务,方法调用完成之后提交事务。
Spring AOP自调用问题
若同一类中的其他没有 @Transactional 注解的方法内部调用有 @Transactional 注解的方法,有 @Transactional 注解的方法的事务会失效。
这是由于 Spring AOP 代理的原因造成的,因为只有当 @Transactional 注解的方法在类以外被调用的时候,Spring事务管理才生效。
MyService 类中的method1()调用method2()就会导致method2()的事务失效。
1 |
|
解决办法就是避免同一类中自调用或者使用AspectJ静态代理取代Spring AOP代理。