面向切面的 Spring —— 如何使用注解创建切面?

来源:互联网 发布:如何在淘宝上搜索店铺 编辑:程序博客网 时间:2024/05/20 08:00

使用注解来创建切面AspectJ 5 所引入的关键特性。

我们前一篇已经定义了 Performance 接口,它是切面中切点的目标对象

Q:如何定义切面?

A: 给出一个描述:如果一场演出没有观众的话,那就不能称之为演出。对不对?从演出的角度来看,观众是非常重要的,但是对演出本身的功能来将,它并不是核心,这是一个单独的关注点。因此,将观众定义为一个切面,并将其应用到演出上就是较为明智的做法。

之前需要在 build.gradle 导入 aspectjrt

// https://mvnrepository.com/artifact/org.aspectj/aspectjrtcompile group: 'org.aspectj', name: 'aspectjrt', version: '1.9.0.BETA-6'
package concert;import org.aspectj.lang.annotation.AfterReturning;import org.aspectj.lang.annotation.AfterThrowing;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;/** * 该类使用 @AspectJ 注解进行了标注。该注解表明该类不仅仅是一个 POJO,还是一个切面。 * 该类中的方法都使用注解来定义切面的具体行为。 */@Aspectpublic class Audience {    //表演之前:将手机调至静音状态    @Before("execution(**concert.Performance.perform(..))")    public void silenceCellPhones() {        System.out.println("Silencing cell phones");    }    //表演之前:就座    @Before("execution(**concert.Performance.perform(..))")    public void takeSeats() {        System.out.println("Taking seats");    }    //表演之后:精彩的话,观众应该会鼓掌喝彩    @AfterReturning("execution(**concert.Performance.perform(..))")    public void applause() {        System.out.println("CLAP CLAP CLAP!!!");    }    //表演失败之后:没有达到观众预期的话,观众会要求退款    @AfterThrowing("execution(**concert.Performance.perform(..))")    public void demandRefund() {        System.out.println("Demanding a refund");    }}

可以简化一下,使用 @Pointcut 注解。

package concert;import org.aspectj.lang.annotation.AfterReturning;import org.aspectj.lang.annotation.AfterThrowing;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.aspectj.lang.annotation.Pointcut;/** * 简化方案:@Pointcut 注解能够在一个 @AspectJ 切面内定义可重用的切点。 */@Aspectpublic class Audience {    //通过在该方法上添加 @Pointcut 注解,我们实际上扩展了切点表达式语言,    //这样就可以在任何的切点表达式中使用 performance()了。    //该方法内容并不重要,实际上应该是空的。只是一个标识,供 @Pointcut 注解依附。    @Pointcut("execution(**concert.Performance.perform(..))")    public void performance(){}    //表演之前:将手机调至静音状态    @Before("performance()")    public void silenceCellPhones() {        System.out.println("Silencing cell phones");    }    //表演之前:就座    @Before("performance()")    public void takeSeats() {        System.out.println("Taking seats");    }    //表演之后:精彩的话,观众应该会鼓掌喝彩    @AfterReturning("performance()")    public void applause() {        System.out.println("CLAP CLAP CLAP!!!");    }    //表演失败之后:没有达到观众预期的话,观众会要求退款    @AfterThrowing("performance()")    public void demandRefund() {        System.out.println("Demanding a refund");    }}

AspectJ 提供了五个注解来定义通知。
这里写图片描述

package concert;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.EnableAspectJAutoProxy;/** * 如果你使用 JavaConfig 的话,可以在配置类的类级别上通过使用 @EnableAspectJAutoProxy 注解启用自动代理功能。 */@Configuration@EnableAspectJAutoProxy //启用 AspectJ 自动代理@ComponentScanpublic class ConcertConfig {    @Bean    public Audience audience(){  // 声明 Audience bean        return new Audience();    }}

或者 XML 中配置:使用 Spring aop 命名空间中的 aop:aspectJ-autoproxy 元素。

<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"       xmlns:context="http://www.springframework.org/schema/context"       xmlns:aop="http://www.springframework.org/schema/aop"       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">    <context:component-scan base-package="concert"/>    <!-- 启用 AspectJ 自动代理 -->    <aop:aspectj-autoproxy/>    <!-- 声明 Audience bean -->    <bean class="concert.Audience"/></beans>

不管使用 JavaConfig 还是 XML,AspectJ 自动代理都会为使用 @Aspect 注解的 bean 创建一个代理,这个代理会围绕着所有该切面的切点所匹配的 bean。在这种情况下,将会为 Concert bean 创建一个代理,Audience 类中的通知方法将会在 perform() 调用前后执行。

需要记住的是:Spring 的 AspectJ 自动代理仅仅使用 @AspectJ 作为创建切面的指导,切面依然是基于代理的。本质上,它依然是 Spring 基于代理的切面。这就意味着,尽管使用的是 @AspectJ 注解,但我们仍然限于代理方法的调用。如果想利用 AspectJ 的所有能力,我们必须在运行时使用 AspectJ 并且不依赖于 Spring 来创建基于代理的切面

Q:如何创建环绕通知?

A: 环绕通知是最为强大的通知类型。实际上就像在一个通知方法中同时编写前置通知和后置通知。

package concert;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Pointcut;/** * 使用环绕通知重新实现 Audience 切面 * 这个通知所达到的效果跟之前的前置后置通知是一样的。但是,现在它们位于一个方法中。 */@Aspectpublic class Audience {    // 定义命名的切点    @Pointcut("execution(**concert.Performance.perform(..))")    public void performance(){}    /**     * 环绕通知方法     * @param joinPoint 这个对象是必须要有的,因为你要在通知中通过它来调用被通知的方法。     */    @Around("performance()")    public void watchPerformance(ProceedingJoinPoint joinPoint){        try {            System.out.println("Silencing cell phones");            System.out.println("Taking seats");            // 通知方法中可以做任何的事情,当要将控制权交给被通知的方法时,它需要调用该方法。            // 如果不调用这个方法的话,那么你的通知实际上会阻塞对被通知方法的调用。            // 有可能这就是你想要的效果,但更多的情况是你希望在某个点上执行被通知的方法。            joinPoint.proceed();            System.out.println("CLAP CLAP CLAP!!!");        } catch (Throwable throwable) {            System.out.println("Demanding a refund");        }    }}

对于 proceed() 方法,你也可以在通知中对它进行多次调用,这样做的一个场景就是实现重试逻辑,也就是在被通知方法失败后,进行重复尝试

Q:如何处理通知的参数?(切面能访问和使用传递给被通知方法的参数吗?)

A:如下列代码所示:BlankDisc.java & CompactDisc.java 参考 Spring 如何通过 XML 装配 bean?,并加以修改 play() 方法。

package concert;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.aspectj.lang.annotation.Pointcut;import java.util.HashMap;import java.util.Map;/** * 假设你想记录每个磁道被播放的次数。由于记录磁道的播放次数与播放本身是不同的关注点,因此应该是切面要完成的任务。 * 使用参数化的通知来记录磁道播放的次数 */@Aspectpublic class TrackCounter {    // 记录每个磁道播放次数    private Map<Integer, Integer> trackCounts = new HashMap<Integer, Integer>();    // 通知 play() 方法,在切点表达式中声明参数,这个参数传入到通知方法 countTrack() 中    // args(trackNumber) 表明传递给 play() 方法的 int 类型参数也会传递到通知中(countTrack()方法)去。    // 参数名称与切点方法签名中的参数相匹配。    @Pointcut("execution(* soundsystem.CompactDisc.play(int)) && args(trackNumber)")    public void trackPlayed(int trackNumber) {    }    // 在播放之前,为该磁道计数    // 这个通知方法是通过 @Before 注解和命名切点 trackPlayed(trackNumber) 定义的。    // 切点定义中的参数与切点方法中的参数名称是一样的,这样就完成了从命名切点到通知方法的参数转移。    @Before("trackPlayed(trackNumber)")    public void countTrack(int trackNumber) {        int currentCount = getPlauCount(trackNumber);        trackCounts.put(trackNumber, currentCount + 1);    }    public int getPlauCount(int trackNumber) {        return trackCounts.containsKey(trackNumber) ? trackCounts.get(trackNumber) : 0;    }}
package soundsystem;import concert.TrackCounter;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.EnableAspectJAutoProxy;import java.util.ArrayList;import java.util.List;/** * 在 Spring 配置中将 BlankDisc 和 TrackCounter 定义为 bean,并启用自动代理。 * 配置 TrackCount 记录每个磁道播放的次数 */@Configuration@EnableAspectJAutoProxy // 启用 AspectJ 自动代理public class TrackCounterConfig {    @Bean    public CompactDisc sgtPeppers(){        BlankDisc cd = new BlankDisc();        cd.setTitle("titleValue01");        cd.setArtist("ArtistValue01");        List<String> tracks = new ArrayList<String>();        tracks.add("trackValue01");        tracks.add("trackValue02");        tracks.add("trackValue03");        tracks.add("trackValue04");        tracks.add("trackValue05");        cd.setTracks(tracks);        return cd;    }    @Bean    public TrackCounter trackCounter(){        return new TrackCounter();    }}

Q:如何通过注解引入新功能?

A: Java 不是动态语言,一旦类编译完成了,我们就很难再为该类添加新功能了。但是我们可以使用切面,为对象拥有的方法添加了新功能,而没有为对象添加任何新的方法。实际上,利用被称为引入的 AOP 概念,切面可以为 Spring bean 添加新的方法

这里写图片描述

注意:当引入接口的方法被调用时,代理会把此调用委托给实现了新接口的某个其他对象。实际上,一个 bean 的实现被拆分到了多个类中

代码如下:

package concert;/** * 为所有的 Performance 实现引入下面的 Encoreable 接口 * * 需要将这个接口应用到 Performance 实现中。 * 两个问题:不能直接实现 Encoreable 接口,并不是所有的 Performance 都是具有 Encoreable 特性的; *           也有可能无法修改所有的 Performance 实现(使用第三方实现并且没有源码时)。 * 解决方案:借助于 AOP 的引入功能,我可以避免以上两个问题。 */public interface Encoreable {    void performEncore();}
package concert;/** * Encoreable 接口的实现类 */public class DefaultEncoreable implements Encoreable {    @Override    public void performEncore() {    }}
package concert;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.DeclareParents;/** * 创建一个切面 */@Aspectpublic class EncoreableIntroducer {    // 通过 @DeclareParents 注解,将 Encoreable 接口引入到 Performance bean 中。    // @DeclareParents 注解由三部分组成:    // ①、value 属性指定了哪种类型的 bean 要引入该接口。    //     此处 value 的值,代表所有实现 Performance 的类型。(标记符后面的 "+" 表示是 Performance 的所有子类型,而不是它本身)    // ②、defaultImpl 属性指定了为引入功能提供实现的类。这里,指定的是 DefaultEncoreable 提供实现。    // ③、@DeclareParents 注解所标注的静态属性指明了要引入的接口。这里,所引入的是 Encoreable 接口。    @DeclareParents(value = "concert.Performance+",defaultImpl = DefaultEncoreable.class)    public static Encoreable encoreable;}
    <!-- 将 EncoreableIntroducer 声明为一个 bean -->    <bean class="concert.EncoreableIntroducer"/>

Spring 的自动代理机制将会获取到它的声明,当 Spring 发现了一个 bean 使用了 @Aspect 注解时,Spring 就会创建一个代理,然后将调用委托给被代理的 bean被引入的实现,这取决于调用的方法属于被代理的 bean 还是属于被引入的接口

面向注解的切面声明有一个明显的劣势你必须能够为通知类添加注解。为了做到这一点,必须要有源码。

如果没有源码的话,或者不想将 AspectJ 注解放到你的代码之中,我们可以在 Spring XML 配置文件中声明切面。

上一篇: 面向切面的 Spring —— 如何通过切点来选择连接点?
下一篇:面向切面的 Spring —— 如何在 XML 中声明切面?

原创粉丝点击