基于@AspectJ和Schema的AOP(精通Spring+4.x++企业应用开发实战 第八章)

来源:互联网 发布:社会融资规模数据特点 编辑:程序博客网 时间:2024/05/17 23:42

Spring对AOP的支持

Spring对AOP功能进行了很重要的增强:
①新值了基于Schema的配置支持,为AOP专门提供了aop命名空间
②新增了对AspectJ切点表达式语言的支持。@AspectJ允许开发者在POJO中定义切面。Spring使用和@AspectJ相同风格的注解,并通过AspectJ提供的注解库和解析库处理切点。由于Spring只支持方法级的切点,仅对@AspectJ提供了有限的支持
③可以无缝地集成AspectJ

注解知识快速进阶

了解注解

在编写代码时,除源程序外,还会使用Javadoc标签对类,方法或成员变量进行注释,以便使用Javadoc工具生成和源码配套的Javadoc文档。这些@param,@return等Javadoc标签就是注解标签,为第三方工具提供了描述程序代码的注释信息。

注解(annotation)是代码的附属信息,注解不能直接干扰程序代码的运行。Java语言解析器会忽略这些注解,由第三方工具负责对注解进行处理。
第三方工具可以利用代码中的注解间接控制程序代码的运行,通过Java反射机制读取注解的信息,并根据这些信息更改目标程序的逻辑,这正式Spring AOP对@AspectJ提供支持所采取的方法

一个简单的注解类

通常,第三方工具不但负责处理特定的注解,还提供了这些注解的定义,所以仅需关注如何使用注解即可。
Java提供了定义注解的语法,下面编写一个简单的注解类
8-1 NeedTest注解类

@Retention(RetentionPolicy.RUNTIME)   //①声明注解的保留期限@Target(ElementType.METHOD)           //②声明可以使用该注解的目标类型public @interface NeedTest {         //③定义注解    boolean value() default true;   //④声明注解成员}

Java规定使用@interface修饰符定义注解类,如③。一个注解可以拥有多个成员,成员声明和接口方法声明类似。这里仅定义了一个成员,如④。

成员声明有几点限制:
①成员以无入参,无抛出异常的方式声明,如boolean value(String str),boolean value() throws Exception 等方式是非法的
②可以通过default为成员指定一个默认值,如String level() default “LOW_LEVEL”,int high() default 2是合法的,也可以不指定默认值
③成员类型是受限的,合法的类型包括原始类型及其封装类,String,Class,enums,注解类型,以及上述类型的数组类型,如ForumService value(),List foo()是非法的

在①和②所看到的注解是Java预定义的注解,称为元注解(Meta-Annotation),它们被Java编译器使用,会对注解类的行为产生影响。
@Retention(RetentionPolicy.RUNTIME)表示NeedTest这个注解可以在运行期被JVM读取,注解的保留期限类型在java.lang.annotation.Retention类中定义,介绍如下:
①SOURCE:注解信息仅保留在目标类代码的源码文件中,但对应的字节码文件将不再保留
②CLASS:注解信息将进入目标类代码的字节码文件中,但类加载器加载字节码文件时不会将注解加载到JVM,即运行期不能获取注解信息
③RUNTIME:注解信息在目标类加载到JVM后仍能保留,在运行期可以通过反射机制读取类中的注解信息

Target(ElementType.METHOD)表示NeedTest这个注解只能应用到目标类的方法上,注解的应用目标在java.lang.annotation.ElementType,介绍如下:
①TYPE:类,接口,注解类,Enum声明处,相应的注解称为类型注解
② FIELD:类成员变量或常量声明处,相应的注解称为域值注解
③METHOD:方法声明处,相应的注解称为方法注解
④PARAMETER:参数声明处,相应的注解称为参数注解
⑤CONSTRUCTOR:构造函数声明处,相应的注解称为构造函数注解
⑥LOCAL_VARIABLE:局部变量声明处,相应的注解称为局部变量注解
⑦ANNOTATION_TYPE:注解类声明处,相应的注解称为注解类注解,ElementType.TYPE包含ElementType.ANNOTATION_TYPE
⑧PACKAGE:包声明处,相应的注解称为包注解

如果注解只有一个成员,则成员名必须取名为valule(),在使用时可以忽略成员名和赋值号(=),如@NeedTest(true)。
当注解类拥有多个成员时,如果仅对value成员进行赋值,也可以不使用赋值号,如果同时对多个成员进行赋值,则必须使用赋值号,如DeclareParents(value=”NaiveWaiter”,defaultImpl=SmartSeller.class).
注解类可以没有成员,没有成员的注解称为标识注解,解释程序以标识注解存在与否进行相应的处理;此外,所有的注解类偶读隐式继承于java.lang.annotation.Annotation,但注解不允许显式继承于其他的接口

使用注解

在ForumService中使用NeedTest注解,标注业务方法是否需要测试

public class ForumService {    @NeedTest(value = true)  //①标注注解    public void deleteForum(int forumId){        System.out.println("删除论坛模块:"+forumId);    }    @NeedTest(value=false)  //②标注注解    public void deleteTopic(int postId){        System.out.println("删除论坛主题:"+postId);    }}

如果注解类和目标类不在同一个包中,则需要通过import引用注解类
由于NeedTest注解的保留期限是RetentionPolicy.RUNTIME类型,因此,当ForumService被加载到JVM时,仍可通过反射机制访问到ForumService各个方法的注解信息

在①和②,使用NeedTest对方法进行注解。在标注注解时,可以通过以下对注解成员进行赋值
这里写图片描述
如果成员是数组类型,则可以通过{}进行赋值,如boolean数组的成员可以设置为{true,.false,true}

下面是注解标注的几个例子
这里写图片描述
@Reviews注解拥有一个@Review注解数组类型的成员。@Review注解类型有3个成员,其中reviewer,comment都是String类型的成员,但comment有默认值,而grade是枚举类型的成员

访问注解

注解不会直接影响程序的运行,但第三方工具或程序可以利用代码中的注解完成特殊的任务,间接控制程序的运行。对于RetentionPolicy.RUNTIME保留期限的注解,可以通过反射机制访问类中的注解

在Java中,Package,Class,Constructor,Method及Field等反射对象都有访问注解信息的方法
<T extends Annotation>T getAnnotation(Class<T> annotationClass),该方法支持通过泛型直接返回注解对象

下面通过反射来访问注解,得出ForumService类通过@NeedTest注解所承载的测试需求:
8-3 ToolTest:访问代码中的注解

public class ToolTest {    @Test    public void tool(){        //①得到ForumService对应的Class对象        Class clazz=ForumService.class;        //②得到ForumService对应的Method数组        Method[] methods=clazz.getDeclaredMethods();        System.out.println(methods.length);        for(Method method:methods){            //③获取方法上所标注的注解对象            NeedTest nt=method.getAnnotation(NeedTest.class);            if(nt!=null){                if(nt.value()){                    System.out.println(method.getName()+"()需要测试");                }else{                    System.out.println(method.getName()+"()不需要测试");                }            }        }    }}

在③处通过方法的反射对象,获取了方法上所标注的NeedTest注解对象,接着访问注解对象的成员

输出
这里写图片描述

注意,需要早pom.xml文件中添加testng类包的依赖

        <!-- testng测试环境依赖-->        <dependency>            <groupId>org.testng</groupId>            <artifactId>testng</artifactId>            <version>${testng.version}</version>            <scope>test</scope>        </dependency>        <properties>           <testng.version>6.8.7</testng.version>        </properties>

着手使用@AspectJ

上一章分别使用Pointcut和Advice接口描述切点和增强,并用Advisor整合二者描述切面,@AspectJ则采用注解来描述切点,注解,只是表达方式不同,二者描述内容是相同的

使用前的准备

Java必须是5.0以上版本。

Spring在处理@AspectJ注解表达式时,需要将Spring 的asm模块添加到类路径中。asm是轻量级的字节码处理框架,因为Java的反射机制无法获取入参名,Spring就利用asm处理@AspectJ锁描述的方法入参名

Spring采用AspectJ提供的@AspectJ注解类库及相应的解析类库,需要在pom.xml文件中添加aspectj.weaver和aspectj.tools类包的依赖

一个简单的例子

@AspectJ采用不同的方式对AOP进行描述,依旧使用NaiveWaiter的例子进行讲解

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+"...");    }}public interface Waiter {    void greetTo(String name);    void serveTo(String name);}

使用@AspectJ注解定义一个切面

@Aspect      //①通过该注解将 PreGreetingAspect标识为一个切面public class PreGreetingAspect {    @Before("execution(* greetTo(..))")      //②定义切点和增强类型    public void beforeGreeting(){       //③增强的横切逻辑        System.out.println("How are you");    }}

需在pom.xml中添加aspectj.weaver类包的依赖

        <!-- 引入aspectj.weaver类包的依赖-->        <dependency>            <groupId>org.aspectj</groupId>            <artifactId>aspectjweaver</artifactId>            <version>${aspectj.version}</version>        </dependency>        <properties>           <aspectj.version>1.8.1</aspectj.version>         </properties>

首先在PreGreetingAspect类定义处标注了@Aspect注解,这样第三方处理程序就可以通过类是否拥有@Aspect注解判断其是否为一个切面,如①

在beforeGreeting()方法定义处标注了@Before注解,并为该注解提供了成员值”“”execution(* greetTo(..))”,如②。提供了两个信息,@Before注解表示该增强是前置增强,而成员值是一个@AspectJ切点表达式。意思是,在目标类的greetTo()方法上织入增强,greetTo()方法可以带任意的入参和任意的返回值

最后,在③的beforeGreeting()方法是增强所使用的横切逻辑,该横切逻辑在目标方法前调用
这里写图片描述

PreGreetingAspect通过注解和代码,将切点,增强类型和增强的横切逻辑糅合到一个类中,使切面的定义更方便。PreGreetingAspect类相当于上一章中BeforeAdvice,NameMatchMethodPointcut及DefaultPointcutAdvisor三者联合表达的意思。上一章基于接口切面的学习可以更深刻了解Spring AOP的内核技术

通过AspectJProxyFactory为NaiveWaiter生成织入PreGreetingAspect切面的代理

public class AspectJProxyTest {    @Test    public void proxy(){        Waiter tartget=new NaiveWaiter();        AspectJProxyFactory factory=new AspectJProxyFactory();        //①设置目标对象        factory.setTarget(tartget);        //②添加切面类        factory.addAspect(PreGreetingAspect.class);        //③生成织入切面的代理对象        Waiter proxy=factory.getProxy();        proxy.greetTo("John");        proxy.serveTo("John");    }}

注意在pom.xml文件中引入Spring类包依赖

        <!-- spring 依赖-->        <dependency>            <groupId>org.springframework</groupId>            <artifactId>spring-beans</artifactId>            <version>${spring.version}</version>        </dependency>        <dependency>            <groupId>org.springframework</groupId>            <artifactId>spring-context</artifactId>            <version>${spring.version}</version>        </dependency>        <dependency>            <groupId>org.springframework</groupId>            <artifactId>spring-context-support</artifactId>            <version>${spring.version}</version>        </dependency>        <properties>           <spring.version>4.2.2.RELEASE</spring.version>          </properties>

上一章通过ProxyFactory织入基于接口描述的切面,这里使用AspectJProxyFactory织入基于@AspectJ的切面。
在①设置了目标对象,在②添加了一个切面类,该类必须是带@Aspect注解的类,在③获取织入了切面的代理对象

输出
这里写图片描述
可以看出greetTo()方法已经被织入了切面类所定义的增强逻辑

如何通过配置使用@AspectJ切面

用配置方法完成切面织入工作

<?xml version="1.0" encoding="UTF-8" ?><beans xmlns="http://www.springframework.org/schema/beans"       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd">    <!-- ①目标Bean-->    <bean id="waiter" class="com.smart.aspectj.example.NaiveWaiter"/>    <!-- ②使用@AspectJ注解的切面类-->    <bean class="com.smart.aspectj.example.PreGreetingAspect"/>    <!-- ③自动代理创建器,自动将@AspectJ注解切面类织入目标Bean中-->    <bean class="org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator"/></beans>

AnnotationAwareAspectJAutoProxyCreator会自动将@AspectJ注解切面类自动织入目标Bean中

测试

public class AspectJProxyTest {    @Test    public void proxy(){        String config="com/smart/aspectj/example/beans.xml";        ApplicationContext ctx=new ClassPathXmlApplicationContext(config);        Waiter waiter=(Waiter)ctx.getBean("waiter");        waiter.greetTo("John");        waiter.serveTo("John");    }}

结果和上面一样

如果基于Schema的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:aop="http://www.springframework.org/schema/aop"       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd           http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">    <!--基于@AspectJ切面的驱动器"/-->    <aop:aspectj-autoproxy/>      <bean id="waiter" class="com.smart.NaiveWaiter" />    <bean class="com.smart.aspectj.example.PreGreetingAspect" /></beans>

首先在配置文件中引入aop命名空间;然后通过aop命名空间的<l;aop:aspectj-autoproxy/> 自动为Spring容器中那些匹配@AspectJ切面的Bean创建代理,完成切面织入。其内部依旧使用AnnotationAwareAspectJAutoProxyCreator进行自动代理的床架工作,内部细节被<l;aop:aspectj-autoproxy/> 隐藏起来

<l;aop:aspectj-autoproxy/> 有一个proxy-target-class属性,默认为false,表示使用JDK动态代理技术织入增强。当配置为<l;aop:aspectj-autoproxy proxy-target-class=”true”/>时,表示使用CGLib动态代理技术织入增强。如果设置为false,但目标类没有声明接口,将自动使用CGLib动态代理

@AspectJ语法基础

切点表达式函数

AspectJ5.0的切点表达式由关键字和操作参数组成,如”execution(* greetTo(..))”,execution为关键字,而”* greetTo(..)”为操作参数,execution代表目标类执行某个方法,而”* greetTo(..)”描述目标方法的匹配模式串,二者联合起来表示目标类greetTo()方法的连接点。将execution()称作函数,将”* greetTo(..)”称作函数的入参

Spring支持9个@AspectJ切点表达式函数,分为4种:
①方法切点函数:通过描述目标类方法的信息定义连接点
②方法入参切点函数:通过描述目标类方法入参的信息定义连接点
③目标类切点函数:通过描述目标类类型的信息定义连接点
④代理类切点函数:通过描述目标类的代理类的信息定义连接点

这里写图片描述

在函数入参中使用通配符

有些函数入参可以接受通配符,@AspectJ支持3种通配符
①*:匹配任意字符,但只能匹配上下文中的一个元素
②.. :匹配任意字符,可以匹配上下文中的多个元素,但在表示类时,必须和*联合使用,而在表示入参时单独使用
③+:表示按类型匹配指定类的所有类,必须在类名后面,如com.smart.Car+。继承或扩展指定类的所有类,还包括类本身

@AspectJ函数按其是否支持通配符及支持的程度,可以分为以下3类:
①支持所有通配符:execution()和within(),如within(com.smart.*),within(com.smart.service..*.*.Service+)等
②仅支持”+”通配符:args(),this()和target(),如args(com.smart.Waiter+),target(java.util.List+)
③不支持通配符:@args(),@within(),@target()和@annotation(),如args(com.smart.anno.NeedTest)

args(),this(),target(),@args(),@within(),@target()和@annotation()这7个函数除了可以指定类名外,还可以指定变量名,并将目标对象中的变量绑定到增强的方法中

逻辑运算符

切点表达式由切点函数组成,切点函数之间可以进行逻辑运算,组成复合切点

Spring支持以下切点运算符
这里写图片描述

在标准的@AspectJ中并不提供adn,or,not操作符,它们是Spirng为了在XML配置文件中方便定义切点表达式而特意添加的等价操作符。

注意在操作符的前后添加空格。
这里写图片描述

不同增强类型

@AspectJ为各种增强类提供了不同的注解类,位于org.aspectj.lang.annotation.*包中。这些注解的存留期限都是RetentionPolicy.RUNTIME,标注目标都是ElementType.METHOD

1.@Before
前置增强,相当于BeforeAdvice,有两个成员
①value:该成员用来定义切点
②argNames:由于无法通过Java反射机制获取方法入参名,所以如果在Java编译时未启动调试信息,或需要在运行期解析切点,就必须通过这个成员指定注解所标注增强方法的参数名(二者名字必须完全相同),多个参数名用逗号分隔

2.@AfterReturning
后置增强,相当于AfterReturningAdvice,有4个成员
①value:该成员用于定义切点
②pointcut:表示切点的信息。如果显式指定pointcut值,它将覆盖value的设置值,可以将pointcut看成value的同义词
③returning:将目标方法对象的返回值绑定给增强的方法
④argNames:由于无法通过Java反射机制获取方法入参名,所以如果在Java编译时未启动调试信息,或需要在运行期解析切点,就必须通过这个成员指定注解所标注增强方法的参数名(二者名字必须完全相同),多个参数名用逗号分隔

3.@Around
环绕增强,相当于MethodIntercept,有两个成员
①value:用于定义切点
②argNames:由于无法通过Java反射机制获取方法入参名,所以如果在Java编译时未启动调试信息,或需要在运行期解析切点,就必须通过这个成员指定注解所标注增强方法的参数名(二者名字必须完全相同),多个参数名用逗号分隔

4.@AfterThrowing
抛出增强,相当于ThrowsAdvice,有4个成员
①value:该成员用于定义切点
②pointcut:表示切点的信息。如果显式指定pointcut值,它将覆盖value的设置值,可以将pointcut看成value的同义词
③throwing:将抛出的异常绑定到增强方法中
④argNames:由于无法通过Java反射机制获取方法入参名,所以如果在Java编译时未启动调试信息,或需要在运行期解析切点,就必须通过这个成员指定注解所标注增强方法的参数名(二者名字必须完全相同),多个参数名用逗号分隔

5.@After
Final增强,不管是抛出异常还是正常退出,该增强都会得到执行。该增强没有对应的增强接口,可以把它看成ThrowsAdvice和AfterReturningAdvice的结合,一般用于释放资源,相当于try{}finally{}控制流,有两个成员
①value:用于定义切点
②argNames:由于无法通过Java反射机制获取方法入参名,所以如果在Java编译时未启动调试信息,或需要在运行期解析切点,就必须通过这个成员指定注解所标注增强方法的参数名(二者名字必须完全相同),多个参数名用逗号分隔

6.@DeclareParents
引介增强,相当于IntroductionInterceptor,有两个成员
①value:该成员用于定义切点,表示在哪个目标类上添加引介增强
②defaultImpl:默认的接口实现类

8.4.5 引介增强

这里写图片描述
如果希望NaiveWaiter能同时充当售货员的角色,可通过切面技术为NaiveWaiter新值Seller接口的实现。

@Aspectpublic class EnableSellerAspect {    //①value中,为NaiveWaiter添加接口实现    //②defaultImpl为默认的接口实现类    @DeclareParents(value="com.smart.NaiveWaiter", defaultImpl = SmartSeller.class)    public Seller seller;     //③要实现的目标接口}

在该切面中,通过 @DeclareParents为NaiveWaiter添加了一个需要实现的Seller接口,并指定其默认实现类为SmartSeller,然后通过切面技术将SmartSeller融合到NaiveWaiter中,这样NaiveWaiter就实现了Seller接口

在配置文件中配置好切面和NaiveWaiter Bean

<!--基于@AspectJ切面的驱动器"/-->    <aop:aspectj-autoproxy/>    <bean id="waiter" class="com.smart.NaiveWaiter" />    <bean class="com.smart.aspectj.basic.EnableSellerAspect" />

测试

public class DeclaredParentsTest {    @Test    public void parent() {        String configPath = "com/smart/aspectj/basic/beans.xml";        ApplicationContext ctx = new ClassPathXmlApplicationContext(configPath);        Waiter waiter = (Waiter)ctx.getBean("waiter");        waiter.greetTo("John");        Seller seller = (Seller)waiter;        seller.sell("Beer", "John");    }}

输出
这里写图片描述

成功实现了Seller接口

切点函数详解

本章用到的类
这里写图片描述
除了SmartSeller#showGoods()方法是protected外,其他的方法都是public

@annotation()

@annotation()属于方法切点函数,表示标注了某个注解的所有方法。

TestAspect定义了一个后置增强,该增强将应用到标注了NeedTest的目标方法中

@Aspectpublic class TestAspect {    @AfterReturning("@annotation(com.smart.anno.NeedTest)")  //① 后置增强切面    public void needTestFun(){        System.out.println("needTestFun() executed!");    }}

NaiveWaiter#greetTo()没有标注@NeedTest注解,而NaughtyWaiter#greetTo()方法标注了

public class NaughtyWaiter implements Waiter {    @NeedTest    public void greetTo(String clientName) {        System.out.println("NaughtyWaiter:greet to "+clientName+"...");    }       public void serveTo(String clientName){        System.out.println("NaughtyWaiter:serving "+clientName+"...");    }    public void joke(String clientName,int times){            System.out.println("NaughtyWaiter:play "+times+" jokes to "+clientName+"...");    }}

配置自动应用切面

    <aop:aspectj-autoproxy/>    <!--基于@AspectJ切面的驱动器"/-->    <bean id="naiveWaiter" class="com.smart.NaiveWaiter" />    <bean id="naughtyWaiter" class="com.smart.NaughtyWaiter"/>    <bean class="com.smart.aspectj.fun.TestAspect" />

测试

public class PointcutFunTest {    @Test    public void pointcut(){        String configPath="com/smart/aspectj/fun/beans.xml";        ApplicationContext ctx=new ClassPathXmlApplicationContext(configPath);        Waiter naiveWaiter=(Waiter)ctx.getBean("naiveWaiter");        Waiter naughtyWaiter=(Waiter)ctx.getBean("naughtyWaiter");        naiveWaiter.greetTo("John");    //该方法未被织入增强        naughtyWaiter.greetTo("John");  //该方法被织入增强    }}

输出
这里写图片描述
切面被正确织入NaughtyWaiter#greetTo()方法中

execution()

execution()属于方法切点函数

语法如下
这里写图片描述

①通过方法签名定义切点
这里写图片描述

②通过类定义切点
这里写图片描述

③通过类包另一切点
这里写图片描述

④通过方法入参定义切点
这里写图片描述

args()和@args()

二者属于方法入参切点函数
args()函数的入参是类名,而@args()函数的入参必须是注解类的类名。

1.args()
该函数接受一个类名,表示目标方法入参对象是指定类(包含子类时),切点匹配,如
args(com.smart.Waiter)
表示运行时入参是Waiter类型的方法,和execution(* *(com.smart.Waiter))的区别在于后者是针对类方法的签名而言,而前者针对运行时的入参类型而言

如args(com.smart.Waiter)既匹配addWaiter(Waiter waiter)又匹配addNaiveWaiter(NaiveWaiter naiveWaiter)
而execution(* *(com.smart.Waiter))只匹配addWaiter(Waiter waiter)

args(com.smart.Waiter)等价于execution(* *(com.smart.Waiter+))也等价于args(com.smart.Waiter+)

2.@args()
该函数接受一个注解类的类名,当方法的运行时入参对象标注了指定的注解时,匹配切点
这里写图片描述
T0.T1.T2.T3具有如图的继承关系,假设目标类方法的签名为fun(T1 t),它的入参为T1,而切面的切点定义为@args(M),T2类标注了@M,当fun(T1 t)的传入对象T2或T3时,方法匹配@args(M)所定义的切点

假设方法的签名为fun(T1 t),它的入参为T1,而标注@M的类为T0,当fun(T1 t)的传入T1,T2,T3的实例时,均不匹配

在类的继承树中,①处为方法签名中入参类型在类继承树中的位置,称为入参类型点,而②标注了@M注解的类在类继承树中的位置,称为注解点

这里写图片描述
①如果在继承类树中注解点②高于入参类型点①,则该目标方法不可能匹配切点@args(M)
②如果在继承树中注解点②低于入参类型点①,则注解点所在类及其子孙类为方法入参时,该方法匹配切点@args(M)

within()

属于目标类切点函数

通过类匹配模式串声明切点,within()函数定义的连接点是针对目标类而言的,而非针对运行期对象的类型而言,这和execution()是相同的。
within()所指定的连接点最小范围只能是类,而execution()所指定的连接点可以大到包,小到方法入参。所以可以认为execution()函数功能覆盖了within()函数的功能

within()函数的语法如下:
这里写图片描述

一些实例
这里写图片描述

@within()和@target()

属于目标类切点函数

二者只接受注解类名作为入参,其中@target(M)匹配任意标注了@M的目标类,而@withiin(M)匹配标注了@M的类及其子孙类

@target(M)切点匹配的匹配规则为
这里写图片描述

@withiin(M)切点匹配的规则为
这里写图片描述
注意,如果标注@M注解的是一个接口,则所有实现该接口的类并不匹配@within(M)。
这是因为@within(),@target()和@annotation()函数都是针对目标类而言,而非针对运行时的引用类型而言的

target()和this()

target()属于目标类切点函数,this()属于代理类切点函数

target()切点函数通过判断目标类是否按类型匹配指定类来决定连接点是否匹配,而this(0函数通过判断代理类是否按类型匹配指定类来决定是否和切点匹配

1.target()
target(M)表示如果目标类按类型匹配于M,则目标类的所有方法都匹配切点
例子:
这里写图片描述

2.this()
一般使用this()和target()来匹配定义切点,二者是等效的

二者的区别体现在通过引介切面产生代理对象时的具体表现
如8.4.5,通为NaiveWaiter引介一个Seller接口的实现,则this(com.smart.Seller)匹配JaiveWaiter代理对象的所有方法,包括NaiverWaiter本身的greetTo()和serveTo()方法,以及通过Seller接口引入的方法。而target(com.smart.Seller)不匹配通过引介切面产生的NaiveWaiter代理对象

EnableSellerAspect是为NaiveWaiter添加Seller接口实现的引介切面

@Aspectpublic class EnableSellerAspect {  //EnableSellerAspect是为NaiveWaiter添加Seller接口实现的引介切面    @DeclareParents(value="com.smart.NaiveWaiter",defaultImpl = SmartSeller.class)    public static Seller seller;}

TestAspect是通过判断运行代理对象所属类型来定义切点e的切面,如

@Aspectpublic class TestAspect {    //① 后置增强切面,织入任何运行期对象为Seller类型的Bean中    @AfterReturning("this(com.smart.Seller)")    public void thisTest(){        System.out.println("thisTest() executed!");    }}

配置这两个切面和NaiveWaiter

  <aop:aspectj-autoproxy/>    <!--基于@AspectJ切面的驱动器"/-->    <bean id="naiveWaiter" class="com.smart.NaiveWaiter" />    <bean class="com.smart.aspectj.fun.EnableSellerAspect"/>    <bean class="com.smart.aspectj.fun.TestAspect" />

EnableSellerAspect切面为NaiveWaiter引介Seller接口产生一个实现Seller接口的代理对象,TestAspect在判断出NaiveWaiter这个代理对象实现Seller接口后,就将其切面织入这个代理对象中。

测试

public class DeclaredParentsTest {    @Test    public void test(){        String configPath = "com/smart/aspectj/fun/beans.xml";        ApplicationContext ctx = new ClassPathXmlApplicationContext(configPath);        Waiter naiveWaiter=(Waiter)ctx.getBean("naiveWaiter");        naiveWaiter.greetTo("John");        naiveWaiter.serveTo("John");        ((Seller)naiveWaiter).sell("Beer","John");    }}

输出
这里写图片描述
可见代理对象的3个方法都织入了通过this()函数定义的切面

@AspectJ进阶

切点复合运算

@Aspectpublic class TestAspect {    @After("within(com.smart.*)&&execution(* greetTo(..))")   //①与运算    public void greeToFun(){        System.out.println("--greeToFun() executed!");    }    @Before("!target(com.smart.NaiveWaiter)&&execution(* serveTo(..))") //②非与运算    public void notServeInNaiveWaiter(){        System.out.println("--notServeInNaiveWaiter() executed!");    }    @AfterReturning("target(com.smart.Waiter||target(com.smart.Seller)")  //③或与运算    public void waiterOrSeller(){        System.out.println("--waiterOrSeller() executed!");    }}

命名切点

切点直接声明在增强方法处,这种切点声明方式称为匿名切点,匿名切点只能在声明处使用。
如果希望在其他地方重用一个切点,可以通过@Pointcut注解及切面类方法对切点进行命名

public class TestNamePointcut {    //①通过注解方法inPackage()对该切点进行命名,方法可视域修饰符为private,表明该命名切点只能在本切面类中使用    @Pointcut("within(com.smart.*)")     private void inPackage(){}    //②通过注解方法greetTo()对该切点进行命名,方法可视域修饰符为protected,表明该命名切点可以在当前包中的切面类,自切面类中使用    @Pointcut("execution(* greetTo(..))")    protected void greetTo(){}    //③引用命名切点定义的切点,本切点也是命名切点,可视域为public    @Pointcut("inPackage() and greetTo()")    public void inPkgGreetTo(){}}

定义了3个命名切点,命名切点的使用类方法作为切点的名称,此外方法的访问修饰符还控制了切点的可引用性,这种可引用性和类方法的可访问性相同,private的切点只能在本类中使用,public的切点可以在任何类中引用,protected的切点能在当前包中的切面类,子切面类中使用。
命名切点仅利用方法名及访问修饰符的信息。所以习惯上方法的返回类型为void,并且方法体为空。
这里写图片描述

③中inPkgGreetTo()切点引用了同类中的greetTo()切点,而inPkgGreetTo()切点可以被任何类引用,用户可以扩展TestNamePointcut,通过类的继承关系定义更多的切点

命名切点定义好后,可以在定义切面时通过名称引用切点

@Aspectpublic class TestAspect {    @Before("TestNamePointcut.inPkgGreetTo()")    public void pkgGreetTo(){        System.out.println("--pkgGreetTo() executed!--");    }    @Before("!target(com.smart.NaiveWaiter)&&TestNamePointcut.inPkgGreetTo()")    public void pkgGreetToNotNaiveWaiter(){        System.out.println("-- pkgGreetToNotNaiveWaiter executed!--");    }}

增强织入的顺序

一个连接点可以同时匹配多个切点,切点对应的增强在连接点上的织入顺序安排
①如果增强在同一个切面类中声明,则依照增强在切面类中定义的顺序进行织入
②如果增强在不同的切面类中,并且这些切面类都实现了org.springframework.core.Ordered接口,则由接口方法的顺序号决定(顺序号小的织入)
③如果增强在不同的切面类中,并且这些切面类没有实现org.springframework.core.Ordered接口,则织入的顺序是不确定的

访问连接点信息

AspectJ使用org.aspectj.lang.JoinPoint接口表示目标类连接点对象,如果是环绕增强,则使用rog.aspectj.lang.ProceedingJoinPoint表示连接点对象,该类是JoinPoint的子接口。
任何增强方法都可以通过将第一个入参声明为JoinPoint访问连接点上下文信息

1.JoinPoint
主要方法
这里写图片描述

2.ProceedingJoinPoint
继承于JoinPoint,新增了两个用于执行连接点方法的方法
这里写图片描述

@Aspectpublic class TestAspect {    @Around("execution(* greetTo(..))&&target(com.smart.NaiveWaiter)")   //①环绕增强    public void joinPointAccess(ProceedingJoinPoint pjp)throws Throwable{     //②声明连接点入参        System.out.println("-------joinPointAccess-------");        //③访问连接点信息        System.out.println("args[0]:"+pjp.getArgs()[0]);        System.out.println("signature:"+pjp.getTarget().getClass());        pjp.proceed();                //通过连接点执行目标对象方法        System.out.println("-------joinPointAccess------");    }}

在②增强方法的第一个入参声明为ProceedingJoinPoint类型(一定要在第一个位置)

配置

<aop:aspectj-autoproxy/>    <bean id="naiveWaiter" class="com.smart.NaiveWaiter" />    <bean id="test" class="com.smart.aspectj.advanced.TestAspect"/>

测试

public class AdvancedTest {    @Test    public void test(){        String configPath="com/smart/aspectj/advanced/beans.xml";        ApplicationContext ctx=new ClassPathXmlApplicationContext(configPath);        Waiter naiveWaiter=(Waiter)ctx.getBean("naiveWaiter");        naiveWaiter.greetTo("John");    }}

输出
这里写图片描述

绑定连接点方法入参

args(),this(),target(),@args(),@within(),@target()和@annotation()这7个函数除了可以指定类名外,还可以指定变量名,并将目标对象中的变量绑定到增强的方法中

其中,args()用于绑定连接点方法的入参;@annotation()用于绑定连接点方法的注解对象;而@args()用于绑定连接点方法入参的注解

这里写图片描述

在①处,通过args(name,num,..)进行连接点参数的绑定当args()函数入参为参数名时,包括两方面的信息
①连接点匹配规则信息:连接点方法的第一个入参是String类型,第二个入参是int类型
②连接点方法入参和增强方法入参的绑定:连接点的第一个入参绑定到增强方法的name参数上,第二个入参绑定到增强方法的num参数上

切点匹配和参数绑定的过程是这样的:
首先,args()根据参数名称爱增强方法中查找到名称相同的入参并获知对应的类型,这样就知道了匹配连接点方法的入参类型;
其次,连接点方法入参类型所在的位置则由参数名在args()函数中声明的位置决定。

args(name,num)匹配的目标类方法的第一个入参必须是String类型,第二个入参必须是int类型,如smile(String name,int times)匹配,而smile(int times,String name)不匹配
这里写图片描述

和args()一样,其他可以绑定连接点参数的切点函数(如@args()和target()等),当指定参数名时,就同时具有匹配切点和参数双重功能

配置

 <aop:aspectj-autoproxy proxy-target-class="true"/>    <bean id="naiveWaiter" class="com.smart.NaiveWaiter" />    <bean id="test" class="com.smart.aspectj.advanced.TestAspect"/>

<aop:aspectj-autoproxy proxy-target-class=”true”/>,启用CGLib动态代理,因为该实例需要对NaiveWaiter类进行代理(因为NaiveWaiter#smile()方法不是Waiter接口的方法),所以必须使用CGLib动态代理生成子类的代理方法。

public class AdvancedTest {    @Test    public void test(){        String configPath="com/smart/aspectj/advanced/beans.xml";        ApplicationContext ctx=new ClassPathXmlApplicationContext(configPath);        NaiveWaiter naiveWaiter=(NaiveWaiter)ctx.getBean("naiveWaiter");        naiveWaiter.smile("John",2);        naiveWaiter.smile2(2,"John");        naiveWaiter.greetTo("John");    }}

输出
这里写图片描述
可以看到,增强方法按预期绑定了NaiveWaiter.smile(String name,int times)方法的运行期入参,顺序或入参不对都不会被织入增强

绑定代理对象

使用this()或target()函数可绑定被代理对象实例,在通过类实例名绑定对象时,依然具有原来连接点匹配的功能,只不过类名是通过增强方法中同名入参的类型间接决定的、

演示this()
这里写图片描述
①处的切点表达式首先按类变量名查找②处增强方法的入参列表,进而获取类变量对应的类为com.smart.Waiter,这样就直到了切点的定义为this(com.smart.Waiter),即所有代理对象为Waiter类的所有方法匹配该切点,②的增强方法通过waiter入参绑定目标对象

测试

public class AdvancedTest {    @Test    public void test(){        String configPath="com/smart/aspectj/advanced/beans.xml";        ApplicationContext ctx=new ClassPathXmlApplicationContext(configPath);        NaiveWaiter naiveWaiter=(NaiveWaiter)ctx.getBean("naiveWaiter");        naiveWaiter.greetTo("John");    }}

配置

 <aop:aspectj-autoproxy proxy-target-class="true"/>    <bean id="naiveWaiter" class="com.smart.NaiveWaiter" />    <bean id="test" class="com.smart.aspectj.advanced.TestAspect"/>

<aop:aspectj-autoproxy proxy-target-class=”true”/>,启用CGLib动态代理,因为该实例需要对NaiveWaiter类进行代理(this是代理类切点函数)

输出
这里写图片描述

绑定类注解对象

@within()和@target()函数可以将目标类的注解对象绑定到增强方法中

演示within()

@Aspectpublic class TestAspect {    @Before("@within(m)")    public void bindTypeAnnoObject(Monitorable m){        System.out.println("-----bindTypeAnnoObject----");        System.out.println(m.getClass().getName());        System.out.println("-----bindTypeAnnoObject----");    }}
@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE)public @interface Monitorable {}

在NaiveWaiter类中标注了@Monitorable注解,所有的NaiveWaiter Bean都匹配切点,其Monitorable注解对象都绑定到增强方法中

@Monitorablepublic class NaiveWaiter implements Waiter {   ...}   

测试

public class AdvancedTest {    @Test    public void test(){        String configPath="com/smart/aspectj/advanced/beans.xml";        ApplicationContext ctx=new ClassPathXmlApplicationContext(configPath);        Waiter naiveWaiter=(Waiter)ctx.getBean("naiveWaiter");        ((NaiveWaiter)naiveWaiter).greetTo("John");        naiveWaiter.serveTo("Tom");    }}

输出为
这里写图片描述

此时配置为

 <aop:aspectj-autoproxy proxy-target-class="true"/>    <bean id="naiveWaiter" class="com.smart.NaiveWaiter" />    <bean id="test" class="com.smart.aspectj.advanced.TestAspect"/>

可以发现,使用CGLib代理NaiveWaiter时,其类的注解Monitorable对象也被代理了(而Monitorable没有实现接口,不能使用默认的JDK动态代理)

配置中去proxy-target-class=”true”,运行
((NaiveWaiter)naiveWaiter).greetTo(“John”);
会发现报错

绑定抛出的异常

连接点抛出的异常必须使用AfterThrowing注解的throwing成员进行绑定

@Aspectpublic class TestAspect {    @AfterThrowing(value = "target(com.smart.SmartSeller)",throwing = "iae") //①    public void bindException(IllegalArgumentException iae){ //②        System.out.println("-----bindException()----");        System.out.println("exception:"+iae.getMessage());        System.out.println("-----bindException()----");    }}

在①处throwing指定的异常名和②处的异常名相同,这个异常增强只在连接点抛出异常instanceof IllegalArgumentException时才匹配,增强方法可以通过iae参数可以访问抛出的异常对象

在SmartSeller中添加一个抛出异常的测试方法

public class SmartSeller implements Seller {    public int sell(String goods,String clientName) {        System.out.println("SmartSeller: sell "+goods +" to "+clientName+"...");        return 100;    }    public void checkBill(int billId){        if(billId == 1) throw new IllegalArgumentException("iae Exception");        else throw new RuntimeException("re Exception");    }}

当billId == 1时抛出IllegalArgumentException,否则抛出RuntimeException

测试

public class AdvancedTest {    @Test    public void test(){        String configPath="com/smart/aspectj/advanced/beans.xml";        ApplicationContext ctx=new ClassPathXmlApplicationContext(configPath);        SmartSeller seller=(SmartSeller)ctx.getBean("seller");        seller.checkBill(1);  //抛出IllegalArgumentException    }}

基于Schema配置切面

一个简单切面的配置

8-15
这里写图片描述

使用一个<aop:aspect>元素标签定义切面,其内部可以定义多个增强。在<aop:config>元素中可以定义多个切面。
在①,切面应用了adviceMethods Bean,该Bean是增强方法所在的类。通过<aop:before>声明了一个前置增强,并通过pointcut属性定义切点表达式,切点表达式的语法和@AspectJ中所用人的语法完全一样,由于&&在XML中使用不便,一般用and操作符代替。
③通过method属性指定增强的方法,该方法应该是adviceMethods Bean中的方法

<aop:config>拥有一个proxy-target-classs属性,当设置为true,表示其中声明的切面均使用CGLib动态代理技术,为false时使用Java动态代理技术。
一个配置文件可以同时定义多个<aop:config>

AdviceMethods是增强方法所在的类

public class AdviceMethods {    public void preGreeting(){        System.out.println("--how are you!--");    }}

测试

public class SchemaTest {    @Test    public void test(){        String configPath="com/smart/schema/beans.xml";        ApplicationContext ctx=new ClassPathXmlApplicationContext(configPath);        Waiter naiveWaiter=(Waiter)ctx.getBean("naiveWaiter");        Waiter naughtyWaiter=(Waiter)ctx.getBean("naughtyWaiter");        naiveWaiter.greetTo("John");        naughtyWaiter.greetTo("Tom");    }}

输出
这里写图片描述
可见切面被正确地织入目标Bean中

配置命名切点

在8-15的②处通过pointcut属性声明的切点是匿名切点,不能被其他增强或其他切面引用。

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:aop="http://www.springframework.org/schema/aop"       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd           http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd">    <aop:config proxy-target-class="true">        <aop:aspect ref="adviceMethods">            <aop:pointcut id="greetToPointcut" expression="target(com.smart.NaiveWaiter) and execution(* greetTo(..))"/>            <aop:before method="preGreeting" pointcut-ref="greetToPointcut"/>        </aop:aspect>    </aop:config>    <bean id="adviceMethods" class="com.smart.schema.AdviceMethods"/>    <bean id="naiveWaiter" class="com.smart.NaiveWaiter"/>    <bean id="naughtyWaiter" class="com.smart.NaughtyWaiter"/></beans>

在①处使用<aop:pointcut>定义了一个切点,并通过id属性进行了命名,在②处,通过pointcut-ref引用这个命名的切点。
和<aop:before>一样,除引介增强外,其他任何增强类的元素都有pointcut,pointcut-ref和method这三个属性。

<aop:pointcut>如果位于<aop:aspect>元素中,则命名切点只能被当前<aop:aspect>内定义的元素访问到。

为了能被整个<aop:config>元素中定义的增强访问,必须在<aop:config>元素下定义切点
这里写图片描述
如果在<aop:config>元素下指定定义<aop:pointcut>,<aop:pointcut>必须在<aop:aspect>之前定义。

配置顺序要求:首先是<aop:pointcut>,然后是<aop:advisor>,最后是<aop:aspect>。
在<aop:aspect>中配置的<aop:pointcut>没有先后顺序的要求
这里写图片描述

各种增强类型的配置

1.后置增强

通过<aop:after-returning>配置后置增强

<aop:config proxy-target-class="true">    <aop:aspect ref="adviceMethods">       <aop:after-returning method="afterReturning" pointcut="target(com.smart.SmartSeller)" returning="retVal"/>    </aop:aspect></aop:config>

returning属性必须和增强方法的入参名一致,后置增强对应的方法

public class AdviceMethods {    //后置增强对应方法    public void afterReturning(int retVal){       System.out.println("----afterReturning()----");       System.out.println("returnValue:"+retVal);       System.out.println("----afterReturning()----");    }}

如果增强方法不希望接收返回值,将配置处的returning属性和增强方法的对应入参去除即可

2.环绕增强
通过<aop:around>配置环绕增强
这里写图片描述

环绕增强对应的方法,可以将第一个入参声明为ProceedingJoinPoint

/环绕增强对应方法    public void aroundMethod(ProceedingJoinPoint pjp){       System.out.println("----aroundMethod()----");       System.out.println("args[0]:"+pjp.getArgs()[0]);       System.out.println("----aroundMethod()----");    }

pjp可以访问到环绕增强的连接点信息

3.抛出异常增强
通过<aop:after-throwing>匹配抛出异常的增强
这里写图片描述
通过throwing属性声明需要绑定的异常对象,指定的异常名必须和增强方法对应的入参名一致

//抛出异常增强    public void afterThrowingMethod(IllegalArgumentException iae){       System.out.println("----afterThrowingMethod()----");       System.out.println("exception msg:"+iae.getMessage());       System.out.println("----afterThrowingMethod()----");    }

4.Final增强
通过<aop:after>配置Final增强
这里写图片描述
对应的Final增强方法为:

//final增强    public void afterMethod(){       System.out.println("----afterMethod()----");    }

5.引介增强
通过<aop:declare-parents>配置引介增强。和其他增强不同,没有method,pointcut和pointcut-ref属性
这里写图片描述

<aop:declare-parents>通过implement-interface属性声明要实现的接口,通过default-impl属性指定默认的接口实现类,通过types-matching属性以AspectJ切点表达式语法指定哪些Bean需要引介Seller接口的实现。

可以通过下面的代码查看到NaiveWaiter已经实施了引介增强

public class SchemaTest {    @Test    public void test(){        String configPath="com/smart/schema/beans.xml";        ApplicationContext ctx=new ClassPathXmlApplicationContext(configPath);        Waiter naiveWaiter=(Waiter)ctx.getBean("naiveWaiter");        ((Seller)naiveWaiter).sell("Beer","John");    }}

虽然<aop:declare-parents>没有为method属性指定增强方法所在Bean,但<aop:aspect ref=”adviceMethods”>中的ref属性仍然要指定一个增强Bean

绑定连接点信息

基于Schema配置的增强方法绑定连接点信息和基于@AspectJ绑定连接点信息的方法相同
①所有增强类型对应的第一个入参可以声明为JoinPoint(环绕增强可以声明为ProceedingJoinPoint)访问连接点信息
②<aop:after-returning>(后置增强)可以通过returning属性绑定连接点方法的返回值,<aop:after-throwing>(抛出异常)可以通过throwing属性绑定连接点方法所抛出的异常
③所有增强类型都可以通过绑定参数的切点函数绑定连接点方法的入参

绑定连接点参数到增强方法

<aop:config proxy-target-class="true">    <aop:aspect ref="adviceMethods">        <aop:before method="bindParams"                    pointcut="target(com.smart.NaiveWaiter) and args(name,num,..)"/>    </aop:aspect> </aop:config>

在切点表达式中通过args(name,num,..)绑定了连接点的两个参数,对应的增强函数如下

 //------------绑定连接点参数----------//    public void bindParams(int num,String name){        System.out.println("----bindParams()----");        System.out.println("name:"+name);        System.out.println("num:"+num);        System.out.println("----bindParams()----");    }

bindParams(int num,String name)和切点函数args(name,num,..)中声明的参数名必须相同

测试

public class SchemaTest {    @Test    public void test(){        String configPath="com/smart/schema/beans.xml";        ApplicationContext ctx=new ClassPathXmlApplicationContext(configPath);        Waiter naiveWaiter=(Waiter)ctx.getBean("naiveWaiter");        ((NaiveWaiter)naiveWaiter).smile("John",2);    }}

输出
这里写图片描述

Advisor配置

Advisor是Spring中切面概念的对应物,是切点和增强的复合体,不过仅包含一个切点和一个增强。在AspectJ中没有对应的等价物,在aop schema配置样式中,可以通过<aop:advisor>配置一个Advisor。
通过advice-ref属性引用基于接口定义的增强,通过pointcut定义切点表达式,或通过pointcut-ref引用一个命名的切点。
这里写图片描述
在③处定义了前置增强Bean,在①引用这个增强,在②使用切点表达式配置切点

TestBeforeAdvice是一个实现了MethodBeforeAdvice接口的增强类

public class TestBeforeAdvice implements MethodBeforeAdvice {    public void before(Method method, Object[] args, Object target)            throws Throwable {        System.out.println("------TestBeforeAdvice------");        System.out.println("clientName:"+args[0]);        System.out.println("------TestBeforeAdvice------");    }}

混合切面类型

目前掌握了4中定义切面的方式:
①基于@AspectJ注解的方式
②基于<aop:aspect>的方式
③基于<aop:advisor>的方式
④基于Advisor类的方式

如果项目采用Java5.0以上,优先使用@AspectJ
如果只能用低版本的JDK,考虑使用<aop:aspect>
如果正在升级一个基于低版本Spring AOP开发的项目,则可以考虑使用<aop:advisor>复用已经存在的Advice类
如果项目只能用低版本的Spring,就只能使用Advisor了

混合使用各种切面类型

Spring提供了4中定义切面的方式,其底层的实现技术都是一样的,就是基于CGLib和JDK动态代理,所以在一个Spring项目中你可以混合使用Spring所提供的各种切面定义方式

这里写图片描述
但在实际中需要采用单一的实现方式,以保证技术的单一性

各种切面类型总结

切面不同定义方式具体实现比较
这里写图片描述
可以看出,<aop:advisor>其实是<aop:aspect>和Advisor的结合,它的切点表达方式和<aop:aspect>相同,增强定义方式和Advisor相同,连接点方法入参的绑定方式和Advisor一样,通过增强接口方法入参进行调用,所以<aop:advisor>在切点表达式内不能使用切点函数绑定连接点方法入参,否则会产生错误

在内部,Spring使用AspectJExpressionPointcut,为@AspectJ,<aop:aspect>及<aop:advisor>提供具体的切点实现

LTW(Load Time Weaving)

JVM Class文件字节码转换基础知识

我们目前所接触的AOP切面织入都是在运行期通过JDK或CGLib动态代理的方式实现的,除了运行期织入切面的方式外,还可以在类加载期通过字节码编辑技术将切面织入目标类中,这种织入方式被称为LTW(Load Time Weaving)

AspectJ LTW使用Java5.0提供的代理功能(agent)完成加载器切面织入工作。JDK的代理功能能够让访问器访问到JVM的底层部件,借此向JVM注册类文件转换器,在类加载时对类文件的字节码进行转换。
AspectJ LTW由于基于JDK动态代理技术工作,而JDK动态代理的作用范围是整个JVM,所以这种工作方式比较粗放,对于单一JVM多个应用的情况尤其不适合。

Spring 为LTW的过程提供了细粒度的控制,支持在单个ClassLoader范围内实施类文件转换,且配置更为简单

java.lang.instrument包的工作原理
该包中有两个能对JVM底层组件进行访问的类。就是通过JVM的java agent代理参数在启动时获取JVM内部组件的引用,以便在后续流程中使用。借助JDK动态代理,可以在JVM启动时装配并应用ClassTransformer,对类字节码进行转换,实现AOP的功能

该包中定义了两个重要的接口:
①ClassFileTransformer:Class文件转换器接口,有唯一的方法
这里写图片描述
该接口对Class文件的字节码进行转换,classfileBuffer是类文件对应的字节码数组,返回的byte[]为转换后的字节码。如果返回null,则表示不进行字节码处理(并非将类的字节码数据置空)
②Instrumentation:代表JVM内部的一个构件
可以通过该接口的方法向JVM内部“组件”注册一些ClassFileTransformer,注册转换器的接口方法为void addTransformet(ClassFileTransformer transformer)

当ClassFileTransformer实例注册到JVM后,JVM在加载Class文件时,会先调用这个ClassFileTransformer的transform()方法对Class文件的字节码进行转换。如果向JVM中注册多个ClassFileTransformer,它们将按注册的顺序组成链式的调用。这样ClassFileTransformer的实现者就可以从JVM层面截获所有类的字节码,并引入希望添加的逻辑。

下图描述了拥有多个转换器的JVM从加载类到最终生成对应的类字节码的过程
这里写图片描述

使用LTW织入切面

Spring 的LTW仅支持AspectJ定义的切面,既可以是直接采用AspectJ语法定义的切面,也可以是采用基于@AspectJ注解,通过Java类定义的切面、

Spring LTW直接采用了与AspectJ LTW相同的基础结果,即利用类路径下的META-INF/aop.xml配置文件找到切面定义及切面所要实现的候选目标类的信息,通过LoadTimeWeaver在ClassLoader加载类文件时将切面织入目标类中

工作原理如图
这里写图片描述
利用特定Web容器的ClassLoader,通过LoadTimeWeaver将Spring提供的ClassFileTransformer注册到ClassLoader中。在类加载器,注册的ClassFileTransformer读取AspectJ的配置文件,即类路径下的META-INF/aop.xml文件,获取切面,对加载到VM中的Bean类进行字节码转换,织入切面。Spring容器初始化Bean实例时,采用的Bean类就是已经被织入了切面的类

1.Spring 的LoadTimeWeaver
大多数的Web应用服务器(除Tomcat外)的ClassLoader都支持直接访问Instrument,无需通过javaagent参数指定代理,拥有这种能力的ClassLoader称为“组件使能(instrumentation-capable)”。通过组件使能,可以方便地访问ClassLoader的Instrument。
Spring利用Web应用服务器类加载器的这个特性,为它们分别提供了专门的LoadTimeWeaver,以便向特定的ClassLoader注册ClassFileTransformer,对类进行字节码转换,实现切面的织入。

Spring的org.springframework.instrument.classloading.LoadTimeWeaver接口规定额类加载器织入的高层协议,该接口有三个方法
这里写图片描述

Spring为LoadTimeWeaver提供了多个实现类
这里写图片描述
这里写图片描述

Spring只需在配置森件中添加一行配置就可以启动LoadTImeWeaver
这里写图片描述

2.在Tomcat下的配置
Spring专门为Tomcat提供了一个TomcatInstrumentableClassLoader,它扩展于Tomcat服务器的org.apache.catalina.loader.WebappClassLoader,并实现了LoadTimeWeaver要求的两个特殊方法

可以按照以下步骤使Tomcat Web应用启动SPring的LoadTimeWeaver
(1)在Spring配置文件中定义<context:load-time-weaver/>
(2)将org.springframework.instrument.tomcat-{version}.jar复制到<TOMCAT_HOME>/lib下,该JAR包中包含了TomcatInstrumentableClassLoader类
③编写以下配置片段,让Tomcat服务器使用TomcatInstrumentableClassLoader作为其ClassLoader
这里写图片描述

对于Tomcat6.x和Tomcat7.x来说,要使以上 配置片段生效,可以使用以下4种方式
这里写图片描述
前两种方式会产生全局,就是Tomcat应用中所有Web应用的类加载器都会调整为TomcatInstrumentableClassLoader。
后两种配置方式只会对相应的Web应用产生影响,其他的Web应用依旧使用Tomcat默认的WebappClassLoader

最后一种配置方式是最佳选择

阅读全文
0 0