一、AOP简介
AOP:是一种面向切面的编程范式,是一种编程思想,旨在通过分离横切关注点,提高模块化,可以跨越对象关注点。Aop的典型应用即spring的事务机制,日志记录。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。主要功能是:日志记录,性能统计,安全控制,事务处理,异常处理等等;主要的意图是:将日志记录,性能统计,安全控制,事务处理,异常处理等代码从业务逻辑代码中划分出来,通过对这些行为的分离,我们希望可以将它们独立到非指导业务逻辑的方法中,进而改变这些行为的时候不影响业务逻辑的代码。
1.1、AOP几个相关的概念:
名称 | 说明 |
切面(Aspect) | 一个关注点的模块化,这个关注点可能会横切多个对象 |
连接点(Joinpoint) | 程序执行过程中的某个特定的点。例如类初始化、方法执行、方法调用、字段调用或处理异常等等,Spring只支持方法执行连接点。 |
通知(Advice) | 在切面的某个特定的连接点上执行的动作。(通知定义了切面是什么以及何时使用。描述了切面要完成的工作和何时需要执行这个工作。) |
切入点(Pointcut) | 匹配连接点的断言,在AOP中通知和一个切入点的表达式。(例如某个类或方法的名称,Spring中允许我们方便的用正则表达式来指定) |
引入(Introduction) | 再不修改类代码的前提下,为类添加新的方法和属性。(也称为内部类型声明,为已有的类添加额外新的字段或方法,Spring允许引入新的接口(必须对应一个实现)到所有被代理对象(目标对象)) |
目标对象(Target Object) | 被一个或多个切面所通知的对象。(需要被织入横切关注点的对象,即该对象是切入点选择的对象,需要被通知的对象,从而也可称为“被通知对象”;由于Spring AOP 通过代理模式实现,从而这个对象永远是被代理对象) |
AOP代理(AOP Proxy) | AOP框架创建的对象,用来实现切面契约(aspect contract)(包括通知方法执行等功能) |
织入(Weaving) | 把切面连接到其他的应用程序类型或者对象上,并创建一个被通知的对象, 分为:编译时织入、类加载时织入、执行时织入。(将切面应用到目标对象从而创建出AOP代理对象的过程,织入可以在编译期、类装载期、运行期进行) |
把切面应用到目标对象来创建新的代理对象的过程,织入一般发生在如下几个时机:
(1)编译时:当一个类文件被编译时进行织入,这需要特殊的编译器才可以做的到,例如AspectJ的织入编译器
(2)类加载时:使用特殊的ClassLoader在目标类被加载到程序之前增强类的字节代码
(3)运行时:切面在运行的某个时刻被织入,SpringAOP就是以这种方式织入切面的,原理应该是使用了JDK的动态代理技术
1.2、Advice的类型
名称 | 说明 |
前置通知(Before advice) | 在某个连接点(join point)之前执行的通知,但不能阻止连接点前的执行(除非它抛出异常) |
返回后通知(After returning advice) | 在某个连接点(join point)正常完成后执行的通知 |
抛出异常后通知(After throwing advice) | 在方法抛出异常退出时执行的通知 |
后通知(After(finally) advice) | 当某个连接点退出的时候执行的通知(无论是正常返回还是异常退出) |
环绕通知(Around advice) | 包围一个连接点(join point)的通知 |
二、Spring的AOP实现
AOP实现方案:AspectJ和Spring AOP。
AspectJ:Aspectj是aop的java实现方案,AspectJ是一种编译期的用注解形式实现的AOP。
(1)AspectJ是一个代码生成工具(Code Generator),其中AspectJ语法就是用来定义代码生成规则的语法。基于自己的语法编译工具,编译的结果是JavaClass文件,运行的时候classpath需要包含AspectJ的一个jar文件(Runtime lib),支持编译时织入切面,即所谓的CTW机制,可以通过一个Ant或Maven任务来完成这个操作。
(2)AspectJ有自己的类装载器,支持在类装载时织入切面,即所谓的LTW机制。使用AspectJ LTW有两个主要步骤,第一,通过JVM的-javaagent参数设置LTW的织入器类包,以代理JVM默认的类加载器;第二,LTW织入器需要一个 aop.xml文件,在该文件中指定切面类和需要进行切面织入的目标类。
(3)AspectJ同样也支持运行时织入,运行时织入是基于动态代理的机制。(默认机制)
见《AspectJ入门》
Spring AOP:Spring AOP是AOP实现方案的一种,它支持在运行期基于动态代理的方式将aspect织入目标代码中来实现AOP。但是spring aop的切入点支持有限,而且对于static方法和final方法都无法支持aop(因为此类方法无法生成代理类);另外spring aop只支持对于ioc容器管理的bean,其他的普通java类无法支持aop。现在的spring整合了aspectj,在spring体系中可以使用aspectj语法来实现aop。
2.1、有接口无接口的Spring AOP 实现区别
- Spring AOP默认使用标准的javaSE动态代理作为AOP代理,这使得任何接口(或者接口集)都可以被代理
- Spring AOP中也可以使用CGLib代理(如果一个业务对象并没有实现一个接口)
2.2、Spring提供了4种实现AOP的方式:
1.经典的基于代理的AOP
2.@AspectJ注解驱动的切面 《spring AOP 之二:@Aspect注解的3种配置》
3.AOP标签的纯POJO切面
4.注入式AspectJ切面(编译期注入)见《AspectJ入门》
前三种都是Spring AOP实现的变体,Spring AOP构建在动态代理基础之上,因此,Spring对AOP的支持局限于方法拦截。
示例1
首先写一个接口叫Sleepable,这是一个牛X的接口,所有具有睡觉能力的东西都可以实现该接口(不光生物,包括关机选项里面的休眠)
package com.dxz.aop.demo1; public interface Sleepable { void sleep(); }
然后写一个Human类,他实现了这个接口
package com.dxz.aop.demo1; public class Human implements Sleepable { public void sleep() { System.out.println("睡觉了!梦中自有颜如玉!"); } }
好了,这是主角,不过睡觉前后要做些辅助工作的,最基本的是脱穿衣服,失眠的人还要吃安眠药什么的,但是这些动作与纯粹的睡觉这一“业务逻辑”是不相干的,如果把这些代码全部加入到sleep方法中,是不是有违单一职责呢?,这时候我们就需要AOP了。
编写一个SleepHelper类,它里面包含了睡觉的辅助工作,用AOP术语来说它就应该是通知了,我们需要实现上面的接口。
package com.dxz.aop.demo1; import java.lang.reflect.Method; import org.springframework.aop.AfterReturningAdvice; import org.springframework.aop.MethodBeforeAdvice; public class SleepHelper implements MethodBeforeAdvice,AfterReturningAdvice{ public void before(Method mtd, Object[] arg1, Object arg2) throws Throwable { System.out.println("通常情况下睡觉之前要脱衣服!"); } public void afterReturning(Object arg0, Method arg1, Object[] arg2, Object arg3) throws Throwable { System.out.println("起床后要先穿衣服!"); } }
然后在spring配置文件applicationContext-aop1.xml中进行配置:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <bean id="human" class="com.dxz.aop.demo1.Human"> </bean> <bean id="sleepHelper" class="com.dxz.aop.demo1.SleepHelper"> </bean> <bean id="sleepPointcut" class="org.springframework.aop.support.JdkRegexpMethodPointcut"> <property name="pattern" value=".*sleep" /> </bean> <bean id="sleepHelperAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor"> <property name="advice" ref="sleepHelper" /> <property name="pointcut" ref="sleepPointcut" /> </bean> <bean id="humanProxy" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="target" ref="human" /> <property name="interceptorNames" value="sleepHelperAdvisor" /> <property name="proxyInterfaces" value="com.dxz.aop.demo1.Sleepable" /> </bean> </beans>
测试类:
package com.dxz.aop.demo1; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; public class Test { public static void main(String[] args){ ApplicationContext appCtx = new ClassPathXmlApplicationContext("applicationContext-aop1.xml"); Sleepable sleeper = (Sleepable)appCtx.getBean("humanProxy"); sleeper.sleep(); } }
程序运行产生结果:
十月 23, 2017 5:12:05 下午 org.springframework.context.support.ClassPathXmlApplicationContext prepareRefresh 信息: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@5197848c: startup date [Mon Oct 23 17:12:05 CST 2017]; root of context hierarchy 十月 23, 2017 5:12:05 下午 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions 信息: Loading XML bean definitions from class path resource [applicationContext-aop1.xml] 通常情况下睡觉之前要脱衣服! 睡觉了!梦中自有颜如玉! 起床后要先穿衣服!
OK!这是我们想要的结果,但是上面这个过程貌似有点复杂,尤其是配置切点跟通知,Spring提供了一种自动代理的功能,能让切点跟通知自动进行匹配,修改配置文件如下:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <bean id="sleepHelper" class="com.dxz.aop.demo1.SleepHelper"> </bean> <bean id="sleepAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor"> <property name="advice" ref="sleepHelper" /> <property name="pattern" value=".*sleep" /> </bean> <bean id="human" class="com.dxz.aop.demo1.Human"> </bean> <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" /> </beans>
执行程序:
public static void main(String[] args){ ApplicationContext appCtx = new ClassPathXmlApplicationContext("applicationContext-aop11.xml"); Sleepable sleeper = (Sleepable)appCtx.getBean("human"); sleeper.sleep(); }
成功输出结果跟前面一样!
只要我们声明了org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator就能为方法匹配的bean自动创建代理!
但是这样还是要有很多工作要做,有更简单的方式吗?有!
一种方式是使用AspectJ提供的注解:
package com.dxz.aop.demo2; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; /** * @Aspect的注解来标识切面 */ @Aspect //@Component public class SleepHelper { public SleepHelper() { } @Pointcut("execution(* *.sleep())") public void sleeppoint() { } @Before("sleeppoint()") public void beforeSleep() { System.out.println("睡觉前要脱衣服!"); } @AfterReturning("sleeppoint()") public void afterSleep() { System.out.println("睡醒了要穿衣服!"); } }
用@Aspect的注解来标识切面,注意不要把它漏了,否则Spring创建代理的时候会找不到它,@Pointcut注解指定了切点,@Before和@AfterReturning指定了运行时的通知,注意的是要在注解中传入切点的名称。
然后我们在Spring配置文件上下点功夫,首先是增加AOP的XML命名空间和声明相关schema,见配置文件applicationContext-aop2.xml:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd"> <aop:aspectj-autoproxy/> <bean id="human" class="com.dxz.aop.demo2.Human"> </bean> <bean id="sleepHelper" class="com.dxz.aop.demo2.SleepHelper"> </bean> </beans>
记得加上这个标签:
<aop:aspectj-autoproxy/> 有了这个Spring就能够自动扫描被@Aspect标注的切面了。
最后是运行,很简单方便了:
public static void main(String[] args){ ApplicationContext appCtx = new ClassPathXmlApplicationContext("applicationContext-aop2.xml"); Sleepable human = (Sleepable)appCtx.getBean("human"); human.sleep(); }
下面我们来看最后一种常用的实现AOP的方式:使用Spring来定义纯粹的POJO切面
AOP标签
前面我们用到了<aop:aspectj-autoproxy/>标签,Spring在aop的命名空间里面还提供了其他的配置元素:
<aop:advisor> 定义一个AOP通知者
<aop:after> 后通知
<aop:after-returning> 返回后通知
<aop:after-throwing> 抛出后通知
<aop:around> 周围通知
<aop:aspect>定义一个切面
<aop:before>前通知
<aop:config>顶级配置元素,类似于<beans>这种东西
<aop:pointcut>定义一个切点
我们用AOP标签来实现:
package com.dxz.aop.demo3; public class SleepHelper { public void beforeSleep() throws Throwable { System.out.println("通常情况下睡觉之前要脱衣服!"); } public void afterSleep() throws Throwable { System.out.println("起床后要先穿衣服!"); } }
代码就不用继承啥了,只是修改配置文件,加入AOP配置即可:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd"> <!-- 配置AOP 《方式一》 --> <aop:config> <!-- 配置切面及通知 --> <aop:aspect ref="sleepHelper"> <aop:before method="beforeSleep" pointcut="execution(public * *..Sleepable.sleep(..))" /> <aop:after method="afterSleep" pointcut="execution(public * *..Sleepable.sleep(..))" /> </aop:aspect> </aop:config> <!-- 配置AOP 《方式二》--> <aop:config> <!-- 配置切点表达式 --> <aop:pointcut id="pointcut" expression="execution(public * *..Sleepable.sleep(..))" /> <!-- 配置切面及通知 --> <aop:aspect ref="sleepHelper"> <aop:before method="beforeSleep" pointcut-ref="pointcut" /> <aop:after method="afterSleep" pointcut-ref="pointcut" /> </aop:aspect> </aop:config> <bean id="human" class="com.dxz.aop.demo3.Human"> </bean> <bean id="sleepHelper" class="com.dxz.aop.demo3.SleepHelper"> </bean> </beans>
4 注入AspectJ切面
虽然Spring AOP能够满足许多应用的切面需求,但是与AspectJ相比,Spring AOP 是一个功能 比较弱的AOP解决方案。AspectJ提供了Spring AOP所不能支持的许多类型的切点。 例如,当我们需要在创建对象时应用通知,构造器切点就非常方便。不像某些其他面向对象语 言中的构造器,Java构造器不同于其他的正常方法。这使得Spring基于代理的AOP无法把通知 应用于对象的创建过程。 对于大部分功能来讲,AspectJ切面与Spring是相互独立的。虽然它们可以织入到任意的Java应 用中,这也包括了Spring应用,但是在应用AspectJ切面时几乎不会涉及到Spring。 但是精心设计且有意义的切面很可能依赖其他类来完成它们的工作。如果在执行通知时,切 面依赖于一个或多个类,我们可以在切面内部实例化这些协作的对象。但更好的方式是,我们 可以借助Spring的依赖注入把bean装配进AspectJ切面中。
示例见《AspectJ入门》中的示例
三、Spring AOP 原理剖析
通过前面介绍可以知道:AOP 代理其实是由 AOP 框架动态生成的一个对象,该对象可作为目标对象使用。AOP 代理包含了目标对象的全部方法,但 AOP 代理中的方法与目标对象的方法存在差异:AOP 方法在特定切入点添加了增强处理,并回调了目标对象的方法。
AOP 代理所包含的方法与目标对象的方法示意图如图 3 所示。
Spring 的 AOP 代理由 Spring 的 IoC 容器负责生成、管理,其依赖关系也由 IoC 容器负责管理。因此,AOP 代理可以直接使用容器中的其他 Bean 实例作为目标,这种关系可由 IoC 容器的依赖注入提供。
纵观 AOP 编程,其中需要程序员参与的只有 3 个部分:
- 定义普通业务组件。
- 定义切入点,一个切入点可能横切多个业务组件。
- 定义增强处理,增强处理就是在 AOP 框架为普通业务组件织入的处理动作。
上面 3 个部分的第一个部分是最平常不过的事情,无须额外说明。那么进行 AOP 编程的关键就是定义切入点和定义增强处理。一旦定义了合适的切入点和增强处理,AOP 框架将会自动生成 AOP 代理,而 AOP 代理的方法大致有如下公式:
代理对象的方法 = 增强处理 + 被代理对象的方法
在上面这个业务定义中,不难发现 Spring AOP 的实现原理其实很简单:AOP 框架负责动态地生成 AOP 代理类,这个代理类的方法则由 Advice 和回调目标对象的方法所组成。
对于前面提到的图 2 所示的软件调用结构:当方法 1、方法 2、方法 3 ……都需要去调用某个具有“横切”性质的方法时,传统的做法是程序员去手动修改方法 1、方法 2、方法 3 ……、通过代码来调用这个具有“横切”性质的方法,但这种做法的可扩展性不好,因为每次都要改代码。
于是 AOP 框架出现了,AOP 框架则可以“动态的”生成一个新的代理类,而这个代理类所包含的方法 1、方法 2、方法 3 ……也增加了调用这个具有“横切”性质的方法——但这种调用由 AOP 框架自动生成的代理类来负责,因此具有了极好的扩展性。程序员无需手动修改方法 1、方法 2、方法 3 的代码,程序员只要定义切入点即可—— AOP 框架所生成的 AOP 代理类中包含了新的方法 1、访法 2、方法 3,而 AOP 框架会根据切入点来决定是否要在方法 1、方法 2、方法 3 中回调具有“横切”性质的方法。
简而言之:AOP 原理的奥妙就在于动态地生成了代理类,这个代理类实现了图 2 的调用——这种调用无需程序员修改代码。接下来介绍的 CGLIB 就是一个代理生成库,下面介绍如何使用 CGLIB 来生成代理类。