Spring基础(精通Spring+4.x++企业应用开发实战 第7章)
2017-09-17 16:21
721 查看
AOP概述
AOP是Aspect Oriented Programing,面向切面编程。
7-1中,斜体的代码是方法性能监视代码,它们在方法调用前启动,在方法调用返回前结束,并在内部记录性能监视的结果信息,而黑色粗体的代码是事务开始和事务提交的代码。
在①②处的业务代码淹没在重复化非业务性的代码之中,性能监视和事务管理这些非业务性代码包围着业务性代码
假设将ForumService业务类看成一段圆木
我们无法通过抽象父类的方式消除如上的重复性横切代码,因为这些逻辑代码依附在业务类的流程中,它们不能转移到其他地方去。
AOP通过横向抽取机制为这类无法通过纵向继承体系进行抽象的重复性代码提供了解决方案。
AOP将分散在业务逻辑代码中的相同代码通过横向切割的方式抽取到一个独立的模块中,并能将这些独立的逻辑融合到业务逻辑中以完成和原来一样的业务逻辑
特定点是程序执行的某个特定位置,如类开始初始化前,类初始化后,类的某个方法调用前/后,方法抛出异常后。一个类或一段代码拥有一些具有边界性质的特定点,这些代码中的特定点就被称为“连接点”。
Spring仅支持方法的连接点,即仅能在方法调用前,方法调用后,方法抛出异常时及方法调用前后这些程序执行点织入增强。
连接点由两个信息确定:一是用方法表示的程序执行点,二是用相对位置表示的方位。如在Test.foo()方法执行前的连接点,执行点为Test.foo(),方法为该方法执行前的位置。Spring使用切点对执行点进行定位,而方位则在增强类型中增强
切点(Pointcut)
每个程序类都有多个连接点,如一个拥有两个方法的类,这两个方法就是连接点,即连接点是程序类中客观存在的事物。AOP通过“切点”定位特定的连接点。连接点相当于数据库中的记录,而切点相当于查询条件,一个切点可以匹配多个连接点。
在Spring中,切点通过org.springframework.aop.Pointcut接口进行描述,它使用类和方法作为连接点的查询条件。Spring AOP的规则解析引擎负责解析切点所设定的查询条件,找到对应的连接点。确切地说,应该是执行点而非连接点,因为连接点是方法执行前/后等包括方位信息的具体程序执行点,而切点只定位到某个方法上,所以如果希望定位到具体的连接点上,需要提供方位信息
增强(Advice)
增强是织入目标类连接点上的一段程序代码。在Spring中,增强除用于描述一段程序代码外,还拥有另一个和连接点相关的信息,就是执行点的方位。结合执行点的方位信息和切点信息,就可以找到特定的连接。
正因为增强即包含由于添加到目标连接点上的一段执行逻辑,又包含用于定位连接点的方位信息,所以Spring所提供的增强接口都是带方位名的,如BeforeAdivce,AfterReturningAdvice和ThrowsAdvice等。BeforeAdvice表示方法调用前的位置,而AfterReturningAdvice表示访问后的位置。
所以只有结合切点和增强,才能确定特定的连接点并实施逻辑增强。
目标对象(Target)
增强逻辑的织入目标类。如果没有AOP,那么目标业务类需要自己实现所有的逻辑,如7-1.
在AOP的帮助下,ForumService只实现那些非横切逻辑的程序逻辑,而性能监视和事务管理等这些横切逻辑就可以使用AOP动态织入特定的连接点上
引介(Introduction)
引介是一种特殊的增强,为类添加一些属性和方法。这样,即使一个业务类原本没有实现某个接口,通过AOP的引介功能,也可以动态地为该业务类添加接口的实现逻辑,使业务类成为这个接口的实现类
织入(Weaving)
织入是将增强添加到目标类的具体连接点上的过程.AOP就像一台织布机,将目标类,增强或引介编织到一切。
AOP有三种织入方式:
①编译期织入,这要求使用特殊的Java编译器
②类装载期织入,这要求使用特殊的类装载器
③动态代理织入,在运行期为目标类添加增强生成子类的方式
Spring采用动态代理织入
代理(Proxy)
一个类被AOP织入增强后,就产生一个结果类,它是融合了原类和增强逻辑的代理类。根据不同的代理方式,代理类既可能是和原类具有相同接口的类,也可能就是原类的子类,所以可以采用与调用原类相同的方式调用代理类。
切面(Aspect)
切面由切点和增强(引介)组成,既包括横切逻辑的定义,也包括连接点的定义。Spring AOP就是负责实施切面的框架,它将切面所定义的横切逻辑织入切面所指定的连接点中
之所以需要两种代理机制,是因为JDK本身只提供接口的代理,而不支持类的代理
7-2中,注释的代码就是具有横切逻辑的代码,每个Service类和每个业务方法体的前后都执行相同的代码逻辑:方法调用前启动启动PerformanceMonitor;方法调用后通知PerformanceMonitor结束性能监视并记录性能监视结果
PerformanceMonitor是性能监视的实现类,下面给出一个简单的实现版本
ThreadLocal将非线程安全类改造为线程安全类。PerformanceMonitor 提供两个方法,通过begin(String method)方法开始对某个目标类方法的监视,其中method为目标类方法的全限定名(如com.smart.proxy.ForumServiceImpl.removeTopic),通过end()方法结束对目标类方法的监视,并给出性能监视信息,必须配套使用。
用于记录性能监视信息的MethodPerformace类
将上面ForumServiceImpl的注释去掉,
通过下面代码测试拥有性能监视能力的ForumServiceImpl业务方法:
如7-2中,当某个方法需要进行性能监视时,必须调整方法代码,在方法体前后分别添加开始性能监视和结束性能监视的代码。这些非业务逻辑的代码破坏了业务逻辑的纯粹性。
通过代理的方式将业务类方法中开启和结束性能监视的横切代码从业务类中完成移除,并通过JDK或CGLib动态代理技术将横切代码织入目标方法的相应位置
其中,InvocationHandler是一个接口,可以通过实现该接口定义横切逻辑,并通过反射机制调用目标类的代码,动态地将横切逻辑和业务逻辑编辑在一起。
而Proxy利用InvocationHandler动态创建一个符合某一接口的实例,生成目标类的代理对象。
在java中,invocation的意思是调用。
可以看到7-2中,性能监视的代码被注释了。从业务类中移除监视横切代码,必须为它找到安置处——InvocationHandler。
将性能监视横切代码安置在PerformanceHandler中
③处的invoke()方法中③-1所示部分的代码为性能监视的横切代码,横切代码只出现一次。③-2中的method.invoke()通过Java反射机制间接调用目标对象的方法,这样invocationHandler的invoke()方法就将横切逻辑代码③-1和业务类方法(method)的业务逻辑代码③-2编织在一起,所以,可以将InvocationHandler看成一个编织器。
首先实现InvocationHandler接口,该接口定义了一个invoke(Object proxy,Method method,Object[] args),其中proxy是最终生成的代理实例,一般不会用到;method是被代理目标实例的某个具体方法,通过它可以发起目标实例方法的反射调用;args是被代理实例某个方法的入参,在方法反射调用时使用。
在构造函数中通过target传入希望被代理的目标对象;在InvocationHandler接口方法的invoke(Object proxy,Method method,Object[] args)里,将目标实例传递给method.invoke()方法,并调用目标实例的方法。
下面通过Proxy结合PerformanceHandler创建ForumService接口的代理实例
上面的代码往完成了业务类代码和横切代码的编织工作并生成了代理实例,在②让PerformanceHandler将性能监视横切逻辑编织到ForumService实例中,然后在③处,通过Proxy的newProxyInstance()静态方法为编织了业务类逻辑和性能监视逻辑的handler创建一个符合ForumService接口的代理实例。
该方法的第一个入参为类加载器;第二个入参为创建代理实例所需要实现的一组接口,第三个入参是整合了业务逻辑和横切逻辑的编织器对象
按照③处的设置方式,这个实例代理实现了业务目标类的所有接口,即ForumServiceImpl的ForumService接口,这样就可以按照调用ForumService接口实例相同的方式调用代理实例,如④,运行代码的输出结果和上面一样。
程序运行效果和直接在业务类中编写性能监视逻辑的效果一致,但原来分散的横切逻辑代码已经被抽取到PerformanceHandler中。当其他业务类的业务方法也需要使用性能监视时,只要用相同方式分别为它们创建代理对象即可。
通过时序图描述创建代理对象进行业务方法调用的整体逻辑,进一步认识代理对象的本质
ForumService代理实例内部利用PerformanceHandler横切逻辑和业务逻辑。调用者调用代理对象的removeForum()和removeTopic()方法时,如上图。
对于没有通过接口定义业务方法的类,CGLib可以动态创建代理实例。
CGLib采用底层的字节码技术,可以为类创建一个子类,在子类中采用方法拦截的技术拦截所有父类方法的调用并顺势织入横切逻辑。
下面用GGLib技术编写一个可以为任何类创建织入性能监视横切逻辑代理对象的代理创建器
在上面代码中,用户可以通过getProxy(Class clazz)方法为一个类创建动态代理对象,该代理对象通过扩展clazz实现代理。在这个代理对象中,织入性能监视的横切逻辑(③-1)。
Intercept(Object obj,Method method,Object[] args,MethodProxy proxy)是CGLib定义的Interceptor接口方法,它拦截所有目标类方法的调用。其中,obj表示目标类的实例;method为目标类方法的反射对象;args为方法的动态入参;proxy为代理类实例。
发现代理类的名字变成
这个就是CGLib为ForumServiceImpl动态创建的子类。
由于CGLib采用动态创建子类的方式生成代理对象,所以不能对目标类中的final或private方法进行代理。
在使用CGLib动态代理时,必须引入CGLib类库
对于singleton的代理对象或者具有实例池的代理,因为无需频繁地创建代理对象,所以比较适合采用CGLib动态代理技术;反之适合采用JDK动态代理技术
带<<spring>>标识的接口是在Spring所定义的扩展增强接口,带<<aopalliance>>标识的接口是AOP联盟定义的接口。
按照增强在目标类方法中的连接点位置,可以分为5类:
①前置增强:org.springframework.aop.BeforeAdvice代表前置增强,因为Spring只支持方法级的增强,所以MethodBeforeAdvice是目前可用的前置增强,表示在目标方法执行前实施增强,而BeforeAdvice是为了将来版本扩展需要而定义的。
②后置增强:org.springframework.aop.AfterReturningAdvice代表后置增强,表示在目标方法执行后实施增强。
③环绕增强:org.aopalliance.intercept.MethodInterceptor代表环绕增强,表示在目标方法执行前后实施增强。
④异常抛出增强:org.springframework.aop.ThrowsAdvice代表抛出异常增强,表示在目标方法抛出异常后实施增强。
⑤引介增强:org.springframework.aop.IntroductionInterceptor代表引介增强,表示在目标类中添加一些新的方法和属性
我们对NaiveWaiter的服务行为进行规范,让其在打招呼和提供服务之前,必须使用礼貌用于
MethodBeforeAdvice接口仅定义了唯一的方法:before(Method method,Object[] args,Object obj)throws Throwable,其中method为目标类的方法;objects为目标类方法的入参,而obj为目标类实例。当方法发生异常时,将阻止目标类方法的执行。
代码7-13
输出
解析ProxyFactory
在BeforeAdviceTest中,使用org.springframework.aop.framework.ProxyFactory代理工厂将GreetingBeforeAdvice的增强织入目标类NaiveWaiter中。ProxyFactory内部就是使用JDK或CGLib动态代理技术将增强应用到目标类中的
Spring定义了org.springframework.aop.framework.AopProxy接口,并提供额两个final类型的实现类
Cglib2AopProxy使用CGLib动态代理技术创建代理,而JdkDynamicAopProxy使用JDK动态代理技术创建代理。
如果通过ProxyFactory的setInterfaces(Class[] interfaces)方法指定目标接口进行代理,则ProxyFactory使用JdkDynamicAopProxy;
如果是针对类的代理,则使用Cglib2AopProxy。
还可以通过ProxyFactory的setOptimize(true)方法让ProxyFactory启动优化代理方式,这样,针对接口的代理也会使用Cglib2AopProxy。
7-13中BeforeAdviceTest使用的是CGLib动态代理技术,当我们指定接口进行代理时,将使用JDK动态代理技术
如果指定启动代理优化,则ProxyFactory还将使用Cglib2AopProxy代理
ProxyFactory通过addAdvice(Advice)方法添加一个增强,可以使用该方法添加多个增强。多个增强形成一个增强链,它们的调用顺序和添加顺序一致,可以通过addAdvice(int ,Advice)方法将增强添加到增强链的具体位置(初始位置为0)
在Spring中的配置
可以通过Spring 的配置声明一个代理
ProxyFactoryBean是FactoryBean接口的实现类,FactoryBean负责实例化一个Bean。ProxyFactoryBean负责为其他Bean创建代理实例,它在内部使用ProxyFactory来完成这项工作。
ProxyFactoryBean的几个常用的可配置属性
①target:代理的目标对象
②proxyInterfaces:代理所要实现的接口,可以是多个接口
③interceptorNames:需要织入目标对象的Bean列表,采用Bean的名称指定。这些Bean必须是实现了org.aopalliance.intercept.MethodInterceptor或org.springframework.aop.Advisor的Bean,配置中的顺序对应调用的顺序。
④singleton:返回的代理是否单实例,默认为单实例。
⑤optimize:当设置为true时,强制使用CGLib动态代理。对于singleton的代理,推荐使用CGLib;对于其他作用域类型的代理,最好使用JDK动态代理
⑥proxyTargetClass:是否对类进行代理(而不是对接口进行代理)。当设置为true时,使用CGLib动态代理
测试增强
7-15
输出以下信息
ProxyFactoryBean使用了JDK动态代理技术。可以调整配置,使用CGLib动态代理技术通过动态创建NaiveWaiter的子类来代理NaiveWaiter对象,如下:
将proxyTargetClass设置为true后,无须再设置proxyInterfaces属性,即使设置也会被忽略。
假如希望服务生在每次服务后也使用规范的礼貌用语,可以通过一个后置增强来实施这一要求。
通过实现AfterReturningAdvice来定义后置增强的逻辑,AfterReturningAdvice接口也只定义了一个方法 afterReturning(Object returnObj, Method method, Object[] args, Object obj) throws Throwable ,其中returnObj为目标实例返回的结果;method为目标类的方法,args为目标方法的入参,而Obj为目标类实例。假设在后置增强中抛出异常,如果该异常是目标方法声明的异常,则该异常归并到目标方法中;如果不是目标方法所声明的异常,则Spring将其转为运行期异常抛出
interceptorNames是String[]类型的
运行7-15中的代码,输出
Spring用AOP联盟(aopalliance)所定义的MethodInterceptor(org.aopalliance.intercept.MethodInterceptor;)作为环绕增强的接口。该接口拥有唯一的接口方法Object invoke(MethodInvocation invocation)throws Throwable.
MethodInvocation不但封装了目标方法及其入参数组,还封装了目标方法所在的实例对象,
通过MethodInvocation的getArguments()方法可以获取目标方法的入参数组
通过proceed()方法反射调用目标实例相应的方法
通过在实现类中定义横切逻辑,可以很容易实现方法前后的增强。
配置
运行7-15中代码,输出信息同上
在模拟业务类ForumService中定义了两个业务方法,removeForum()抛出运行期异常,而updateForum()抛出SQLException。
下面通过TransactionManager这个异常抛出增强对业务方法进行增强处理,统一捕捉抛出的异常并回滚事务
ThrowsAdvice异常抛出增强接口没有定义任何方法,它是一个标签接口,在运行期Spring使用反射机制自行跑断,必须采用以下签名形式定义异常抛出的增强方法:
void afterThrowing(Method method,Object[] args,Object target,Throwable);
方法名必须是afterThrowing,前三个入参Method method,Object[] args,Object target是可选的(要么都提供,要么都不提供),最后一个入参是Throwable或其子类。
可以在同一个异常抛出中定义多个afterThrowing()方法,当目标类方法抛出异常时,Spring会自动选用最匹配的增强方法。
标签接口是没有任何方法和属性的接口,不对实现类有任何语义上的要求,仅表明它的实现属于一个特定的类型,Java用它标识某一类对象。有两个用途:
①通过标签接口标识同一类型的类,这些类本身可能并不具有相同的方法,如Advice接口
②通过标签接口使程序或JVM采取一些特殊处理,如java.io.Serializable,它告诉JVM对象可以被序列化
对这个异常抛出增强进行配置,因为ForumService是类,使用CGLib代理
采用下面的代码
输出
可见,ForumService的两个方法所抛出的异常都被TransactionManager这个异常抛出增强捕获并成功处理了,将ForumService从事务管理繁复的代码中解放出来。
Spring定义了引介增强接口IntroductionInterceptor,该接口没有定义任何方法,Spring为其提供了DelagatingIntroductionInterceptor实现类。一般情况下,通过扩展该实现类定义自己的引介增强类。
前面性能监视的例子,我们对所有的业务类都织入了性能监视的增强。由于性能监视会影响业务系统的性能,所以是否启动性能监视应该是可控的。现在通过引介增强来实现
首先定义一个用于标识目标类是否支持性能监视的接口
通过setMonitorActive(boolean active)接口方法控制业务类性能监视功能的激活和关闭状态
下面通过扩展DelegatingIntroductionInterceptor为目标类引入性能监视的可控功能
7-22
扩展DelegatingIntroductionInterceptor的同时还实现了Monitorable接口,提供接口方法的实现。
在①定义了一个ThreadLocal类型的变量,用于保存性能监视开关状态,为了解决单实例singleton线程安全的问题,通过ThreadLocal让每个线程单独使用一个状态。
在③中覆盖了父类中的invoke()方法,该方法用于拦截目标类方法的调用,根据开关状态有条件地对目标实例方法进行性能监视。
在④处,MonitorStatusMap.get()方法返回的Boolean被自动拆包为boolean类型的值
通过Spring配置,将这个引介增强织入业务类ForumService中
首先,需要指定引介增强所实现的接口,如①。其次,由于只能通过为目标创建子类的方法生成引介增强的代理,所以必须将proxyTargetClass设置为true
如果没对ControllablePerformanceMonitor进行线程安全的特殊处理,就必须将singleton属性设置为true,让ProxyFactoryBean产生prototype作用域类型的代码。当CGLib动态创建代理的性能很低,而每次通过getBean()方法中从容器中获取作用域类型为prototype的Bean时都将返回一个新的代理实例,这种性能影响是巨大的,这就是为什么通过ThreadLocal对ControllablePerformanceMonitor的开关状态进行线程安全化处理的原因。
通过线程安全化处理后,就可以使用默认的singleton Bean作用域,这样创建代理的动作仅发生一次。
进行测试
②处,强制性地将forumService转换为Monitorable类型,代码的成功执行表示从Spring容器中返回的代理确实引入了Monitorable接口方法的实现。
输出
Spring通过org.springframework.aop.Pointcut接口描述切点,Pointcut由ClassFilter和MethodMatcher构成,通过ClassFilter定位到特定类上,通过MethodMatcher定位到某些特定方法上,这样Pointcut就拥有了描述某些类的某些特定方法的能力。
ClassFilter只定义了方法matches(Class clazz),参数代表一个被检测类,该方法判断被检测的类是否匹配过滤条件
Spring支持两种方法匹配器(MethodMatcher):静态方法匹配器和动态方法匹配器。
静态方法匹配器仅对方法名签名(包括方法名和入参类型及顺序)进行匹配,而动态方法匹配器会在运行期检查方法入参的值。静态匹配仅判别一次,而动态匹配因为每次调用方法的入参可能不一样,所以每次调用方法都必须判断。因此,动态匹配对性能的影响很大。一般,动态匹配不常使用。
方法匹配器的类型由isRuntime()方法的返回值决定,返回false表示的是静态方法匹配器,返回true表示是动态方法匹配器
但切点仅代表目标类连接点的部分信息(类和方法的定位),所以仅有切点无法制作出一个切面,必须结合增强才能制作出切面
Spring使用org.springframework.aop.Advisor接口表示切面的概念,一个切面同时包含横切代码和连接点信息,可以分为3类:一般切面,切点切面和引介切面,可以通过Spring所定义的切面清除地了解切面的分类
①Advisor:代表一般切面,仅包含一个Advice。因为Advice包含了横切代码和连接点信息,所以Advice(增强)就是一个简单的切面,只不过它代表的横切的连接点是目标类的所有方法,因为这个横切面太宽泛,一般不会直接使用
②PointcutAdvisor:代表具有切点的切面,包含Advice和Pointc两个类,可以通过类,方法名及方法方位等信息灵活地定义切面的连接点,提供更具适用性的切面。
③IntroductionAdvisor:代表引介切面,它对应引介增强的特殊的切面,应用于类层面上,所以引介切点用ClassFilter进行定义
PointcutAdvisor的主要实现类体系:
6个具体的实现类
①DefaultPointcutAdvisor:最常用 切面类型,可以通过任意Pointcut和Advice定义一个切面,唯一不支持的是引介的切面类型,一般可以通过扩展该类实现自定义的切面
②NameMatchMethodPointcutAdvisor:通过该类可以定义按方法名定义切点的切面
③RegexpMethodPointcutAdvisor:对于按正则表达式匹配方法名进行切点定义的切面,可以通过扩展实现类进行操作。RegexpMethodPointcutAdvisor允许用户以正则表达式模式串定义方法匹配的切点,其内部通过JdkRegexpMethodPointcut构造出正则表达式方法名切点。
④StaticMethodMatcherPointcutAdvisor:静态方法匹配器切点定义的切面,默认情况下匹配所有的目标类
⑤AspectJExpressionPointcAdvisor:用于AspectJ切点表达式定义切点的切面
⑥AspectJPointcutAdvisor:用于AspectJ语法定义切点的切面。
希望通过StaticMethodMatcherPointcutAdvisor定义一个切面,在Waiter#greetTo()方法调用前织入一个增强,即连接点为Waiter#greetTo()方法调用前的位置。
StaticMethodMatcherPointcutAdvisor抽象类唯一需要定义的是matches()方法,在默认情况下,该切面匹配所有的类,这里通过覆盖getClassFilter()方法,让它仅匹配Waiter类及其子类
Advisor还需要一个增强类的配置,为Advisor定义一个前置增强
可以直接通过ProxyFactory,通过手工编码的方式织入切面生成代理类,参考7-13在③使用addAdvisor()方法即可
代码清单 7-29 通过Spring配置来定义切面
在①处,将greetAdvice增强配置到greetingAdvisor切面中。StaticMethodMatcherPointcutAdvisor除具有advice属性外,还可以定义另外两个属性
①classFilter:类匹配过滤器,在GreetingAdvisor中用编码的方式设定了classFilter
②order:切面织入时的顺序,该属性用于定义Ordered接口表示的顺序
由于需要分别为waiter和seller两个Bean定义代理器,而且两者有很多公共的配置信息,所以使用一个父<bean>简化配置,在③和④,通过引用父<bean>定义了两个织入切面的代理
输出
可见切面只织入Waiter.greetTo()方法调用前的连接点上,Waiter.serveTo()和Seller.greetTo()方法没有织入切面。
RegexpMethodPointcutAdvisor是正则表达式方法匹配切面实现类
实例
7-30
在②处定义了一个匹配模式串”.greet.“,该模式串匹配Waiter.greetTo()方法。匹配模式串匹配的目标类方法的全限定名,即带类名的方法名
运行
输出
可见Waiter.greetTo()方法被织入了切面,而Waiter.serveTo()方法没有被织入切面,除了例子中使用的patterns和advice-ref属性外,还有两个属性
①pattern:如果只有一个匹配模式串,则可以使用该属性进行配置。patterns属性用于定义多个匹配模式串,这些匹配模式串之间是“或”的关系
②order:切面在织入时对应的顺序
正则表达式的语法
DynamicMethodMatcherPointcut是一个抽象类,将isRuntime()标识为final且返回true,这样子类就一定是一个动态切点。该抽象类默认匹配所有的类和方法,因此需要通过扩展该类编写复合要求的动态切点
上面的类既有用于静态切点检查的方法,又有用于动态切点检查的方法。
由于动态切点检查对性能会造成很大的影响,所以应当尽量避免在运行时每次都对目标类的每个方法进行动态检查。
Spring采用这样的机制:在创建代理时对目标类的每个连接点使用静态切点检查,如果仅通过静态切点检查就可以知道连接点不匹配,则在运行时不再进行动态检查;如果静态切点检查是匹配的,则在运行时才进行动态切点检查
在动态切点类中定义静态切点检查的方法可以避免不必要的动态检查操作,从而极大地提高运行效率。
在③中通过matches(Method method, Class
使用DefaultPointcutAdvisor定义切面,使用内部Bean方式注入动态切点GreetingDynamicPointcut,用内部Bean方式注入增强GreetingBeforeAdvice。此外DefaultPointcutAdvisor还有一个order属性,用于定义切面的织入顺序。
运行测试代码
输出
可以发现,Spring在创建代理织入切面时,对目标类中的所有方法都进行静态切点检查;在生成织入切面的代理对象后,第一次调用代理类的每一个方法进行一次静态切点检查,如果本次检查就能从候选者队列中将该方法排除,则以后对该方法的调用就不再执行静态切点检查;对于那些在静态切点匹配时匹配的方法,在后续调用该方法时,将执行动态切点检查。
在例子中,切点匹配的规则是:目标类为com.smart.advisor.Waiter或其子类;方法名为greetTo;动态入参clientNaem必须是特殊名单中的客户
基于这条规则,serveTo()及从Object中继承而来的toString(),hashCode()和clone()等方法通过静态切点检查就可以排除在候选者之外,只有greetTo()方法是动态切点检查的候选者,每次调用都会进行动态切点检查
可以发现,每次调用代理对象的任何一个方法,都会执行动态切点检查,这将导致很大的性能问题,所以在定义动态切点时,不要忘记同时覆盖getClassFilter(0和matches(Method method,Class clazz)方法,通过静态切点检查排除大部分方法
假设通过一个WaiterDelegate类代理Waiter所有的方法
如果希望所有由WaiterDelegate#service()方法发起调用的其他方法都织入GreetingBeforeAdvice增强,就必须使用流程切面来完成目标。
使用DefaultPointcutAdvisor配置一个流程切面来完成这一需求
代码7-35
ControlFlowPointcut有两个构造函数,分别是ControlFlowPointcut(Class clazz)和ControlFlowPointcut(Class clazz,String methodName)。第一个构造函数指定一个类作为流程切点;而第二个构造函数指定一个类和某个方法作为流程切点
在这里指定WaiterDelegate#service()方法作为切点,表示所有通过该方法直接或间接调用发起的调用匹配切点。
输出
①和②是直接通过waiter调用serveTo()和greetTo()方法的输出,此时增强没有起作用,③是通过WaiterDelegate#service()调用Waiter的serveTo()和greetTo()方法的输出,此时发现Waiter的两个方法都织入了增强。
流程切面和动态切面从某种程度可以算是一类切面,因为二者都需要在运行期判断动态的环境。
对于流程切面来说,代理对象在每次调用目标类方法时,都需要判断方法调用堆栈中是否有满足流程切点要求的方法,和动态切面一样,流程切面对性能的影响也很大。
假设我们希望由WaiterDelegate#service()发起调用调用而且被调用的方法是Waiter#greetTo()时才织入增强,这个点就是复合切点,它由两个单独的切点共同确定,第一个点就是7-35所定义的流程切点,另一个切点是方法名为“greetTo”的方法名切点。
用户可以只通过一个切点描述同时满足以上两个匹配条件的连接点,但更好的方法是使用Spring提供的ComposablePointcut将两个切点组合起来,通过切点的复合运算表示。ComposablePointcut可以将多个切点以并集和交集的方式组合起来,提供了切点之间复合运算的功能。
ComposablePointcut本身也是一个切点,实现了Pointcut接口,下面是ComposablePointcut的构造函数:
①ComposablePointcut():构造一个匹配所有类所有方法的复合切点
②ComposablePointcut(ClassFilter classFilter):构造一个匹配特定类所有方法的复合切点
③ComposablePointcut(MethodMatcher methodMatcher):构造一个匹配所有类特定方法的复合切点
④ComposablePointcut(ClassFilter classFilter,MethodMatcher methodMatcher):构造一个匹配特定类特定方法的复合切点
ComposablePointcut提供了三个交集运算的方法:
①ComposablePointcut intersection(ClassFilter classFilter):将复合切点和一个ClassFilter对象进行交集运算,得到一个结果复合切点
②ComposablePointcut intersection(MethodMatcher mm):将复合切点和一个MethodMatcher对象进行交集运算,得到一个结果复合切点
③ComposablePointcut intersection(Pointcut other):将复合切点和一个切点对象进行交集运算,得到一个结果复合切点
ComposablePointcut提供了两个并集运算的方法:
①ComposablePointcut union(ClassFilter filter):将复合切点和一个ClassFilter对象进行并集运算,得到一个结果复合切点
②ComposablePointcut union(MethodMatcher mm):将复合切点和一个MethodMatcher对象进行并集运算,得到一个结果复合切点
ComposablePointcut没有提供直接对两个切点进行并集元素的方法,可以使用Spring提供的org.springframework.aop.support.Pointcuts工具类,它有两个很好用的静态方法:
①Pointcut intersection(Pointcut a.Pointcut b):对两个切点进行交集运算,返回一个结果切点,该切点即ComposablePointcut对象的实例
②Pointcut union(Pointcut a,Pointcut b):对两个切点进行并集运算,返回一个结果切点,该切点即ComposablePointcut对象的实例
通过ComposablePointcut创建一个流程切点和方法切点的相交切点
配置复合切点切面的配置
在①处使用util命名空间的标签引用另一个Bean的属性
测试
输出
只有通过WaiterDelegate#service()方法调用Waiter#greetTo()方法时才织入了增强
IntroductionAdvisor同时继承Advisor和IntroductionInfo接口,IntroductionInfo接口描述了目标类需要实现的新接口。IntroductionAdvisor和PointcutAdvisor接口不同,它只有一个类过滤器器而没有MethodMatcher,这是因为引介切面的切点是类级别的,而Pointcut的切点是方法级别的。
IntroductionAdvisor有两个实现类,分别是DefaultIntroductionAdvisor和DeclareParentsAdvisor,前者是引介切面最常用的实现类,后者用于实现AspectJ语言的DeclareParent注解表示的引介切面
DefaultIntroductionAdvisor拥有三个构造函数
①DefaultIntroductionAdvisor(Advice advice):通过一个增强创建的引介切面,引介切面讲为目标对象新增增强对象中所有接口的实现
②DefaultIntroductionAdvisor(DynamicIntroductionAdvice advice,Class clazz)通过一个增强和一个指定的接口类创建引介切面,仅为目标对象新增clazz接口的实现
③DefaultIntroductionAdvisor(Advice advice,IntroductionInfo introductionInfo):通过一个增强和一个IntroductionInfo创建引介切面,目标对象需要实现哪些接口由introductionInfo对象的getInterfaces()方法表示
下面通过DefaultIntroductionAdvisor为代码7-22中的引介增强(即ControllablePerformanceMonitor)配置切面
Spring提供了自动代理机制,让容器自动生成代理,Spring使用BeanPostProcessor自动完成这项工作
实现类介绍
基于BeanPostProcessor的自动代理创建器的实现类,将根据一些规则自动在容器实例化Bean时为匹配的Bean生成代理实例,这些代理创建器可以分为3类:
①基于Bean配置名规则的自动代理创建器:允许为一组特定配置名的Bean自动创建代理实例的代理创建器,实现类为BeanNameAutoProxyCreator
②基于Advisor匹配机制的自动代理创建器:它会对容器中的所有Advisor进行扫描,自动将这些切面应用到匹配的Bean中(为目标Bean创建代理实例),实现类为DefaultAdvisorAutoProxyCreator
③基于Bean中AspectJ注解标签的自动代理创建器:为包含AspectJ注解的Bean自动创建代理实例,实现类为AnnotationAwareAspectJAutoProxyCreator
可以看到所有的自动代理创建器都实现了BeanPostProcessor,在容器实例化Bean时,BeanPostProcessor将对它进行加工处理,所以,自动代理创建器有机会对满足匹配规则的Bean 自动创建代理对象
下面通过BeanNameAutoProxyCreator完成相同的功能
BeanNameAutoProxyCreator有一个beanNames属性,允许用户指定一组需要自动代理的Bean名称,Bean名称可以用*通配符。
一般不会为FactoryBean的Bean创建代理,如果有需求,则需要在beanNames中指定添加$的Bean属性,如<property name=”beanNames vlaue=”$waiter” />等
BeanNameAutoProxyCreator的interceptorNames属性指定一个或多个增强Bean的名称。还有一个常用的optimize属性,如果设置为true,将强制使用CGLib动态代理技术。
通过这样的配置,容器在创建waiter和seller Bean的实例时,就会自动为它们创建代理对象
输出
从容器中返回的waiter和seller Bean的greetTo()都被织入了增强,即返回的waiter 和seller Bean都是被代理过的对象
DefaultAdvisorAutoProxyCreator能扫描容器中的Advisor并将Advisor自动织入匹配目标的Bean中,即为匹配的目标Bean自动创建代理
用DefaultAdvisorAutoProxyCreator定义了一个Bean,负责Advisor织入匹配的目标Bean中
测试
输出
Waiter#serveTo()方法没有被织入增强,而Waiter和Seller中的greetTo()方法都被织入了增强,增强被正确地织入匹配的连接点中
①基于JDK动态代理,通过接口来实现方法拦截,所以必须要确保要拦截的目标方法在接口中有定义,否则将无法实现拦截
⑤GCLib动态代理,通过动态生成子类来实现方法拦截,必须确保要拦截的目标方法可被子类访问,即目标方法必须定义为非final。且非私有实例方法。
在这里,引入DefaultAdvisorAutoProxyCreator为容器中所有带”To”方法名的目标Bean自动创建代理
输出
可以看到两个方法都被织入了增强,增强被正确地织入匹配的连接点中。
现在将Waiter中的serveTo()方法改为
输出
可以看出serveTo()和greetTo()都被织入了增强,但serveTo()内部调用的greetTo没有被织入增强。
在方法内部之间调用的时候,不会使用被增强的代理类,而是直接使用未被增强原类的方法。
想解决这个问题,就是在内部方法调用时,让其通过代理类调用其内部方法,所以,需要让原来的Waiter实现一个可注入自身代理类的接口BeanSelfProxyAware
需要实对所有实现了BeanSelfPorxyAware的Bean执行自身代理Bean的注入,设计一个可复用的注入装配器BeanSelfProxyAwareMounter
在①通过实现ApplicationContextAware#setApplicationContext()接口方法来注入Spring容器上下文。
在②实现了系统启动器SystemBootAddon#onReady()接口方法。
这个接口在系统所有组件都装载完成后,准备就绪前调用的插件接口,用于Spring容器启动完成后触发调用注入装配器BeanSelfProxyAwareMounter
在②中从Spring 容器中获取所有实现自身代理织入接口BeanSelfProxyAware的Bean,循环迭代遍历这些Bean,并调用setSelfProxy()方法将自身代理类注入自身
同时实现了Bean加载顺序接口Ordered,在插件中可以实现Ordered#getOrder()方法,返回一个整形数字来指定插件的执行顺序,值越小优先被加载处理。
最后需要设置一个启动管理器,告诉Spring什么时候触发BeanSelfProxyAwareMounter装配器
在①通过自动注入方法注入所有实现SystemBootAddon接口的插件
在②通过监听Spring容器的ContextRefreshedEvent时间调用容器中所有已注册的SystemRootAddon插件。
在Waiter类中让其实现BeanSelfProxyAware接口
AOP是Aspect Oriented Programing,面向切面编程。
7-1中,斜体的代码是方法性能监视代码,它们在方法调用前启动,在方法调用返回前结束,并在内部记录性能监视的结果信息,而黑色粗体的代码是事务开始和事务提交的代码。
在①②处的业务代码淹没在重复化非业务性的代码之中,性能监视和事务管理这些非业务性代码包围着业务性代码
假设将ForumService业务类看成一段圆木
我们无法通过抽象父类的方式消除如上的重复性横切代码,因为这些逻辑代码依附在业务类的流程中,它们不能转移到其他地方去。
AOP通过横向抽取机制为这类无法通过纵向继承体系进行抽象的重复性代码提供了解决方案。
AOP将分散在业务逻辑代码中的相同代码通过横向切割的方式抽取到一个独立的模块中,并能将这些独立的逻辑融合到业务逻辑中以完成和原来一样的业务逻辑
AOP术语
连接点(Joinpoint)特定点是程序执行的某个特定位置,如类开始初始化前,类初始化后,类的某个方法调用前/后,方法抛出异常后。一个类或一段代码拥有一些具有边界性质的特定点,这些代码中的特定点就被称为“连接点”。
Spring仅支持方法的连接点,即仅能在方法调用前,方法调用后,方法抛出异常时及方法调用前后这些程序执行点织入增强。
连接点由两个信息确定:一是用方法表示的程序执行点,二是用相对位置表示的方位。如在Test.foo()方法执行前的连接点,执行点为Test.foo(),方法为该方法执行前的位置。Spring使用切点对执行点进行定位,而方位则在增强类型中增强
切点(Pointcut)
每个程序类都有多个连接点,如一个拥有两个方法的类,这两个方法就是连接点,即连接点是程序类中客观存在的事物。AOP通过“切点”定位特定的连接点。连接点相当于数据库中的记录,而切点相当于查询条件,一个切点可以匹配多个连接点。
在Spring中,切点通过org.springframework.aop.Pointcut接口进行描述,它使用类和方法作为连接点的查询条件。Spring AOP的规则解析引擎负责解析切点所设定的查询条件,找到对应的连接点。确切地说,应该是执行点而非连接点,因为连接点是方法执行前/后等包括方位信息的具体程序执行点,而切点只定位到某个方法上,所以如果希望定位到具体的连接点上,需要提供方位信息
增强(Advice)
增强是织入目标类连接点上的一段程序代码。在Spring中,增强除用于描述一段程序代码外,还拥有另一个和连接点相关的信息,就是执行点的方位。结合执行点的方位信息和切点信息,就可以找到特定的连接。
正因为增强即包含由于添加到目标连接点上的一段执行逻辑,又包含用于定位连接点的方位信息,所以Spring所提供的增强接口都是带方位名的,如BeforeAdivce,AfterReturningAdvice和ThrowsAdvice等。BeforeAdvice表示方法调用前的位置,而AfterReturningAdvice表示访问后的位置。
所以只有结合切点和增强,才能确定特定的连接点并实施逻辑增强。
目标对象(Target)
增强逻辑的织入目标类。如果没有AOP,那么目标业务类需要自己实现所有的逻辑,如7-1.
在AOP的帮助下,ForumService只实现那些非横切逻辑的程序逻辑,而性能监视和事务管理等这些横切逻辑就可以使用AOP动态织入特定的连接点上
引介(Introduction)
引介是一种特殊的增强,为类添加一些属性和方法。这样,即使一个业务类原本没有实现某个接口,通过AOP的引介功能,也可以动态地为该业务类添加接口的实现逻辑,使业务类成为这个接口的实现类
织入(Weaving)
织入是将增强添加到目标类的具体连接点上的过程.AOP就像一台织布机,将目标类,增强或引介编织到一切。
AOP有三种织入方式:
①编译期织入,这要求使用特殊的Java编译器
②类装载期织入,这要求使用特殊的类装载器
③动态代理织入,在运行期为目标类添加增强生成子类的方式
Spring采用动态代理织入
代理(Proxy)
一个类被AOP织入增强后,就产生一个结果类,它是融合了原类和增强逻辑的代理类。根据不同的代理方式,代理类既可能是和原类具有相同接口的类,也可能就是原类的子类,所以可以采用与调用原类相同的方式调用代理类。
切面(Aspect)
切面由切点和增强(引介)组成,既包括横切逻辑的定义,也包括连接点的定义。Spring AOP就是负责实施切面的框架,它将切面所定义的横切逻辑织入切面所指定的连接点中
基础知识
Spring AOP使用了两种代理机制:一种是基于JDK的动态代理;另一种是基于CGLib的动态代理。之所以需要两种代理机制,是因为JDK本身只提供接口的代理,而不支持类的代理
带有横切逻辑的实例
public class ForumServiceImpl implements ForumService { public void removeTopic(int topicId) { //开始对该方法进行性能监视 // PerformanceMonitor.begin("com.smart.proxy.ForumServiceImpl.removeTopic"); System.out.println("模拟删除Topic记录:"+topicId); try { Thread.currentThread().sleep(20); } catch (Exception e) { throw new RuntimeException(e); } //结束对该方法的性能监视 // PerformanceMonitor.end(); } public void removeForum(int forumId) { //开始对该方法进行性能监视 // PerformanceMonitor.begin("com.smart.proxy.ForumServiceImpl.removeForum"); System.out.println("模拟删除Forum记录:"+forumId); try { Thread.currentThread().sleep(40); } catch (Exception e) { throw new RuntimeException(e); } //结束对该方法的性能监视 // PerformanceMonitor.end(); } }
7-2中,注释的代码就是具有横切逻辑的代码,每个Service类和每个业务方法体的前后都执行相同的代码逻辑:方法调用前启动启动PerformanceMonitor;方法调用后通知PerformanceMonitor结束性能监视并记录性能监视结果
PerformanceMonitor是性能监视的实现类,下面给出一个简单的实现版本
public class PerformanceMonitor { //通过一个ThreadLocal保存于调用线程相关的性能监视信息 private static ThreadLocal<MethodPerformace> performaceRecord = new ThreadLocal<MethodPerformace>(); //启动对某一目标方法的性能监视 public static void begin(String method) { System.out.println("begin monitor..."); MethodPerformace mp = performaceRecord.get(); if(mp == null){ mp = new MethodPerformace(method); performaceRecord.set(mp); } 20000 else{ mp.reset(method); } } public static void end() { System.out.println("end monitor..."); MethodPerformace mp = performaceRecord.get(); //打印出方法性能监视的结果信息 mp.printPerformace(); } }
ThreadLocal将非线程安全类改造为线程安全类。PerformanceMonitor 提供两个方法,通过begin(String method)方法开始对某个目标类方法的监视,其中method为目标类方法的全限定名(如com.smart.proxy.ForumServiceImpl.removeTopic),通过end()方法结束对目标类方法的监视,并给出性能监视信息,必须配套使用。
用于记录性能监视信息的MethodPerformace类
public class MethodPerformace { private long begin; private long end; private String serviceMethod; public MethodPerformace(String serviceMethod){ reset(serviceMethod); } public void printPerformace(){ end = System.currentTimeMillis(); //获取目标类方法执行完成后的系统时间 long elapse = end - begin; //计算出执行时间 System.out.println(serviceMethod+"花费"+elapse+"毫秒。"); //报告执行时间 } public void reset(String serviceMethod){ this.serviceMethod = serviceMethod; this.begin = System.currentTimeMillis(); //记录目标类方法开始执行点的系统时间 } }
将上面ForumServiceImpl的注释去掉,
通过下面代码测试拥有性能监视能力的ForumServiceImpl业务方法:
public class TestForumService { public static void main(String[] args) { ForumService forumService=new ForumServiceImpl(); forumService.removeTopic(10); forumService.removeForum(1012); } }
如7-2中,当某个方法需要进行性能监视时,必须调整方法代码,在方法体前后分别添加开始性能监视和结束性能监视的代码。这些非业务逻辑的代码破坏了业务逻辑的纯粹性。
通过代理的方式将业务类方法中开启和结束性能监视的横切代码从业务类中完成移除,并通过JDK或CGLib动态代理技术将横切代码织入目标方法的相应位置
JDK动态代理
JDK的动态代理主要涉及java.lang.reflect包中的两个类:Proxy和InvocationHandler。其中,InvocationHandler是一个接口,可以通过实现该接口定义横切逻辑,并通过反射机制调用目标类的代码,动态地将横切逻辑和业务逻辑编辑在一起。
而Proxy利用InvocationHandler动态创建一个符合某一接口的实例,生成目标类的代理对象。
在java中,invocation的意思是调用。
可以看到7-2中,性能监视的代码被注释了。从业务类中移除监视横切代码,必须为它找到安置处——InvocationHandler。
将性能监视横切代码安置在PerformanceHandler中
public class PerformanceHandler implements InvocationHandler { //① 实现invocationHandler private Object target; public PerformanceHandler(Object target){ //target为目标类 this.target=target; } public Object invoke(Object proxy,Method method,Object[] args)throws Throwable{ //③ PerformanceMonitor.begin(target.getClass().getName()+"."+method.getName()); //③-1 Object obj=method.invoke(target,args); //③-2 通过反射方法调用业务类的目标方法 PerformanceMonitor.end(); //③-1 return obj; } }
③处的invoke()方法中③-1所示部分的代码为性能监视的横切代码,横切代码只出现一次。③-2中的method.invoke()通过Java反射机制间接调用目标对象的方法,这样invocationHandler的invoke()方法就将横切逻辑代码③-1和业务类方法(method)的业务逻辑代码③-2编织在一起,所以,可以将InvocationHandler看成一个编织器。
首先实现InvocationHandler接口,该接口定义了一个invoke(Object proxy,Method method,Object[] args),其中proxy是最终生成的代理实例,一般不会用到;method是被代理目标实例的某个具体方法,通过它可以发起目标实例方法的反射调用;args是被代理实例某个方法的入参,在方法反射调用时使用。
在构造函数中通过target传入希望被代理的目标对象;在InvocationHandler接口方法的invoke(Object proxy,Method method,Object[] args)里,将目标实例传递给method.invoke()方法,并调用目标实例的方法。
下面通过Proxy结合PerformanceHandler创建ForumService接口的代理实例
public class ForumServiceTest { @Test public void proxy(){ ForumService target=new ForumServiceImpl(); //希望被代理的目标业务类 PerformanceHandler handler=new PerformanceHandler(target); //②将目标业务类和横切代码编织在一起 // ③根据编织了目标业务逻辑的和性能监视横切逻辑的InvocationHandler实例创建代理实例 ForumService proxy=(ForumService) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), handler); proxy.removeForum(10); //④调用代理实例 proxy.removeTopic(1012); //调用代理实例 } }
上面的代码往完成了业务类代码和横切代码的编织工作并生成了代理实例,在②让PerformanceHandler将性能监视横切逻辑编织到ForumService实例中,然后在③处,通过Proxy的newProxyInstance()静态方法为编织了业务类逻辑和性能监视逻辑的handler创建一个符合ForumService接口的代理实例。
该方法的第一个入参为类加载器;第二个入参为创建代理实例所需要实现的一组接口,第三个入参是整合了业务逻辑和横切逻辑的编织器对象
按照③处的设置方式,这个实例代理实现了业务目标类的所有接口,即ForumServiceImpl的ForumService接口,这样就可以按照调用ForumService接口实例相同的方式调用代理实例,如④,运行代码的输出结果和上面一样。
程序运行效果和直接在业务类中编写性能监视逻辑的效果一致,但原来分散的横切逻辑代码已经被抽取到PerformanceHandler中。当其他业务类的业务方法也需要使用性能监视时,只要用相同方式分别为它们创建代理对象即可。
通过时序图描述创建代理对象进行业务方法调用的整体逻辑,进一步认识代理对象的本质
ForumService代理实例内部利用PerformanceHandler横切逻辑和业务逻辑。调用者调用代理对象的removeForum()和removeTopic()方法时,如上图。
CGLib动态代理
JDK创建代理有一个限制,即只能为接口创建代理实例,可以从Proxy的接口方法newProxyInstance(ClassLoader loader,Class[] interfaces,InvocationHandler h)中看得出来。第二个入参interfaces就是需要代理实例实现的接口列表。对于没有通过接口定义业务方法的类,CGLib可以动态创建代理实例。
CGLib采用底层的字节码技术,可以为类创建一个子类,在子类中采用方法拦截的技术拦截所有父类方法的调用并顺势织入横切逻辑。
下面用GGLib技术编写一个可以为任何类创建织入性能监视横切逻辑代理对象的代理创建器
import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; import java.lang.reflect.Method; public class CglibProxy implements MethodInterceptor { private Enhancer enhancer=new Enhancer(); public Object getProxy(Class clazz){ enhancer.setSuperclass(clazz); //① 设置需要创建子类的类 enhancer.setCallback(this); return enhancer.create(); //② 通过字节码技术动态创建子类实例 } //③拦截父类的所有方法 public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy)throws Throwable{ PerformanceMonitor.begin(obj.getClass().getName()+"."+method.getName()); //③-1 Object result=proxy.invokeSuper(obj,args); //③-2通过代理类调用父类中的方法 PerformanceMonitor.end(); return result; } }
在上面代码中,用户可以通过getProxy(Class clazz)方法为一个类创建动态代理对象,该代理对象通过扩展clazz实现代理。在这个代理对象中,织入性能监视的横切逻辑(③-1)。
Intercept(Object obj,Method method,Object[] args,MethodProxy proxy)是CGLib定义的Interceptor接口方法,它拦截所有目标类方法的调用。其中,obj表示目标类的实例;method为目标类方法的反射对象;args为方法的动态入参;proxy为代理类实例。
发现代理类的名字变成
这个就是CGLib为ForumServiceImpl动态创建的子类。
由于CGLib采用动态创建子类的方式生成代理对象,所以不能对目标类中的final或private方法进行代理。
在使用CGLib动态代理时,必须引入CGLib类库
<!-- cglib依赖(spring依赖) --> <dependency> <groupId>org.ow2.asm</groupId> <artifactId>asm</artifactId> <version>${asm.version}</version> </dependency> <dependency> <groupId>org.ow2.asm</groupId> <artifactId>asm-util</artifactId> <version>${asm.version}</version> </dependency> <dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>${cglib.version}</version> <exclusions> <exclusion> <artifactId>asm</artifactId> <groupId>org.ow2.asm</groupId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>${slf4j.version}</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>${slf4j.version}</version> </dependency> <dependency> <groupId>aopalliance</groupId> <artifactId>aopalliance</artifactId> <version>${aopalliance.version}</version> </dependency> <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>${commons-codec.version}</version> </dependency>
代理知识小结
CGLib所创建的动态代理对象的性能比JDK所创建的动态代理对象的性能高很多,但CGLib在创建代理对象时所花费的时间比JDK动态代理多。对于singleton的代理对象或者具有实例池的代理,因为无需频繁地创建代理对象,所以比较适合采用CGLib动态代理技术;反之适合采用JDK动态代理技术
创建增强类
Spring使用增强类定义横切逻辑,同时由于Spring只支持方法连接点,增强还包括在方法的哪一点加入横切代码的方位信息,所以增强既包含横切逻辑,又包含部分连接点的信息增强类型
AOP联盟是众多开源AOP项目的联合组织。AOP联盟为增强定义了org.aopalliance.aop.Advice接口,Spring支持5种类型的增强带<<spring>>标识的接口是在Spring所定义的扩展增强接口,带<<aopalliance>>标识的接口是AOP联盟定义的接口。
按照增强在目标类方法中的连接点位置,可以分为5类:
①前置增强:org.springframework.aop.BeforeAdvice代表前置增强,因为Spring只支持方法级的增强,所以MethodBeforeAdvice是目前可用的前置增强,表示在目标方法执行前实施增强,而BeforeAdvice是为了将来版本扩展需要而定义的。
②后置增强:org.springframework.aop.AfterReturningAdvice代表后置增强,表示在目标方法执行后实施增强。
③环绕增强:org.aopalliance.intercept.MethodInterceptor代表环绕增强,表示在目标方法执行前后实施增强。
④异常抛出增强:org.springframework.aop.ThrowsAdvice代表抛出异常增强,表示在目标方法抛出异常后实施增强。
⑤引介增强:org.springframework.aop.IntroductionInterceptor代表引介增强,表示在目标类中添加一些新的方法和属性
前置增强
实例public interface Waiter { void greetTo(String name); void serveTo(String name); }
public class NaiveWaiter implements Waiter{ public void greetTo(String name) { System.out.println("greet to"+name+"..."); } public void serveTo(String name) { System.out.println("serving"+name+"..."); } }
我们对NaiveWaiter的服务行为进行规范,让其在打招呼和提供服务之前,必须使用礼貌用于
public class GreetingBeforeAdvice implements MethodBeforeAdvice { public void before(Method method, Object[] objects, Object obj) throws Throwable { String clientName=(String)objects[0]; System.out.println("How are you!Mr "+clientName); } }
MethodBeforeAdvice接口仅定义了唯一的方法:before(Method method,Object[] args,Object obj)throws Throwable,其中method为目标类的方法;objects为目标类方法的入参,而obj为目标类实例。当方法发生异常时,将阻止目标类方法的执行。
代码7-13
public class BeforeAdviceTest { @Test public void before(){ Waiter target=new NaiveWaiter(); BeforeAdvice advice=new GreetingBeforeAdvice(); //①Spring提供的代理工厂 ProxyFactory pf=new ProxyFactory(); //②设置代理目标 pf.setTarget(target); //③为代理目标添加增强 pf.addAdvice(advice); //④生成代理实例 Waiter proxy=(Waiter) pf.getProxy(); proxy.greetTo("John"); proxy.serveTo("Tom"); } }
输出
解析ProxyFactory
在BeforeAdviceTest中,使用org.springframework.aop.framework.ProxyFactory代理工厂将GreetingBeforeAdvice的增强织入目标类NaiveWaiter中。ProxyFactory内部就是使用JDK或CGLib动态代理技术将增强应用到目标类中的
Spring定义了org.springframework.aop.framework.AopProxy接口,并提供额两个final类型的实现类
Cglib2AopProxy使用CGLib动态代理技术创建代理,而JdkDynamicAopProxy使用JDK动态代理技术创建代理。
如果通过ProxyFactory的setInterfaces(Class[] interfaces)方法指定目标接口进行代理,则ProxyFactory使用JdkDynamicAopProxy;
如果是针对类的代理,则使用Cglib2AopProxy。
还可以通过ProxyFactory的setOptimize(true)方法让ProxyFactory启动优化代理方式,这样,针对接口的代理也会使用Cglib2AopProxy。
7-13中BeforeAdviceTest使用的是CGLib动态代理技术,当我们指定接口进行代理时,将使用JDK动态代理技术
//①Spring提供的代理工厂 ProxyFactory pf=new ProxyFactory(); //②设置代理目标 pf.setInterfaces(target.getClass().getInterfaces());//指定接口进行代理 pf.setTarget(target);
如果指定启动代理优化,则ProxyFactory还将使用Cglib2AopProxy代理
//①Spring提供的代理工厂 ProxyFactory pf=new ProxyFactory(); //②设置代理目标 pf.setInterfaces(target.getClass().getInterfaces());//指定接口进行代理 pf.setTarget(target);
pf.setOpaque(true); //启动优化
ProxyFactory通过addAdvice(Advice)方法添加一个增强,可以使用该方法添加多个增强。多个增强形成一个增强链,它们的调用顺序和添加顺序一致,可以通过addAdvice(int ,Advice)方法将增强添加到增强链的具体位置(初始位置为0)
在Spring中的配置
可以通过Spring 的配置声明一个代理
ProxyFactoryBean是FactoryBean接口的实现类,FactoryBean负责实例化一个Bean。ProxyFactoryBean负责为其他Bean创建代理实例,它在内部使用ProxyFactory来完成这项工作。
ProxyFactoryBean的几个常用的可配置属性
①target:代理的目标对象
②proxyInterfaces:代理所要实现的接口,可以是多个接口
③interceptorNames:需要织入目标对象的Bean列表,采用Bean的名称指定。这些Bean必须是实现了org.aopalliance.intercept.MethodInterceptor或org.springframework.aop.Advisor的Bean,配置中的顺序对应调用的顺序。
④singleton:返回的代理是否单实例,默认为单实例。
⑤optimize:当设置为true时,强制使用CGLib动态代理。对于singleton的代理,推荐使用CGLib;对于其他作用域类型的代理,最好使用JDK动态代理
⑥proxyTargetClass:是否对类进行代理(而不是对接口进行代理)。当设置为true时,使用CGLib动态代理
测试增强
7-15
String configPath="com/smart/advice/beans.xml"; ApplicationContext ctx=new ClassPathXmlApplicationContext(configPath); Waiter waiter=(Waiter)ctx.getBean("waiter"); waiter.greetTo("John");
输出以下信息
ProxyFactoryBean使用了JDK动态代理技术。可以调整配置,使用CGLib动态代理技术通过动态创建NaiveWaiter的子类来代理NaiveWaiter对象,如下:
<bean id="waiter" class="org.springframework.aop.framework.ProxyFactoryBean" p:interceptorNames="greetingAdvice" p:target-ref="target" p:proxyTargetClass="true"/>
将proxyTargetClass设置为true后,无须再设置proxyInterfaces属性,即使设置也会被忽略。
后置增强
后置增强在目标类方法调用后执行。假如希望服务生在每次服务后也使用规范的礼貌用语,可以通过一个后置增强来实施这一要求。
public class GreetingAfterAdvice implements AfterReturningAdvice { //在目标类方法调用后执行 public void afterReturning(Object returnObj, Method method, Object[] args, Object obj) throws Throwable { System.out.println("Please enjoy yourself"); } }
通过实现AfterReturningAdvice来定义后置增强的逻辑,AfterReturningAdvice接口也只定义了一个方法 afterReturning(Object returnObj, Method method, Object[] args, Object obj) throws Throwable ,其中returnObj为目标实例返回的结果;method为目标类的方法,args为目标方法的入参,而Obj为目标类实例。假设在后置增强中抛出异常,如果该异常是目标方法声明的异常,则该异常归并到目标方法中;如果不是目标方法所声明的异常,则Spring将其转为运行期异常抛出
<bean id="greetingBefore" class="com.smart.advice.GreetingBeforeAdvice"/> <bean id="greetingAfter" class="com.smart.advice.GreetingAfterAdvice" /> <bean id="target" class="com.smart.advice.NaiveWaiter"/> <bean id="waiter" class="org.springframework.aop.framework.ProxyFactoryBean" p:proxyInterfaces="com.smart.advice.Waiter" p:interceptorNames="greetingBefore,greetingAfter" p:target-ref="target"/>
interceptorNames是String[]类型的
运行7-15中的代码,输出
环绕增强
环绕增强允许在目标类方法调用前后织入横切逻辑,综合实现了前置,后置增强的功能。import org.aopalliance.intercept.MethodInterceptor; public class GreetingInterceptor implements MethodInterceptor { //截获目标类方法的执行,并在前后添加横切逻辑 public Object invoke(MethodInvocation invocation)throws Throwable{ Object[] args=invocation.getArguments(); //获取目标方法入参数组 String clientName=(String)args[0]; System.out.println("How are you!Mr."+clientName+"."); //在目标方法执行前调用 Object obj=invocation.proceed(); //通过反射机制调用目标方法 System.out.println("Please enjoy yourself"); //在目标方法执行后调用 return obj; } }
Spring用AOP联盟(aopalliance)所定义的MethodInterceptor(org.aopalliance.intercept.MethodInterceptor;)作为环绕增强的接口。该接口拥有唯一的接口方法Object invoke(MethodInvocation invocation)throws Throwable.
MethodInvocation不但封装了目标方法及其入参数组,还封装了目标方法所在的实例对象,
通过MethodInvocation的getArguments()方法可以获取目标方法的入参数组
通过proceed()方法反射调用目标实例相应的方法
通过在实现类中定义横切逻辑,可以很容易实现方法前后的增强。
配置
<bean id="greetingAround" class="com.smart.advice.GreetingInterceptor"/> <bean id="target" class="com.smart.advice.NaiveWaiter"/> <bean id="waiter" class="org.springframework.aop.framework.ProxyFactoryBean" p:proxyInterfaces="com.smart.advice.Waiter" p:interceptorNames="greetingAround" p:target-ref="target"/>
运行7-15中代码,输出信息同上
异常抛出增强
异常抛出增强最适合的应用场景是事务管理,当参与事务的某个DAO发生异常时,事务管理器就必须回滚事务。public class ForumService { public void removeForum(int forumId) { // do sth... throw new RuntimeException("运行异常。"); } public void updateForum(Forum forum) throws Exception{ // do sth... throw new SQLException("数据更新操作异常。"); } }
在模拟业务类ForumService中定义了两个业务方法,removeForum()抛出运行期异常,而updateForum()抛出SQLException。
下面通过TransactionManager这个异常抛出增强对业务方法进行增强处理,统一捕捉抛出的异常并回滚事务
public class TransactionManager implements ThrowsAdvice { //定义增强逻辑 public void afterThrowing(Method method,Object[] args,Object target,Exception ex)throws Throwable{ System.out.println("------------"); System.out.println("method"+method.getName()); System.out.println("抛出异常"+ex.getMessage()); System.out.println("成功回滚事务"); } }
ThrowsAdvice异常抛出增强接口没有定义任何方法,它是一个标签接口,在运行期Spring使用反射机制自行跑断,必须采用以下签名形式定义异常抛出的增强方法:
void afterThrowing(Method method,Object[] args,Object target,Throwable);
方法名必须是afterThrowing,前三个入参Method method,Object[] args,Object target是可选的(要么都提供,要么都不提供),最后一个入参是Throwable或其子类。
可以在同一个异常抛出中定义多个afterThrowing()方法,当目标类方法抛出异常时,Spring会自动选用最匹配的增强方法。
标签接口是没有任何方法和属性的接口,不对实现类有任何语义上的要求,仅表明它的实现属于一个特定的类型,Java用它标识某一类对象。有两个用途:
①通过标签接口标识同一类型的类,这些类本身可能并不具有相同的方法,如Advice接口
②通过标签接口使程序或JVM采取一些特殊处理,如java.io.Serializable,它告诉JVM对象可以被序列化
对这个异常抛出增强进行配置,因为ForumService是类,使用CGLib代理
<!--4. 异常抛出增强 --> <bean id="forumServiceTarget" class="com.smart.advice.ForumService" /> <bean id="transactionManager" class="com.smart.advice.TransactionManager" /> <bean id="forumService" class="org.springframework.aop.framework.ProxyFactoryBean" p:interceptorNames="transactionManager" p:target-ref="forumServiceTarget" p:proxyTargetClass="true"/>
采用下面的代码
public class ThrowAdviceTest { @Test public void throwAdvice(){ String configPath = "com/smart/advice/beans.xml"; ApplicationContext ctx = new ClassPathXmlApplicationContext(configPath); ForumService forumService = (ForumService)ctx.getBean("forumService"); try{ forumService.removeForum(10); } catch (Exception e) {} try{ forumService.updateForum(new Forum()); } catch (Exception e) {} } }
输出
可见,ForumService的两个方法所抛出的异常都被TransactionManager这个异常抛出增强捕获并成功处理了,将ForumService从事务管理繁复的代码中解放出来。
引介增强
引介增强为目标类创建新的方法和属性,所以引介增强的连接点是类级别的,而非方法级别的。通过引介增强,可以为目标类添加一个接口的实现,可以为目标类创建实现某接口的代理。Spring定义了引介增强接口IntroductionInterceptor,该接口没有定义任何方法,Spring为其提供了DelagatingIntroductionInterceptor实现类。一般情况下,通过扩展该实现类定义自己的引介增强类。
前面性能监视的例子,我们对所有的业务类都织入了性能监视的增强。由于性能监视会影响业务系统的性能,所以是否启动性能监视应该是可控的。现在通过引介增强来实现
首先定义一个用于标识目标类是否支持性能监视的接口
public interface Monitorable { void setMonitorActive(boolean active); }
通过setMonitorActive(boolean active)接口方法控制业务类性能监视功能的激活和关闭状态
下面通过扩展DelegatingIntroductionInterceptor为目标类引入性能监视的可控功能
7-22
public class ControllablePerformanceMonitor extends DelegatingIntroductionInterceptor implements Monitorable{ private ThreadLocal<Boolean> MonitorStatusMap=new ThreadLocal<Boolean>(); //① public void setMonitorActive(boolean active){ //② MonitorStatusMap.set(active); } //③拦截方法 public Object invoke(MethodInvocation mi) throws Throwable { Object obj=null; //④对于支持性能监视可控代理,通过判断其状态决定是否开启性能监视功能 if(MonitorStatusMap.get()!=null&&MonitorStatusMap.get()){ PerformanceMonitor.begin(mi.getClass().getName()+"."+mi.getMethod().getName()); obj=super.invoke(mi); PerformanceMonitor.end(); }else{ obj=super.invoke(mi); } return obj; } }
扩展DelegatingIntroductionInterceptor的同时还实现了Monitorable接口,提供接口方法的实现。
在①定义了一个ThreadLocal类型的变量,用于保存性能监视开关状态,为了解决单实例singleton线程安全的问题,通过ThreadLocal让每个线程单独使用一个状态。
在③中覆盖了父类中的invoke()方法,该方法用于拦截目标类方法的调用,根据开关状态有条件地对目标实例方法进行性能监视。
在④处,MonitorStatusMap.get()方法返回的Boolean被自动拆包为boolean类型的值
通过Spring配置,将这个引介增强织入业务类ForumService中
首先,需要指定引介增强所实现的接口,如①。其次,由于只能通过为目标创建子类的方法生成引介增强的代理,所以必须将proxyTargetClass设置为true
如果没对ControllablePerformanceMonitor进行线程安全的特殊处理,就必须将singleton属性设置为true,让ProxyFactoryBean产生prototype作用域类型的代码。当CGLib动态创建代理的性能很低,而每次通过getBean()方法中从容器中获取作用域类型为prototype的Bean时都将返回一个新的代理实例,这种性能影响是巨大的,这就是为什么通过ThreadLocal对ControllablePerformanceMonitor的开关状态进行线程安全化处理的原因。
通过线程安全化处理后,就可以使用默认的singleton Bean作用域,这样创建代理的动作仅发生一次。
进行测试
public class IntroduceTest { @Test public void introduce(){ String configPath="com/smart/introduce/beans.xml"; ApplicationContext ctx=new ClassPathXmlApplicationContext(configPath); ForumService forumService=(ForumService)ctx.getBean("forumService"); forumService.removeForum(10); //默认情况下未开启性能监视功能 forumService.removeTopic(1024); Monitorable monitorable=(Monitorable)forumService; //开启性能监视,实现了Monitorable接口 monitorable.setMonitorActive(true); forumService.removeForum(10); forumService.removeTopic(1024); } }
②处,强制性地将forumService转换为Monitorable类型,代码的成功执行表示从Spring容器中返回的代理确实引入了Monitorable接口方法的实现。
输出
创建切面
增强提供了连接点方位信息,如织入到方法前面,后面等,而切点进一步描述了织入哪些类的哪些方法上。Spring通过org.springframework.aop.Pointcut接口描述切点,Pointcut由ClassFilter和MethodMatcher构成,通过ClassFilter定位到特定类上,通过MethodMatcher定位到某些特定方法上,这样Pointcut就拥有了描述某些类的某些特定方法的能力。
ClassFilter只定义了方法matches(Class clazz),参数代表一个被检测类,该方法判断被检测的类是否匹配过滤条件
Spring支持两种方法匹配器(MethodMatcher):静态方法匹配器和动态方法匹配器。
静态方法匹配器仅对方法名签名(包括方法名和入参类型及顺序)进行匹配,而动态方法匹配器会在运行期检查方法入参的值。静态匹配仅判别一次,而动态匹配因为每次调用方法的入参可能不一样,所以每次调用方法都必须判断。因此,动态匹配对性能的影响很大。一般,动态匹配不常使用。
方法匹配器的类型由isRuntime()方法的返回值决定,返回false表示的是静态方法匹配器,返回true表示是动态方法匹配器
切点类型
Spring提供6中类型的切点,静态方法切点,动态方法切点,注解切点,表达式切点,流程切点,复合切点切面类型
由于增强即包含横切代码,又包含部分连接点信息(方法前,方法后主方位信息),所以可以仅通过增强类生成一个切面。但切点仅代表目标类连接点的部分信息(类和方法的定位),所以仅有切点无法制作出一个切面,必须结合增强才能制作出切面
Spring使用org.springframework.aop.Advisor接口表示切面的概念,一个切面同时包含横切代码和连接点信息,可以分为3类:一般切面,切点切面和引介切面,可以通过Spring所定义的切面清除地了解切面的分类
①Advisor:代表一般切面,仅包含一个Advice。因为Advice包含了横切代码和连接点信息,所以Advice(增强)就是一个简单的切面,只不过它代表的横切的连接点是目标类的所有方法,因为这个横切面太宽泛,一般不会直接使用
②PointcutAdvisor:代表具有切点的切面,包含Advice和Pointc两个类,可以通过类,方法名及方法方位等信息灵活地定义切面的连接点,提供更具适用性的切面。
③IntroductionAdvisor:代表引介切面,它对应引介增强的特殊的切面,应用于类层面上,所以引介切点用ClassFilter进行定义
PointcutAdvisor的主要实现类体系:
6个具体的实现类
①DefaultPointcutAdvisor:最常用 切面类型,可以通过任意Pointcut和Advice定义一个切面,唯一不支持的是引介的切面类型,一般可以通过扩展该类实现自定义的切面
②NameMatchMethodPointcutAdvisor:通过该类可以定义按方法名定义切点的切面
③RegexpMethodPointcutAdvisor:对于按正则表达式匹配方法名进行切点定义的切面,可以通过扩展实现类进行操作。RegexpMethodPointcutAdvisor允许用户以正则表达式模式串定义方法匹配的切点,其内部通过JdkRegexpMethodPointcut构造出正则表达式方法名切点。
④StaticMethodMatcherPointcutAdvisor:静态方法匹配器切点定义的切面,默认情况下匹配所有的目标类
⑤AspectJExpressionPointcAdvisor:用于AspectJ切点表达式定义切点的切面
⑥AspectJPointcutAdvisor:用于AspectJ语法定义切点的切面。
静态普通方法名匹配切面
StaticMethodMatcherPointcutAdvisor代表一个静态方法匹配切面,通过StaticMethodMatcherPointcut来定义切点,并通过类过滤和方法名来匹配定义的切点。希望通过StaticMethodMatcherPointcutAdvisor定义一个切面,在Waiter#greetTo()方法调用前织入一个增强,即连接点为Waiter#greetTo()方法调用前的位置。
public class GreetingAdvisor extends StaticMethodMatcherPointcutAdvisor { public boolean matches(Method method, Class clazz) { //切点方法匹配规则,方法名为greetTo return "greetoTo".equals(method.getName()); } //切点类匹配规则:为Waiter的类或子类 public ClassFilter getClassFilter() { return new ClassFilter() { public boolean matches(Class clazz) { return Waiter.class.isAssignableFrom(clazz); } }; } }
StaticMethodMatcherPointcutAdvisor抽象类唯一需要定义的是matches()方法,在默认情况下,该切面匹配所有的类,这里通过覆盖getClassFilter()方法,让它仅匹配Waiter类及其子类
Advisor还需要一个增强类的配置,为Advisor定义一个前置增强
public class GreetingBeforeAdvice implements MethodBeforeAdvice { public void before(Method method, Object[] objects, Object o) throws Throwable { System.out.println(o.getClass().getName()+"."+method.getName()); //输出切点 String cilentName=(String)objects[0]; System.out.println("How are you!Mr."+cilentName+"."); } }
可以直接通过ProxyFactory,通过手工编码的方式织入切面生成代理类,参考7-13在③使用addAdvisor()方法即可
代码清单 7-29 通过Spring配置来定义切面
在①处,将greetAdvice增强配置到greetingAdvisor切面中。StaticMethodMatcherPointcutAdvisor除具有advice属性外,还可以定义另外两个属性
①classFilter:类匹配过滤器,在GreetingAdvisor中用编码的方式设定了classFilter
②order:切面织入时的顺序,该属性用于定义Ordered接口表示的顺序
由于需要分别为waiter和seller两个Bean定义代理器,而且两者有很多公共的配置信息,所以使用一个父<bean>简化配置,在③和④,通过引用父<bean>定义了两个织入切面的代理
@Test public void test(){ String configPath="com/smart/advisor/beans.xml"; ApplicationContext ctx=new ClassPathXmlApplicationContext(configPath); Waiter waiter=(Waiter)ctx.getBean("waiter"); Seller seller=(Seller)ctx.getBean("seller"); waiter.greetTo("Tom"); waiter.serveTo("Tom"); seller.greetTo("Tom"); }
输出
可见切面只织入Waiter.greetTo()方法调用前的连接点上,Waiter.serveTo()和Seller.greetTo()方法没有织入切面。
静态正则表达式方法匹配切面
在StaticMethodMatcherPointcutAdvisor中仅能通过方法名定义切点,这不够灵活,假设目标类中有多个方法,且都满足一定的命名规范,使用正则表达式进行匹配描述就要灵活多了。RegexpMethodPointcutAdvisor是正则表达式方法匹配切面实现类
实例
7-30
在②处定义了一个匹配模式串”.greet.“,该模式串匹配Waiter.greetTo()方法。匹配模式串匹配的目标类方法的全限定名,即带类名的方法名
运行
@Test public void test(){ String configPath="com/smart/advisor/beans.xml"; ApplicationContext ctx=new ClassPathXmlApplicationContext(configPath); Waiter waiter=(Waiter)ctx.getBean("waiter1"); waiter.greetTo("Tom"); waiter.serveTo("Tom"); }
输出
可见Waiter.greetTo()方法被织入了切面,而Waiter.serveTo()方法没有被织入切面,除了例子中使用的patterns和advice-ref属性外,还有两个属性
①pattern:如果只有一个匹配模式串,则可以使用该属性进行配置。patterns属性用于定义多个匹配模式串,这些匹配模式串之间是“或”的关系
②order:切面在织入时对应的顺序
正则表达式的语法
动态切面
使用DefaultPointcutAdvisor和DynamicMethodMatcherPointcut可以创建动态切面。DynamicMethodMatcherPointcut是一个抽象类,将isRuntime()标识为final且返回true,这样子类就一定是一个动态切点。该抽象类默认匹配所有的类和方法,因此需要通过扩展该类编写复合要求的动态切点
public class GreetingDynamicPointcut extends DynamicMethodMatcherPointcut { private static List<String> sepciaClientList=new ArrayList<String>(); static{ sepciaClientList.add("John"); sepciaClientList.add("Tom"); } @Override public ClassFilter getClassFilter() { //①对类进行静态切点检查 return new ClassFilter() { public boolean matches(Class<?> aClass) { System.out.println("调用getClassFilter()对"+aClass.getName()+"做静态检查"); return Waiter.class.isAssignableFrom(aClass); //传入的类为指定的类 } }; } @Override public boolean matches(Method method, Class<?> targetClass) { //②对方法进行静态切点检查 System.out.println("调用matches(method,targetClass)"+targetClass.getName()+"."+method.getName()+"做静态检查"); return "greetTo".equals(method.getName()); } public boolean matches(Method method, Class<?> aClass, Object[] objects) { //③对方法进行动态切点检查 System.out.println("调用matches(method,aClass)"+aClass.getName()+"."+method.getName()+"做动态检查"); String clientName=(String)objects[0]; return sepciaClientList.contains(clientName); } }
上面的类既有用于静态切点检查的方法,又有用于动态切点检查的方法。
由于动态切点检查对性能会造成很大的影响,所以应当尽量避免在运行时每次都对目标类的每个方法进行动态检查。
Spring采用这样的机制:在创建代理时对目标类的每个连接点使用静态切点检查,如果仅通过静态切点检查就可以知道连接点不匹配,则在运行时不再进行动态检查;如果静态切点检查是匹配的,则在运行时才进行动态切点检查
在动态切点类中定义静态切点检查的方法可以避免不必要的动态检查操作,从而极大地提高运行效率。
在③中通过matches(Method method, Class
<bean id="waiterTarget" class="com.smart.advisor.Waiter"/> <bean id="dynamicAdvisor" class="org.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor" > <property name="pointcut"> <bean class="com.smart.advisor.GreetingDynamicPointcut"/> </property> <property name="advice"> <bean class="com.smart.advisor.GreetingBeforeAdvice"/> </property> </bean> <bean id="waiter2" class="org.springframework.aop.framework.ProxyFactoryBean" p:interceptorNames="dynamicAdvisor" p:target-ref="waiterTarget" p:proxyTargetClass="true"/>
使用DefaultPointcutAdvisor定义切面,使用内部Bean方式注入动态切点GreetingDynamicPointcut,用内部Bean方式注入增强GreetingBeforeAdvice。此外DefaultPointcutAdvisor还有一个order属性,用于定义切面的织入顺序。
运行测试代码
public class DynamicAdvisorTest { @Test public void dynamic(){ String configPath="com/smart/advisor/beans.xml"; ApplicationContext ctx=new ClassPathXmlApplicationContext(configPath); Waiter waiter=(Waiter)ctx.getBean("waiter2"); waiter.serveTo("Peter"); waiter.greetTo("Peter"); waiter.greetTo("John"); } }
输出
可以发现,Spring在创建代理织入切面时,对目标类中的所有方法都进行静态切点检查;在生成织入切面的代理对象后,第一次调用代理类的每一个方法进行一次静态切点检查,如果本次检查就能从候选者队列中将该方法排除,则以后对该方法的调用就不再执行静态切点检查;对于那些在静态切点匹配时匹配的方法,在后续调用该方法时,将执行动态切点检查。
在例子中,切点匹配的规则是:目标类为com.smart.advisor.Waiter或其子类;方法名为greetTo;动态入参clientNaem必须是特殊名单中的客户
基于这条规则,serveTo()及从Object中继承而来的toString(),hashCode()和clone()等方法通过静态切点检查就可以排除在候选者之外,只有greetTo()方法是动态切点检查的候选者,每次调用都会进行动态切点检查
可以发现,每次调用代理对象的任何一个方法,都会执行动态切点检查,这将导致很大的性能问题,所以在定义动态切点时,不要忘记同时覆盖getClassFilter(0和matches(Method method,Class clazz)方法,通过静态切点检查排除大部分方法
流程切面
Spring的流程切面由DefaultPointcutAdvisor和ControlFlowPointcut实现。流程切点代表由某个方法直接或间接发起调用的其他方法。假设通过一个WaiterDelegate类代理Waiter所有的方法
public class WaiterDelegate { private Waiter waiter; public void service(String clientName){ //waiter的方法通过该方法发起调用 waiter.greetTo(clientName); waiter.serveTo(clientName); } public void setWaiter(Waiter waiter){ this.waiter=waiter; } }
如果希望所有由WaiterDelegate#service()方法发起调用的其他方法都织入GreetingBeforeAdvice增强,就必须使用流程切面来完成目标。
使用DefaultPointcutAdvisor配置一个流程切面来完成这一需求
代码7-35
ControlFlowPointcut有两个构造函数,分别是ControlFlowPointcut(Class clazz)和ControlFlowPointcut(Class clazz,String methodName)。第一个构造函数指定一个类作为流程切点;而第二个构造函数指定一个类和某个方法作为流程切点
在这里指定WaiterDelegate#service()方法作为切点,表示所有通过该方法直接或间接调用发起的调用匹配切点。
public class ControlFlowAdvisorTest { @Test public void ControlFlowAdvisor(){ String configPath="com/smart/advisor/beans.xml"; ApplicationContext ctx=new ClassPathXmlApplicationContext(configPath); Waiter waiter=(Waiter)ctx.getBean("waiter3"); WaiterDelegate wd=new WaiterDelegate(); wd.setWaiter(waiter); waiter.serveTo("Peter"); waiter.greetTo("Peter"); wd.service("Peter"); } }
输出
①和②是直接通过waiter调用serveTo()和greetTo()方法的输出,此时增强没有起作用,③是通过WaiterDelegate#service()调用Waiter的serveTo()和greetTo()方法的输出,此时发现Waiter的两个方法都织入了增强。
流程切面和动态切面从某种程度可以算是一类切面,因为二者都需要在运行期判断动态的环境。
对于流程切面来说,代理对象在每次调用目标类方法时,都需要判断方法调用堆栈中是否有满足流程切点要求的方法,和动态切面一样,流程切面对性能的影响也很大。
复合切点切面
在前面的例子中定义的切面仅有一个切点,有时一个切点难以描述目标连接点的信息。假设我们希望由WaiterDelegate#service()发起调用调用而且被调用的方法是Waiter#greetTo()时才织入增强,这个点就是复合切点,它由两个单独的切点共同确定,第一个点就是7-35所定义的流程切点,另一个切点是方法名为“greetTo”的方法名切点。
用户可以只通过一个切点描述同时满足以上两个匹配条件的连接点,但更好的方法是使用Spring提供的ComposablePointcut将两个切点组合起来,通过切点的复合运算表示。ComposablePointcut可以将多个切点以并集和交集的方式组合起来,提供了切点之间复合运算的功能。
ComposablePointcut本身也是一个切点,实现了Pointcut接口,下面是ComposablePointcut的构造函数:
①ComposablePointcut():构造一个匹配所有类所有方法的复合切点
②ComposablePointcut(ClassFilter classFilter):构造一个匹配特定类所有方法的复合切点
③ComposablePointcut(MethodMatcher methodMatcher):构造一个匹配所有类特定方法的复合切点
④ComposablePointcut(ClassFilter classFilter,MethodMatcher methodMatcher):构造一个匹配特定类特定方法的复合切点
ComposablePointcut提供了三个交集运算的方法:
①ComposablePointcut intersection(ClassFilter classFilter):将复合切点和一个ClassFilter对象进行交集运算,得到一个结果复合切点
②ComposablePointcut intersection(MethodMatcher mm):将复合切点和一个MethodMatcher对象进行交集运算,得到一个结果复合切点
③ComposablePointcut intersection(Pointcut other):将复合切点和一个切点对象进行交集运算,得到一个结果复合切点
ComposablePointcut提供了两个并集运算的方法:
①ComposablePointcut union(ClassFilter filter):将复合切点和一个ClassFilter对象进行并集运算,得到一个结果复合切点
②ComposablePointcut union(MethodMatcher mm):将复合切点和一个MethodMatcher对象进行并集运算,得到一个结果复合切点
ComposablePointcut没有提供直接对两个切点进行并集元素的方法,可以使用Spring提供的org.springframework.aop.support.Pointcuts工具类,它有两个很好用的静态方法:
①Pointcut intersection(Pointcut a.Pointcut b):对两个切点进行交集运算,返回一个结果切点,该切点即ComposablePointcut对象的实例
②Pointcut union(Pointcut a,Pointcut b):对两个切点进行并集运算,返回一个结果切点,该切点即ComposablePointcut对象的实例
通过ComposablePointcut创建一个流程切点和方法切点的相交切点
public class GreetingComposablePointcut { public Pointcut getIntersectionPointcut(){ ComposablePointcut cp=new ComposablePointcut(); //创建一个复合切点 Pointcut pt1=new ControlFlowPointcut(WaiterDelegate.class,"service");//创建一个流程切点,对指定类的指定方法有效 NameMatchMethodPointcut pt2=new NameMatchMethodPointcut(); //创建一个方法名切点 pt2.addMethodName("greetTo"); return cp.intersection(pt1).intersection((Pointcut) pt2); //将两个切点进行交集操作,注意将NameMatchMethodPointcut转换为Pointcut } }
配置复合切点切面的配置
在①处使用util命名空间的标签引用另一个Bean的属性
测试
public class ComposablePointcutAdvisorTest { @Test public void test(){ String config="com/smart/advisor/beans.xml"; ApplicationContext ctx=new ClassPathXmlApplicationContext(config); Waiter waiter=(Waiter)ctx.getBean("waiter4"); WaiterDelegate wd=new WaiterDelegate(); wd.setWaiter(waiter); waiter.serveTo("Peter"); waiter.greetTo("Peter"); wd.service("Peter"); } }
输出
只有通过WaiterDelegate#service()方法调用Waiter#greetTo()方法时才织入了增强
引介切面
引介切面是引介增强的封装器,通过引介切面,可以更容易地为现有对象添加任何接口的实现。IntroductionAdvisor同时继承Advisor和IntroductionInfo接口,IntroductionInfo接口描述了目标类需要实现的新接口。IntroductionAdvisor和PointcutAdvisor接口不同,它只有一个类过滤器器而没有MethodMatcher,这是因为引介切面的切点是类级别的,而Pointcut的切点是方法级别的。
IntroductionAdvisor有两个实现类,分别是DefaultIntroductionAdvisor和DeclareParentsAdvisor,前者是引介切面最常用的实现类,后者用于实现AspectJ语言的DeclareParent注解表示的引介切面
DefaultIntroductionAdvisor拥有三个构造函数
①DefaultIntroductionAdvisor(Advice advice):通过一个增强创建的引介切面,引介切面讲为目标对象新增增强对象中所有接口的实现
②DefaultIntroductionAdvisor(DynamicIntroductionAdvice advice,Class clazz)通过一个增强和一个指定的接口类创建引介切面,仅为目标对象新增clazz接口的实现
③DefaultIntroductionAdvisor(Advice advice,IntroductionInfo introductionInfo):通过一个增强和一个IntroductionInfo创建引介切面,目标对象需要实现哪些接口由introductionInfo对象的getInterfaces()方法表示
下面通过DefaultIntroductionAdvisor为代码7-22中的引介增强(即ControllablePerformanceMonitor)配置切面
<!--引入切面--> <bean id="introduceAdvisor" class="org.springframework.aop.support.DefaultIntroductionAdvisor"> <constructor-arg> <bean class="com.smart.introduce.ControllablePerformaceMonitor" /> </constructor-arg> </bean> <bean id="forumServiceTarget" class="com.smart.introduce.ForumService" /> <bean id="forumService" class="org.springframework.aop.framework.ProxyFactoryBean" p:interceptorNames="introduceAdvisor" p:target-ref="forumServiceTarget" p:proxyTargetClass="true"/>
自动创建代理
前面都通过ProxyFactoryBean创建织入切面的代理,每个需要被代理的Bean都需要使用一个ProxyFactoryBean进行配置。Spring提供了自动代理机制,让容器自动生成代理,Spring使用BeanPostProcessor自动完成这项工作
实现类介绍
基于BeanPostProcessor的自动代理创建器的实现类,将根据一些规则自动在容器实例化Bean时为匹配的Bean生成代理实例,这些代理创建器可以分为3类:
①基于Bean配置名规则的自动代理创建器:允许为一组特定配置名的Bean自动创建代理实例的代理创建器,实现类为BeanNameAutoProxyCreator
②基于Advisor匹配机制的自动代理创建器:它会对容器中的所有Advisor进行扫描,自动将这些切面应用到匹配的Bean中(为目标Bean创建代理实例),实现类为DefaultAdvisorAutoProxyCreator
③基于Bean中AspectJ注解标签的自动代理创建器:为包含AspectJ注解的Bean自动创建代理实例,实现类为AnnotationAwareAspectJAutoProxyCreator
可以看到所有的自动代理创建器都实现了BeanPostProcessor,在容器实例化Bean时,BeanPostProcessor将对它进行加工处理,所以,自动代理创建器有机会对满足匹配规则的Bean 自动创建代理对象
BeanNameAutoProxyCreator
代码清单7-29中通过配置两个ProxyFactoryBean分别为waiter和seller的Bean创建代理对象。下面通过BeanNameAutoProxyCreator完成相同的功能
bean id="waiter" class="com.smart.advisor.Waiter" /> <bean id="seller" class="com.smart.advisor.Seller" /> <bean id="greetingAdvice" class="com.smart.advisor.GreetingBeforeAdvice" /> <!-- 通过Bean名称自动创建代理 --> <bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator" p:beanNames="*er" p:interceptorNames="greetingAdvice" p:optimize="true"/>
BeanNameAutoProxyCreator有一个beanNames属性,允许用户指定一组需要自动代理的Bean名称,Bean名称可以用*通配符。
一般不会为FactoryBean的Bean创建代理,如果有需求,则需要在beanNames中指定添加$的Bean属性,如<property name=”beanNames vlaue=”$waiter” />等
BeanNameAutoProxyCreator的interceptorNames属性指定一个或多个增强Bean的名称。还有一个常用的optimize属性,如果设置为true,将强制使用CGLib动态代理技术。
通过这样的配置,容器在创建waiter和seller Bean的实例时,就会自动为它们创建代理对象
public class BeanNameAutoProxyCreatorTest { @Test public void test(){ String config="com/smart/autoproxy/beans.xml"; ApplicationContext ctx=new ClassPathXmlApplicationContext(config); Waiter waiter=(Waiter)ctx.getBean("waiter"); Seller seller=(Seller)ctx.getBean("seller"); waiter.greetTo("John"); seller.greetTo("Tom"); } }
输出
从容器中返回的waiter和seller Bean的greetTo()都被织入了增强,即返回的waiter 和seller Bean都是被代理过的对象
DefaultAdvisorAutoProxyCreator
切面Advisor是切点和增强的复合体,Advisor本身已包含足够的信息,如横切逻辑(要织入什么)及连接点(织入哪里)DefaultAdvisorAutoProxyCreator能扫描容器中的Advisor并将Advisor自动织入匹配目标的Bean中,即为匹配的目标Bean自动创建代理
<bean id="waiter" class="com.smart.advisor.Waiter" /> <bean id="seller" class="com.smart.advisor.Seller" /> <bean id="greetingAdvice" class="com.smart.advisor.GreetingBeforeAdvice" /> <!-- 通过Advisor自动创建代理--> <bean id="regexpAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor" p:patterns=".*greet.*" p:advice-ref="greetingAdvice"/> <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>
用DefaultAdvisorAutoProxyCreator定义了一个Bean,负责Advisor织入匹配的目标Bean中
测试
public class DefaultAdvisorAutoProxyCreatorTest { @Test public void test(){ String config="com/smart/autoproxy/beans.xml"; ApplicationContext ctx=new ClassPathXmlApplicationContext(config); Waiter waiter=(Waiter)ctx.getBean("waiter"); Seller seller=(Seller)ctx.getBean("seller"); waiter.serveTo("John"); waiter.greetTo("John"); seller.greetTo("Tom"); } }
输出
Waiter#serveTo()方法没有被织入增强,而Waiter和Seller中的greetTo()方法都被织入了增强,增强被正确地织入匹配的连接点中
AOP无法增强疑难问题剖析
Aop底层实现有两种方法:①基于JDK动态代理,通过接口来实现方法拦截,所以必须要确保要拦截的目标方法在接口中有定义,否则将无法实现拦截
⑤GCLib动态代理,通过动态生成子类来实现方法拦截,必须确保要拦截的目标方法可被子类访问,即目标方法必须定义为非final。且非私有实例方法。
public class AopAwareTest { @Test public void autoProxy(){ String config="com/smart/autoproxy/beans-aware.xml"; ApplicationContext ctx=new ClassPathXmlApplicationContext(config); Waiter waiter=(Waiter)ctx.getBean("waiter"); waiter.serveTo("John"); waiter.greetTo("John"); } }
在这里,引入DefaultAdvisorAutoProxyCreator为容器中所有带”To”方法名的目标Bean自动创建代理
<?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:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <context:component-scan base-package="com.smart.aop" /> <bean id="waiter" class="com.smart.advisor.Waiter" /> <bean id="greetingAdvice" class="com.smart.advisor.GreetingBeforeAdvice" /> <!--通过Advisor自动创建代理--> <bean id="regexpAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor" p:patterns=".*To.*" p:advice-ref="greetingAdvice" /> <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" p:proxyTargetClass="true" /> </beans>
输出
可以看到两个方法都被织入了增强,增强被正确地织入匹配的连接点中。
现在将Waiter中的serveTo()方法改为
public void serveTo(String name){ System.out.println("waiter serving "+name+"..."); greetTo(name); }
输出
可以看出serveTo()和greetTo()都被织入了增强,但serveTo()内部调用的greetTo没有被织入增强。
在方法内部之间调用的时候,不会使用被增强的代理类,而是直接使用未被增强原类的方法。
想解决这个问题,就是在内部方法调用时,让其通过代理类调用其内部方法,所以,需要让原来的Waiter实现一个可注入自身代理类的接口BeanSelfProxyAware
public interface BeanSelfProxyAware { void setSelfProxy(Object object); //织入自身代理类接口 }
需要实对所有实现了BeanSelfPorxyAware的Bean执行自身代理Bean的注入,设计一个可复用的注入装配器BeanSelfProxyAwareMounter
@Component public class BeanSelfProxyAwareMounter implements SystemBootAddon, ApplicationContextAware { private Logger logger= LoggerFactory.getLogger(this.getClass()); private ApplicationContext applicationContext; //①注入Spirng容器 public void setApplicationContext(ApplicationContext applicationContext) throws BeansException{ this.applicationContext = applicationContext; } //②实现系统启动器接口中的装配就绪方法 public void onReady(){ //从容器中获取所有自动注入的自动代理Bean Map<String,BeanSelfProxyAware> proxyAwareMap= applicationContext.getBeansOfType(BeanSelfProxyAware.class); if(proxyAwareMap!=null){ for(BeanSelfProxyAware beanSelfProxyAware:proxyAwareMap.values()){ beanSelfProxyAware.setSelfProxy(beanSelfProxyAware); if(logger.isDebugEnabled()){ logger.debug("{}注册自身被代理的实例"); } } } } public int getOrder(){ return Ordered.HIGHEST_PRECEDENCE; } }
在①通过实现ApplicationContextAware#setApplicationContext()接口方法来注入Spring容器上下文。
在②实现了系统启动器SystemBootAddon#onReady()接口方法。
这个接口在系统所有组件都装载完成后,准备就绪前调用的插件接口,用于Spring容器启动完成后触发调用注入装配器BeanSelfProxyAwareMounter
public interface SystemBootAddon extends Ordered { //在系统就绪后调用的方法 void onReady(); }
在②中从Spring 容器中获取所有实现自身代理织入接口BeanSelfProxyAware的Bean,循环迭代遍历这些Bean,并调用setSelfProxy()方法将自身代理类注入自身
同时实现了Bean加载顺序接口Ordered,在插件中可以实现Ordered#getOrder()方法,返回一个整形数字来指定插件的执行顺序,值越小优先被加载处理。
最后需要设置一个启动管理器,告诉Spring什么时候触发BeanSelfProxyAwareMounter装配器
@Component public class SystemBootManager implements ApplicationListener<ContextRefreshedEvent> { private Logger logger= LoggerFactory.getLogger(this.getClass()); private List<SystemBootAddon> systemBootAddons= Collections.EMPTY_LIST; private boolean hasRunOnce=false; //①注入所有SystemBootAddon插件 @Autowired(required = false) //找不到也不报错 public void setSystemBootAddons(List<SystemBootAddon> systemBootAddons){ Assert.notEmpty(systemBootAddons); OrderComparator.sort(systemBootAddons); this.systemBootAddons=systemBootAddons; } //②触发所有插件 public void onApplicationEvent(ContextRefreshedEvent event){ if(!hasRunOnce){ for(SystemBootAddon systemBootAddon:systemBootAddons){ systemBootAddon.onReady(); if(logger.isDebugEnabled()){ logger.debug("执行插件:{}",systemBootAddon.getClass().getCanonicalName()); } } hasRunOnce=true; }else{ if (logger.isDebugEnabled()) { logger.debug("已执行过容器启动插件集,本次忽略之."); } } } }
在①通过自动注入方法注入所有实现SystemBootAddon接口的插件
在②通过监听Spring容器的ContextRefreshedEvent时间调用容器中所有已注册的SystemRootAddon插件。
在Waiter类中让其实现BeanSelfProxyAware接口
相关文章推荐
- Spring对DAO的支持(精通Spring+4.x++企业应用开发实战 第十章)
- Spring的事务管理难点剖析(精通Spring+4.x++企业应用开发实战 第12章)
- Spring 入门实例 简易登录系统(精通Spring+4.x++企业应用开发实战 学习笔记一)
- Spring boot入门实例 简易登录(精通Spring+4.x++企业应用开发实战 学习笔记二)
- Spring容器高级主题(精通Spring+4.x++企业应用开发实战 五)
- 基于@AspectJ和Schema的AOP(精通Spring+4.x++企业应用开发实战 第八章)
- 精通Spring 4.x企业应用开发实战——IoC(五)
- Spring SpEL(精通Spring+4.x++企业应用开发实战 第九章)
- 精通Spring 4.x企业应用开发实战——笔记(二)
- 精通Spring 4.x企业应用开发实战——AOP(七②)
- 精通Spring 4.x企业应用开发实战——IoC(四)
- 精通Spring 4.x企业应用开发实战——高级主题(六)
- 精通Spring 4.x企业应用开发实战——AOP(七①)
- 精通Spring 4.x企业应用开发实战——IoC(三)
- 精通Spring 4.x企业应用开发实战——笔记(一)
- 在IoC容器中装配Bean(精通Spring+4.x++企业应用开发实战 四)
- 《精通Spring 4.X企业应用开发实战》读书笔记1-1(IoC容器和Bean)
- 论坛开发实例 (精通Spring+4.x++企业应用开发实战)
- Ioc容器 (精通Spring+4.x++企业应用开发实战 三)
- 《Spring 3.x 企业应用开发实战》学习笔记 第三章 IoC容器概述 3.2 相关Java基础知识 类装载器 反射机制