Spring AOP

AOP(Aspect Oriented Programming),即面向切面编程,官方的解释是:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。换一个相对好理解的说法,就是可以把程序中重复的代码(日志记录、事务管理等)抽取出来,在需要执行的时候,使用动态代理技术,在不修改源码的基础上去执行,实现对目标对象方法、参数的拦截,可在目标对象前后等位置添加上抽取出来的功能,实现其功能上的增强。以减少重复代码,提高开发效率,方便维护。

AOP 也是 Spring 框架中的主要内容,但 Spring AOP 只支持目标对象方法的拦截、增强,Spring AOP 常用的实现方式有基于XML和基于注解两种。在学习这两种方式前,我们有必要先了解 AOP 中的一些概念。

一、AOP 的相关概念

  • 连接点(JoinPoint):在Spring中代表类中的定义的方法
  • 切入点(Pointcut):指定要对哪些连接点(方法)进行拦截、增强,需要通过切入点表达式来指定哪些方法可以作为切入点。
  • 通知(Advice):拦截到切入点后要做的事情就是通知,即要增强什么功能
  • 目标对象(Target):要被代理的目标对象
  • 织入(Weaving):把通知应用到目标对象,来创建增强的代理对象的过程,若目标对象的类实现了接口,Spring 默认采用JDK动态代理实现织入,否则采用CGLIB动态代理
  • 代理对象(Proxy):通过织入产生的代理对象
  • 切面(Aspect):切入点和通知的结合称作切面

上边提到的通知按照用途分为以下几种:

  • 前置通知:在切入点方法之前执行
  • 后置通知:在切入点方法正常还执行完后执行,和异常通知只会执行其中一个
  • 异常通知:当切入点方法发生异常时执行,和后置通知只会执行其中一个
  • 最终通知:无论切入点方法是否发生异常都会执行,可以获取方法的返回值
  • 环绕通知:环绕通知更加灵活,可以不用配置上边四种通知来实现切入点方法的增强,让开发者通过编码的方式主动控制增强代码执行的时机。但这要求开发者必须主动调用切入点方法。Spring 提供了ProceedingJoinPoint接口,该接口可以作为环绕通知的方法参数,调用它的proceed()方法,就相当于调用切入点方法。

注意:环绕通知和前边四种通知不要一起使用。

还有一个知识点需要我们先了解下,那就是切入点表达式,先看一个切入点表达式:

execution(* com.shh.aop.CoffeeShop.sale(..))

它的作用就是匹配com.shh.aop包下,CoffeeShop类的sale()方法,实现拦截。先分析从这个切入点表达式可以看到的一些信息:

  • execution:使用该关键字定义切入点表达式
  • *:星号代表通配符,可以匹配返回值、包名、类名、方法
  • (..):方法名后边括号中的..表示方法的任意参数(包括无参),也可以显式的指定参数类型,例如int、java.lang.String
  • 切入点表达式中可以省略方法的权限修饰符

切入点表达式的定义很灵活,可以根据实际的需求变通,例如:

execution(* com.shh.aop.CoffeeShop.*(..))

表示会拦截com.shh.aop包下,CoffeeShop类的所有方法。

execution(* com.shh.aop.*.*(..))

表示会拦截com.shh.aop包下所有类的所有方法。

有了这些基础知识的铺垫,就更好理解后边的内容了。Spring AOP 使用的例子,会在之前 Java 动态代理 中例子的基础上扩展。

二、基于xml的AOP使用

示例代码要实现的功能大致是:有一个CoffeeShop实现类,其中sale()仅负责售卖咖啡,但我们希望增强sale()方法的功能,在sale()执行前先向客户问好,如果sale()正常执行结束则去提示用户付款,否则提示错误信息,最后向用户告别。

首先定义Shop接口:

public interface Shop {
    void sale(String name);
}

CoffeeShop类实现Shop接口,重写了要被拦截、增强的sale()方法,如果sale()方法接收到的参数为空,则直接抛出异常:

public class CoffeeShop implements Shop {
    public void sale(String name) {
        if (!StringUtils.isEmpty(name)) {
            System.out.println("开始制作" + name + "......制作完成!");
        } else {
            throw new RuntimeException();
        }
    }
}

这样被代理的类就定义好了,先放着后边再用。然后定义通知类,也就是要对CoffeeShop类的sale()方法增强哪些功能:

public class Greet {
    public void welcome() {
        System.out.println("前置通知:欢迎!");
    }

    public void cashier() {
        System.out.println("后置通知:请扫码付款!");
    }

    public void soldOut() {
        System.out.println("异常通知:商品名不能为空!");
    }

    public void goodbye() {
        System.out.println("最终通知:再见!");
    }
}

然后通过xml来配置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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--将CoffeeShop类交给IoC管理-->
    <bean id="coffeeShop" class="com.shh.aop.CoffeeShop"/>
    <!--将招呼的通知类交给IoC管理-->
    <bean id="greet" class="com.shh.aop.Greet"/>

    <!--Spring中基于xml的AOP配置-->
    <!--1.使用<aop:pointcut>定义切入点表达式,注意要定义在<aop:aspect>前,如果定义在<aop:aspect>标签里只能当前切面使用,不能公用-->
    <!--2.使用<aop:config>标签开始配置AOP-->
    <!--3.使用<aop:aspect>标签配置切面
          id:切面的唯一标识。
          ref:需要引用通知类的bean id。-->
    <!--4.使用<aop:before>标签配置前置通知,欢迎客户
              method:用Greet类的那个方法作为通知方法。
              pointcut-ref:配置切入点表达式的引用,指定要对CoffeeShop类中的那些方法使用前置通知,实现增强。
          使用<aop:after-returning>标签配置后置通知,提醒客户支付
          使用<aop:after-throwing>标签配置异常通知,商品信息有误时的处理
          使用<aop:after>标签配置最终通知,和客户告别-->
    <aop:config>
        <aop:pointcut id="sale" expression="execution(* com.shh.aop.CoffeeShop.sale(..))"/>
        <aop:aspect id="greetAdvice" ref="greet">
            <aop:before method="welcome" pointcut-ref="sale"/>
            <aop:after-returning method="cashier" pointcut-ref="sale"/>
            <aop:after-throwing method="soldOut" pointcut-ref="sale"/>
            <aop:after method="goodbye" pointcut-ref="sale"/>
        </aop:aspect>
    </aop:config>
</beans>

关键的说明信息都在注释里边了,到这里编码配置工作就结束了,接下来就是测试:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:aop.xml"})
public class AOPTest {
    @Autowired
    Shop coffeeShop;

    @Test
    public void saleTest() {
        coffeeShop.sale("拿铁");
    }
}

输出:


如果商品名为空:coffeeShop.sale(""),则会有异常,是这样的输出结果:

这也符合我们开始设定的场景,经过xml中的通知配置,额外的功能按约定好的规则自动添加到了要被增强的方法前后。可以看出后置通知和异常通知只会执行其中一个。

接下来使用环绕通知实现这个功能,首先修改通知类,只有一个greeting()方法:

public class Greet {
    /**
     * 环绕通知
     */
    public Object greeting(ProceedingJoinPoint pjp) {
        try {
            System.out.println("前置通知:欢迎!");
            // 获取切入点方法的参数
            Object[] params = pjp.getArgs();
            // 主动调用切入点方法(即执行sale()方法)
            Object result = pjp.proceed(params);
            System.out.println("后置通知:请扫码付款!");
            return result;
        } catch (Throwable throwable) {
            System.out.println("异常通知:商品名不能为空!");
            throw new RuntimeException(throwable);
        } finally {
            System.out.println("最终通知:再见!");
        }
    }
}

注意切入点方法的调用,即要增强的方法需要我们主动调用,然后需要我们在合适位置自行添加要增强的功能即可。

再修改xml配置文件,配置环绕通知:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--将CoffeeShop类交给IoC管理-->
    <bean id="coffeeShop" class="com.shh.aop.CoffeeShop"/>
    <!--将招呼的通知类交给IoC管理-->
    <bean id="greet" class="com.shh.aop.Greet"/>

    <!--Spring中基于xml的AOP配置-->
    <aop:config>
        <aop:pointcut id="sale" expression="execution(* com.shh.aop.CoffeeShop.sale(..))"/>
        <aop:aspect id="greetAdvice" ref="greet">
            <aop:around method="greeting" pointcut-ref="sale"/>
        </aop:aspect>
    </aop:config>
</beans>

同样可以实现上边的效果。

三、基于注解的AOP使用

使用注解配置时,就是要用对应的注解,替换掉之前xml中的配置:

<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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">
    <bean id="coffeeShop" class="com.shh.aop.CoffeeShop"/>
    <bean id="greet" class="com.shh.aop.Greet"/>
    <aop:config>
        <aop:pointcut id="sale" expression="execution(* com.shh.aop.CoffeeShop.sale(..))"/>
        <aop:aspect id="greetAdvice" ref="greet">
            <aop:before method="welcome" pointcut-ref="sale"/>
            <aop:after-returning method="cashier" pointcut-ref="sale"/>
            <aop:after-throwing method="soldOut" pointcut-ref="sale"/>
            <aop:after method="goodbye" pointcut-ref="sale"/>
        </aop:aspect>
    </aop:config>
</beans>
  • <bean>可以用@Component注解代替
  • <aop:aspect>可以用@Aspect注解代替,来配置切面类
  • <aop:pointcut>可以用@Pointcut注解代替,来配置切点
  • <aop:before><aop:after-returning><aop:after-throwing><aop:after><aop:around>分别对应@Before@AfterReturning@AfterThrowing@After@Around注解,来配置各种通知

修改CoffeeShop类,添加@Component

@Component
public class CoffeeShop implements Shop {
    ......
}

新建Greet2类,使用@Aspect@Component,即切面类:

@Aspect
public class Greet2 {
    /**
     * 定义切入点(使用切入点表达式)
     */
    @Pointcut("execution(* com.shh.aop.CoffeeShop.sale(..))")
    public void sale() {

    }

    /**
     * 定义切入点(使用注解,即要增强方法上使用的注解)
     */
    @Pointcut("annotation(注解名)")
    public void sale2() {

    }
    /**
     * 注解的参数为切入点的引用
     */
    @Before("sale()")
    public void welcome() {
        System.out.println("前置通知:欢迎!");
    }

    @AfterReturning(pointcut = "sale()", returning = "result")
    public void cashier(Object result) {
        System.out.println("后置通知:请扫码付款!");
    }

    @AfterThrowing(pointcut = "sale()", throwing = "e")
    public void soldOut(Exception e) {
        System.out.println("异常通知:商品名不能为空!");
    }

    @After("sale()")
    public void goodbye() {
        System.out.println("最终通知:再见!");
    }
}

到这里基于注解的AOP配置就基本完了,用一个切面类替换掉了之前xml中配置。

最后就是开启 Spring 对基于注解AOP的支持,以及创建IoC容器时要扫描的包,有两种方式可选:xml配置、java配置类。

xml配置方式如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <!--配置创建IoC容器时要扫描的包-->
    <context:component-scan base-package="com.shh.aop"/>
    <!--开启Spring对基于注解AOP的支持-->
    <aop:aspectj-autoproxy/>
</beans>

java配置类的方式如下:

@Configuration
@ComponentScan(basePackages = "com.shh.aop")
@EnableAspectJAutoProxy
public class AOPConfig {
}

根据自己的实际需求选择即可。

接下来测试一下:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {AOPConfig.class})
public class AOPTest {
    @Autowired
    Shop coffeeShop;

    @Test
    public void saleTest() {
        coffeeShop.sale("拿铁");
    }
}


如果商品名为空:coffeeShop.sale(""),则会有异常,是这样的输出结果:

注意,从图中可以看出,后置通知、异常通知始终是最后输出的,按照正常的逻辑应该是最终通知最后输出,而使用基于xml的AOP配置时确实正常的期望结果,这一点需要注意!!!

但是如果使用基于注解的环绕通知则不会用这样的问题,毕竟环绕通知更加灵活,切入点方法和增强内容的执行顺序可以由我们控制:

只需修改切面类,定义环绕通知的配置方法:

@Component
@Aspect
public class Greet2 {
    /**
     * 定义切入点
     */
    @Pointcut("execution(* com.shh.aop.CoffeeShop.sale(..))")
    public void sale() {

    }

    /**
     * 环绕通知
     */
    @Around("sale()")
    public Object greeting(ProceedingJoinPoint pjp) {
        try {
            System.out.println("前置通知:欢迎!");
            // 获取切入点方法的参数
            Object[] params = pjp.getArgs();
            // 主动调用切入点方法
            Object result = pjp.proceed(params);
            System.out.println("后置通知:请扫码付款!");
            return result;
        } catch (Throwable throwable) {
            System.out.println("异常通知:商品名不能为空!");
            throw new RuntimeException(throwable);
        } finally {
            System.out.println("最终通知:再见!");
        }
    }
}

测试结果如下:


关于 Spring AOP 的内容就先到这里了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,711评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,079评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,194评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,089评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,197评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,306评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,338评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,119评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,541评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,846评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,014评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,694评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,322评论 3 318
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,026评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,257评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,863评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,895评论 2 351