Java后端

Spring AOP

2020-01-13  本文已影响0人  SheHuan

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

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

一、AOP 的相关概念

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

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

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

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

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

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

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>

修改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() {

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

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

    @AfterThrowing("sale()")
    public void soldOut() {
        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 的内容就先到这里了。

上一篇下一篇

猜你喜欢

热点阅读