Spring 5企业级开发实战
上QQ阅读APP看书,第一时间看更新

3.5 Spring集成AspectJ实战

本章3.4节中阐述了使用Spring的方式实现AOP编程,本节将以AspectJ相关注解的方式来实现AOP编程。

AspectJ是一个面向切面的框架,其可以生成遵循Java字节码规范的Class文件。

Spring AOP和AscpectJ之间的关系:Spring使用了和AspectJ一样的注解,并使用AspectJ来做切入点解析和匹配。但是Spring AOP运行时并不依赖于AspectJ的编译器或者织入器等特性。

3.5.1 使用AspectJ方式配置Spring AOP

本节将通过AspectJ的方式实现AOP编程,并通过案例阐述使用不同的AspectJ注解实现各种类型的通知。

在AspectJ中使用“@Aspect”注解来标示一个切面;使用“@Pointcut”注解标示切入点;各种通知类型通过“@Before(前置通知)”“@Around(环绕通知)”“@AfterReturning(后置通知)”和“@AfterThrowing(异常通知)”等注解来实现。

下面将通过案例阐述AspectJ的各种注解的使用。本例中有一个Person类,其包含一个说话方法say(),Person类的代码如下:

定义一个切面AllAspect,切面中实现各种通知,切面AllAspect的代码如下:

下面创建一个测试类,从Spring上下文中获取Person对象,并调用Person的say()方法。测试代码如下:

测试结果如下:

     around advice 1
     before advice
     Hello Spring 5
     around advice 2
     after advise
     afterReturning advise

测试结果与使用3.4节中Spring AOP的结果类似,此处不再阐述。

3.5.2 AspectJ各种切点指示器

Spring中支持若干个AspectJ切点指示器,它们用不同的方式描述目标类的连接点,表3-1所示是Spring中常见的几种AspectJ切点指示器。

表3-1 Spring中常见的AspectJ切点指示器

下面将通过案例讲解每一种指示器的使用和其对应的效果。

3.5.3 args()与“@args()”

args()匹配的是方法的入参类型,该函数接收一个类名,表示目标类方法入参对象是指定类(包含子类)时切点匹配。

比如args(com.test.Waiter)表示运行时入参是Waiter类型的方法,args与execution的区别在于execution是针对类方法的签名而言的,而args是针对运行时的入参类型而言。

“@args()”函数接收一个注解类的类名,当方法的运行时入参对象标注了指定的注解时,匹配切点。

下面通过案例阐述两者的使用。创建一个Factory接口,用FoodFactory和PhoneFactory两个类分别实现Factory。三者的代码如下。

Factory接口中定义了两个方法,做产品的make方法和运输产品的delivery方法。

FoodFactory实现Factory接口,并添加额外的testArgsAnnotation()方法,代码如下:

PhoneFactory实现Factory接口,重写make()和delivery()方法,代码如下:

创建一个监听功能的自定义注解“@Listen”,其目的是为了被“@args”匹配。自定义注解的实现如下:

另有如下两个类FreshFoodFactory和FrozenFoodFactory。FreshFoodFactory继承自FoodFactory,FrozenFoodFactory继承自FreshFoodFactory。其中需要注意的是,在FreshFoodFactory类上加上注解@Listen。

FreshFoodFactory继承自FoodFactory。

FrozenFoodFactory继承自FreshFoodFactory。

下面自定义一个ArgsAspect切面,其中前置增强匹配字符串类型的方法入参,后置增强匹配被“@Listen”标注的类。

用一个测试类AspectJExpressionDemo来验证args()和@args()函数。测试代码如下:

其中的配置文件spring-chapter3-aspectjargsexpression.xml的配置如下:

运行测试代码,得到的测试结果如下:

     args匹配方法入参是String的方法
     销售食品至上海
     -----分割线-----
     args匹配方法入参是String的方法
     运输手机至北京
     -----分割线-----
     @args匹配到方法执行了
     -----分割线-----
     @args匹配到方法执行了

从测试结果可以看出,args()匹配了FoodFactory和PhoneFactory类中的入参是String类型的delivery方法,“@args()”匹配到了FreshFoodFactory和FrozenFoodFactory类中的方法testArgsAnnotation(FreshFoodFactory freshFoodFactory)。

值得一提的是,本例中testArgsAnnotation(FreshFoodFactory freshFoodFactory)的方法签名为入参类型点,被“@Listen”注解标记的FreshFoodFactory称为注解点。按图3-14所示的从上到下的继承关系,当注解点“低于”入参类型点时,那么入参类型点的所有子孙类都可以被“@args()”匹配,否则将不会被“@args()”匹配。

图3-14 测试案例相关类继承结构图

如果修改此例中的“@Listen”注解点的位置到FoodFactory类上,那么“@args()”是没办法匹配到testArgsAnnotation()方法执行的。此时的FoodFactory代码如下:

删除FreshFoodFactory上的“@Listen”注解,此时的FreshFoodFactory代码如下:

再次执行测试代码,将会发现此时“@args()”注解匹配不到testArgsAnnotation()方法的执行,运行测试代码将得到如下结果:

     args匹配方法入参是String的方法
     销售食品至上海
     -----分割线-----
     args匹配方法入参是String的方法
     运输手机至北京
     -----分割线-----
     -----分割线-----

3.5.4 @annotation()

“@annotation”匹配被指定注解标记的所有方法。

新建一个自定义注解“@Log”表示用于记录日志,将“@Log”加在3.5.3节的案例中的PhoneFactory类的make()方法上。自定义注解“@Log”的代码如下:

被“@Log”注解后PhoneFactory的make()方法如下:

定义切面逻辑,使用“@annotation()”来为所有加了“@Log”注解的方法织入增强,定义的切面AnnotationAspect代码如下:

编写测试代码,并在测试代码中调用PhoneFactory的make()方法,观察make()方法是否被增强。测试代码如下:

配置文件spring-chapter3-aspectjannotationexpression.xml中的相关配置如下:

     <context:component-scan base-package="com.test.aspectj.expression"/>
     <bean id="annotationAspect"
     class="com.test.aspectj.expression.annotation.AnnotationAspect"/>
     <aop:aspectj-autoproxy/>

运行测试代码,测试结果如下:

     生产食品
     -----分割线-----
     生产手机
     打印日志

从测试结果可以证明,foodFactory对象中的make()方法因没有被“@Log”注解,因此没有被增强;phoneFactory中的make()方法加了“@Log”注解,所以在phoneFactory对象的make()方法执行后得到了增强,执行了切面AnnotationAspect中的log()方法。

3.5.5 execution

execution是最常用的切点函数,其具体语法如下:

     execution(<修饰符模式>?<返回类型模式><方法名模式>(<参数模式>)<异常模式>?)

execution是匹配某些类的某些方法执行的。下面定义一个切面ExecutionAspect使用execution函数,并匹配3.5.3节Factory中所有方法的执行,切面ExecutionAspect代码如下:

重点分析execution表达式的含义:

在表达式“* com.test.aspectj.expression.Factory.*(..)”中,第1个“*”表示任意的方法返回值类型,“com.test.aspectj.expression.Factory.*”表示Factory中的所有的方法,(..)表示任意类型参数且参数个数不限。因此整个表达式的含义就是匹配Factory中的任意返回值、任意入参的所有方法。

测试代码如下:

执行测试代码,得到测试结果如下:

     生产食品
     make方法执行了
     -----分割线-----
     生产手机
     make方法执行了

从测试结果可以看出,Factory中的make()方法执行后,都被增强了。

表达式的写法除了本例中的以外,还有很多不同的写法。下面详细介绍每种表达式写法的含义,如表3-2所示。

表3-2 execution表达式

3.5.6 target()与“@target()”

target()表示目标类型是指定的类型时,目标类型的所有方法都匹配到。target()可以匹配所有实现类及其子孙类中的所有方法。

“@target()”匹配标注了指定注解的类。

下面通过代码演示target()和“@target()”的使用。先创建一个注解“@Run”,代码如下:

再创建一个HuaweiPhoneFactory类,该类继承PhoneFactory,并用注解“@Run”标注HuaweiPhoneFactory类。

接着定义切面TargetAspect,该注解中分别使用target()和“@target()”函数,切面代码如下:

在测试代码中,调用HuaweiPhoneFactory类的make()方法,并观察输出结果:

配置文件spring-chapter3-aspectjtargetexpression.xml中的主要配置如下:

执行测试代码,得到如下的测试结果:

     target匹配到,方法执行前增强
     生产手机
     @target匹配到,方法执行后增强

从测试结果可以证明,target(com.test.aspectj.expression.PhoneFactory)匹配到了其子类HuaweiPhoneFactory的make()方法的执行,@target(com.test.aspectj.expression.target.Run)匹配到了已加注解“@Run”的类HuaweiPhoneFactory。

3.5.7 this()

this()与target()几乎是等效的,两者在引介切面的场景下略有差别。下面通过案例分析两者的区别。

创建一个接口Listener,在接口中定义一个监听方法listen(),Listener接口如下:

创建一个Listener接口的实现类DefaultListener,其中重写了Listener接口的listen()方法,DefaultListener类的实现如下:

定义引介切面ListenerAspect,其为FoodFactory植入Listener接口,ListenerAspect的实现如下:

除了上面的引介切面外,还需要一个切面ThisAspect,这个切面中分别使用this()和target()函数,ThisAspect的实现如下:

创建测试类,观察this()和target()函数的区别,测试代码如下:

配置文件spring-chapter3-aspectjthisexpression.xml主要配置如下:

运行测试代码,测试结果如下:

从测试结果可以看到,在调用make()方法时,ThisAspect类中的before()方法并未执行,即target没有匹配到make()方法的执行;当调用listen()方法时,ThisAspect类中的before()方法执行了。

可以得出以下结论。

this(com.test.aspectj.expression.thisexpression.Listener)不仅可以匹配Listener接口中定义的方法,而且还可以匹配FoodFactory中的方法;target(com.test.aspectj.expression.thisexpression.Listener)仅仅匹配Listener中定义的方法。

3.5.8 within()与“@within()”

within()与execution()的功能类似,两者的区别是,within()定义的连接点的最小范围是类级别的,而execution()定义的连接点的最小范围可以精确到方法的入参,因此可以认为execution()涵盖了within()的功能。

“@within()”匹配标注了指定注解的类及其子孙类。

下面通过案例阐述within()和“@within()”的使用。

首先创建一个表示监控的注解“@Monitor”,代码如下:

下面修改PhoneFactory,在其中加入testWithin()方法,修改后的PhoneFactory代码如下:

接着创建MobilePhoneFactory类,使其继承PhoneFactory,并重写父类PhoneFactory中的testWithin()方法,并使用“@Monitor”注解标注:

下面创建IPhoneFactory类继承PhoneFactory,代码如下:

下面创建切面类WithinAspect,要分别使用within()和“@within()”,代码如下:

测试代码中分别调用了FoodFactory和PhoneFactory的make()方法用以验证within(),分别调用IPhoneFactory和IPhoneFactory的testWithin()方法用以验证“@within()”。测试代码如下:

执行测试代码,测试结果如下:

     方法执行前增强
     生产食品
     -----分割线-----
     @within匹配到,执行增强
     -----分割线-----
     @within匹配到,执行增强

从测试结果可以证明,within(com.test.aspectj.expression.FoodFactory)匹配到了FoodFactory类的make()方法的执行;@within(com.test.aspectj.expression.within.Monitor)不仅匹配到了被@Monitor标注的类MobilePhoneFactory,而且还匹配到了MobilePhoneFactory的子类IPhoneFactory。