Spring AOP详解
AOP介绍
AOP(Aspect Oriented Programming),即面向切面编程,可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系,这种散布在各处的无关的代码被称为横切(cross cutting),在OOP设计中,它导致了大量代码的重复,不利于模块的复用。AOP利用一种称为”横切”的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可复用模块,并将其命名为”Aspect”,即切面。所谓”切面”,简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。使用”切面”技术,AOP把软件系统分为两个部分:核心关注点 和 横切关注点 。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事物。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。
AOP(Aspect Orient Programming),我们一般称为面向方面(切面)编程,作为面向对象的一种补充,用于处理系统中分布于各个模块的横切关注点,比如事务管理、日志、缓存等等。AOP实现的关键在于AOP框架自动创建的AOP代理,AOP代理主要分为静态代理和动态代理,静态代理的代表为AspectJ;而动态代理则以Spring AOP为代表。
与AspectJ的静态代理不同,Spring AOP使用的动态代理,所谓的动态代理就是说AOP框架不会去修改字节码,而是在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。Spring AOP中的动态代理主要有两种方式,JDK动态代理和CGLIB动态代理。JDK动态代理通过反射来接收被代理的类,并且 要求被代理的类必须实现一个接口 。JDK动态代理的核心是InvocationHandler接口和Proxy类。__如果目标类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类__。CGLIB(Code Generation Library),是一个代码生成的类库,是利用asm开源包,可以在运行时动态的生成某个类的子类。注意, __CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的__。
这里有注意的几点如下:
- 从Spring 3.2以后不再将CGLIB放在项目的classpath下,而是将CGLIB类打包放在spring-core下面的org.springframework中。这个就意味着基于CGLIB的动态代理与JDK的动态代理在支持“just works”就一样了。
- 在Spring 4.0中,因为CGLIB代理实例是通过Objenesis创建的,所以代理对象的构造器不再有两次调用。
- 在 Spring Boot 2.0 中,Spring Boot现在默认使用CGLIB动态代理(基于类的动态代理), 包括AOP. 如果需要基于接口的动态代理(JDK基于接口的动态代理) , 需要设置spring.aop.proxy-target-class属性为false。
AOP核心概念
- 横切关注点
对哪些方法进行拦截,拦截后怎么处理,这些关注点称之为横切关注点 - 切面(aspect)
类是对物体特征的抽象,切面就是对横切关注点的抽象 - 连接点(joinpoint)
被拦截到的点,因为Spring只支持方法类型的连接点,所以在Spring中连接点指的就是被拦截到的方法 - 切入点(pointcut)
对连接点进行拦截的定义 - 通知(advice)
所谓通知指的就是指拦截到连接点之后要执行的代码,通知分为前置、后置、异常、最终、环绕通知五类 - 目标对象
代理的目标对象 - 织入(weave)
将切面应用到目标对象并导致代理对象创建的过程 - 引入(introduction)
在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段
Spring对AOP的支持
Spring中AOP代理由Spring的IOC容器负责生成、管理,其依赖关系也由IOC容器负责管理。因此,AOP代理可以直接使用容器中的其它bean实例作为目标,这种关系可由IOC容器的依赖注入提供。Spring创建代理的规则为:
- 默认使用Java动态代理来创建AOP代理,这样就可以为任何接口实例创建代理了
- 当需要代理的类没有实现任何接口的时候,Spring会切换为使用CGLIB代理,也可强制使用CGLIB
Spring除了支持Schema方式配置AOP,还支持注解方式:使用@AspectJ风格的切面声明。@AspectJ 作为通过 Java 5 注释注释的普通的 Java 类,它指的是声明 aspects 的一种风格。AspectJ是静态代理的增强,所谓的静态代理就是AOP框架会在编译阶段生成AOP代理类,因此也称为编译时增强。
AspectJ: 基于字节码操作(Bytecode Manipulation),通过编织阶段(Weaving Phase),对目标Java类型的字节码进行操作,将需要的Advice逻辑给编织进去,形成新的字节码。毕竟JVM执行的都是Java源代码编译后得到的字节码,所以AspectJ相当于在这个过程中做了一点手脚,让Advice能够参与进来。
而编织阶段可以有两个选择,分别是加载时编织(也可以成为运行时编织)和编译时编织
- 加载时编织(Load-Time Weaving):顾名思义,这种编织方式是在JVM加载类的时候完成的。
- 编译时编织(Compile-Time Weaving):需要使用AspectJ的编译器来替换JDK的编译器。
添加spirng aop支持和AspectJ依赖
1 | <dependency> |
或(springboot)
1 | <dependency> |
使用场景是比如记录核心服务方法的执行耗时。
启用@AspectJ的支持
Spring默认不支持@AspectJ风格的切面声明,为了支持需要使用如下配置:
1 | <!-- 自动扫描使用了aspectj注解的类 --> |
或
1 |
|
声明切面
1 |
|
声明切入点
@AspectJ风格的命名切入点使用org.aspectj.lang.annotation包下的@Pointcut+方法(方法必须是返回void类型)实现。
1 |
|
- value:指定切入点表达式;
- argNames:指定命名切入点方法参数列表参数名字,可以有多个用“,”分隔,这些参数将传递给通知方法同名的参数,同时比如切入点表达式“args(param)”将匹配参数类型为命名切入点方法同名参数指定的参数类型。
- pointcutName:切入点名字,可以使用该名字进行引用该切入点表达式。
切入点表达式
execution表达式
1 | execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) |
上面的就是execution表达式的格式,execution匹配的就是连接点(Joinpoint,一般是方法),看上面的表达式execution是固定的,方法的修饰符是可选的,返回类型是必须的,定义的全限类型也是可选的,名称是必须的,参数是必须的,这些都可以使用通配符。
任何的public方法
1 | execution(public * *(..)) |
以set开始的方法
1 | execution(* set*(..)) |
定义在cn.freemethod.business.pack.Say接口中的方法
1 | execution(* cn.freemethod.business.pack.Say.*(..)) |
任何cn.freemethod.business包中的方法
1 | execution(* cn.freemethod.business.*.*(..)) |
任何定义在com.xyz.service包或者其子包中的方法
1 | execution(* cn.freemethod.business..*.*(..)) |
其他表达式
任何在com.xyz.service包中的方法
1 | within(com.xyz.service.*) |
任何定义在com.xyz.service包或者其子包中的方法
1 | within(com.xyz.service..*) |
任何实现了com.xyz.service.AccountService接口中的方法
1 | this(com.xyz.service.AccountService) |
任何目标对象实现了com.xyz.service.AccountService的方法
1 | target(com.xyz.service.AccountService) |
一般情况下代理类(Proxy)和目标类(Target)都实现了相同的接口,所以上面的2个基本是等效的。
有且只有一个Serializable参数的方法
args(java.io.Serializable)
只要这个参数实现了java.io.Serializable接口就可以,不管是java.io.Serializable还是Integer,还是String都可以。
目标(target)使用了@Transactional注解的方法
1 | @target(org.springframework.transaction.annotation.Transactional) |
目标类(target)如果有Transactional注解中的所有方法
1 | @within(org.springframework.transaction.annotation.Transactional) |
任何方法有Transactional注解的方法
1 | @annotation(org.springframework.transaction.annotation.Transactional) |
有且仅有一个参数并且参数上类型上有Transactional注解
1 | @args(org.springframework.transaction.annotation.Transactional) |
注意是参数类型上有Transactional注解,而不是方法的参数上有注解。
bean的名字为tradeService中的方法
1 | bean(simpleSay) |
bean名字为simpleSay中的所有方法。
bean名字能匹配
1 | bean(*Impl) |
bean名字匹配*Impl的bean中的所有方法。
实例
计算核心服务的耗时
定义注解
1 |
|
定义切面
1 |
|
使用注解
1 |
|
如果要改变参数需要使用Around,Before只能读取而不能改变参数内容。
根据参数注解改变参数内容,设置默认值
1 |
|
参考
Spring AOP 之一:基本概念与流程
Spring AOP 之二:Pointcut注解表达式
Spring AOP 介绍