Spring学习总结(三)- Spring的核心特性之面向切面编程(AOP)总结

在上一篇文章中介绍了如何使用依赖注入(DI)管理和配置应用对象。DI有助于应用对象之间的解耦,而AOP可以实现横切关注点与它们所影响的对象之间的解耦。

1、写在前面的话

前面有一堆理论,是从书中总结后加上自己的理解写的。如果没有基础的话,理解起来有点抽象。如果想要快速应用AOP的话,可以跳到后面示例部分,有不懂的地方再返回来看理论概念也是可以的。

想要了解Spring是如何实现切面的,就从AOP的基础知识开始吧。

2、什么是面向切面编程?

在软件开发中,散布于应用中多处的功能(例如日志、事务之类的功能会散布于应用中多处,而业务逻辑一般只会在它相应的模块中出现)被称为横切关注点(crosscuttingconcern)。通常来讲,这些横切关注点从概念上是与应用的业务逻辑相分离的(但是往往会直接嵌入到应用的业务逻辑之中)。把这些横切关注点与业务逻辑相分离正是面向切面编程(AOP)所要解决的问题。

横切关注点可以被模块化为特殊的类,这些类被称为切面(aspect)

3、定义AOP术语

在我们进入某个领域之前,必须学会在这个领域该如何说话。
为了理解AOP,我们必须了解AOP自己的术语。

我们在做一件事的时候,大概需要三个信息:做什么?什么时候做?在哪里做?。切面亦是如此。
在这里插入图片描述

3.1、通知(Advice)

在AOP术语中,切面的工作被称为通知。
通知定义了切面具体做什么工作以及什么时候做。也就是说,通知是包含了两件事:1、做什么 2、什么时候做

做什么工作很好理解,具体做什么工作也就是需要我们自己写代码的地方,比如记录个日志啊,做个计数啊啥的。

那么通知是怎样解决了何时执行这个工作的问题呢?
Spring切面可以应用5种类型的通知:

  • 前置通知(Before):在目标方法被调用之前调用通知功能;
  • 后置通知(After):在目标方法完成之后调用通知,此时不会关心方法的输出是什么;
  • 返回通知(After-returning):在目标方法成功执行之后调用通知;
  • 异常通知(After-throwing):在目标方法抛出异常后调用通知;
  • 环绕通知(Around):通知包裹了被通知的方法,在被通知的方法调用之前和调用之后执行自定义的行为。

3.2、连接点(Join point)

我们的程序可能也有数以千计的时机应用通知。这些时机被称为连接点。
连接点是在程序执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。

也就是说连接点就是程序中所有可应用通知的点,实际上我们不可能在所有的时间都插入切面,那么具体在什么地方插入,就是下面切点的概念。

3.3、切点(Poincut)

一个切面并不需要通知应用的所有连接点。切点有助于缩小切面所通知的连接点的范围。
如果说通知定义了切面的“什么”和“何时”的话,那么切点就定义了“何处”。

我们通常使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。

3.4、切面(Aspect)

切面是通知和切点的结合。
通知和切点共同定义了切面的全部内容——它是什么,在何时和何处完成其功能。

3.5、引入(Introduction)

引入允许我们向现有的类添加新方法或属性。可以在无需修改这些现有的类的情况下,让它们具有新的行为和状态。(如果不能理解得话没有关系,这里只是先认识认识它。后面根据实际的案例来深入理解)

3.6、织入(Weaving)

织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中
在目标对象的生命周期里有多个点可以进行织入:

  • 编译期:切面在目标类编译时被织入。这种方式需要特殊的编译器。AspectJ的织入编译器就是以这种方式织入切面的。
  • 类加载期:切面在目标类加载到JVM时被织入。这种方式需要特殊的类加载器(ClassLoader),它可以在目标类被引入应用之前增强该目标类的字节码。AspectJ
    5的加载时织入(load-timeweaving,LTW)就支持以这种方式织入切面。
  • 运行期:切面在应用运行的某个时刻被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态地创建一个代理对象。Spring
    AOP就是以这种方式织入切面的

回看上图,这张图能帮我们了解如下知识:

  • 通知包含了需要用于多个应用对象的横切行为;
  • 连接点通知包含了需要用于多个应用对象的横切行为,连接点是程序执行过程中能够应用通知的所有点;
  • 切点定义了通知被应用的具体位置(在哪些连接点)。

其中关键的概念是切点定义了哪些连接点会得到通知。

现在让我们再看看这些AOP的核心概念是如何在Spring中实现的。

4、AOP在Spring中的应用

Spring AOP构建在动态代理基础之上,因此,Spring对AOP的支持局限于方法拦截。

在开始之前,我们必须要了解Spring AOP框架的一些关键知识。

4.1、Spring在运行时通知对象

通过在代理类中包裹切面,Spring在运行期把切面织入到Spring管理的bean中。如下图所示,代理类封装了目标类,并拦截被通知方法的调用,再把调用转发给真正的目标bean。当代理拦截到方法调用时,在调用目标bean方法之前,会执行切面逻辑。

在这里插入图片描述

4.2、Spring只支持方法级别的连接点

因为Spring基于动态代理,所以Spring只支持方法连接点

5、AOP应用案例

今年天气很热,想喝杯果汁,于是就翻出新买的榨汁机,洗好了水果再将水果放入榨汁机。按下开关,现在只需等待即可。
如果一台榨汁机没有水果放进去,那是不是可以理解成这台榨汁机没有什么用处?换句话说,榨汁机是依赖于水果的。
再如果主人不去按下榨汁机的开关,榨汁机是不会运作的。从榨汁机的角度来看,主人按开关是非常重要的,但是对于榨汁机本身的功能来说,它(主人按下开关的动作)并不是核心,这是一个关注点。因此,将主人定义为一个切面,并且将其应用到榨汁机上就是比较明智的做法。
下面这个案例主要为了体现榨汁机的榨汁这个核心功能。

5.1、榨果汁基本功能

首先定义一个水果接口

/**
 * 水果接口
 */
public interface IFruits {

    /**
     * 获取果汁方法
     */
    void getJuice();

}

然后再写一个实现类,并且表明它是个组件类(使用@Component)。

/**
 * 苹果类,实现与水果(IFruits)接口,并实现其获取果汁的方法
 */
@Component
public class Apple implements IFruits {
    @Override
    public void getJuice() {
        System.out.println("Get some apple juice...");
    }
}

水果准备好了,就该准备榨汁机了。先写一个榨汁机接口,因为我有很多不同品牌的榨汁机。

/**
 * 榨汁机接口
 */
public interface IJuicer {

    /**
     * 榨汁的方法
     */
    void juicing(IFruits fruits);
}

这个榨汁的方法需要传入一个水果类进去,不然榨空气吗。

然后我再拿出我的某品牌榨汁机。

/**
 * 某品牌榨汁机,实现于榨汁机(IJuicer)接口,并实现其榨汁的方法
 */
@Component
public class JoyoungJuicer implements IJuicer{

    @Override
    public void juicing(IFruits fruits) {
        fruits.getJuice();
    }
}

然后还需要一个配置类,启动扫描。

@Configuration
@ComponentScan(basePackages = "com.juiceRelated")
public class JuicingConfig {

}

现在基本的榨汁功能已经可以使用了,我们来测试一下。
测试类

/**
 * 测试类
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = JuicingConfig.class)
public class JuicingTest {

    @Autowired
    private Apple apple;

    @Test
    public void beginJuicing(){
        AnnotationConfigApplicationContext applicationContext =
                new AnnotationConfigApplicationContext(JuicingConfig.class);
		
        JoyoungJuicer joyoungJuicer = (JoyoungJuicer) applicationContext.getBean("joyoungJuicer");
        joyoungJuicer.juicing(apple);
    }
}

打印结果:

...
Get some apple juice...
...

我这里获取榨汁机的方式是从上下文取的。当然也可以像Apple类那样注入进来,都是可以的。我这里演示两种获取Bean的方式,就这样写了。

现在基本的功能完成了,接下来就是AOP的应用。

5.2、将AOP应用到榨果汁功能上

我们新建一个Master类来充当切面,Master要做的事情就是洗苹果、喝果汁如果榨汁机有问题,就退换货。

/**
 * 主人类
 * 使用@Aspect注解表示这是一个切面
 */
@Aspect
@Component
public class Master {

    /**
     * 榨汁之前
     * 先洗水果
     */
    @Before("execution(* com.juiceRelated.IFruits.getJuice(..))")
    public void washFruits(){
        System.out.println("Wash the fruit and press the switch...");
    }

    /**
     * 榨汁之后
     * 喝果汁
     */
    @AfterReturning("execution(* com.juiceRelated.IFruits.getJuice(..))")
    public void drinkFruitJuice(){
        System.out.println("Drink fruit juice...");
    }

    /**
     * 榨汁之后
     * 榨汁机坏掉(抛异常了)
     */
    @AfterThrowing("execution(* com.juiceRelated.IFruits.getJuice(..))")
    public void juicerBroken(){
        System.out.println("Return of goods...");
    }

}

可以看到,这些方法都使用了通知注解来表明它们应该在什么时候调用。

AspectJ提供了五个注解来定义通知

  • @After 通知方法会在目标方法返回或抛出异常后调用
  • @AfterReturning 通知方法会在目标方法返回后调用
  • @AfterThrowing 通知方法会在目标方法抛出异常后调用
  • @Around 通知方法会将目标方法封装起来
  • @Before 通知方法会在目标方法调用之前执行

6、切点表达式

所有的这些注解都给定了一个切点表达式。这个表达式表明了你要在哪个方法或哪些方法织入切面。
方法表达式以“*”号开始,表明了我们不关心方法返回值的类型。然后,我们指定了全限定类名和方法名。对于方法参数列表,我们使用两个点号(…)表明切点要选择任意的 getJuice()方法,无论该方法的入参是什么。

在这里插入图片描述
这里表达式写死了方法名字叫做getJuice,如果你想要匹配IFruits下面的所有方法,只需要把getJuice改成“*”号就行了。

execution(* com.juiceRelated.IFruits.*(..))

注意看,所有的这些注解都给定了一个切点表达式作为它的值,同时,这四个方法的切点表达式都是相同的。
其实,它们可以设置成不同的切点表达式,但是在这里,这个切点表达式就能满足所有通知方法的需求。
相同的切点表达式我们重复了四遍。这样的重复让人感觉很低级。

如果我们只定义这个切点一次,然后每次需要的时候引用它,那不就很Nice吗。

@Pointcut注解能够在一个@AspectJ切面内定义可重用的切点

/**
 * 主人类
 * 使用@Aspect注解表示这是一个切面
 */
@Aspect
@Component
public class Master {

    @Pointcut("execution(* com.juiceRelated.IFruits.*(..))")
    public void juice(){}

    /**
     * 榨汁之前
     * 先洗水果
     */
    @Before("juice()")
    public void washFruits(){
        System.out.println("Wash the fruit and press the switch...");
    }

    /**
     * 榨汁之后
     * 喝果汁
     */
    @AfterReturning("juice()")
    public void drinkFruitJuice(){
        System.out.println("Drink fruit juice...");
    }

    /**
     * 榨汁之后
     * 榨汁机坏掉(抛异常了)
     */
    @AfterThrowing("juice()")
    public void juicerBroken(){
        System.out.println("Return of goods...");
    }

}

juice()方法的实际内容并不重要,在这里它实际上应该是空的。其实该方法本身只是一个标识,供@Pointcut注解依附。

需要注意的是, Master 类依然是一个POJO。我们能够像使用其他的Java类那样调用它的方法,它的方法也能够独立地进行单元测试,这与其他的Java类并没有什么区别。Master只是一个Java类,只不过它通过注解表明会作为切面使用而已。

我在 Master 类上面也加了@Component注解,表明它是一个组件类。如果就此止步的话,Master只会是Spring容器中的一个bean。即便使用了AspectJ注解,但它并不会被视为切面,这些注解不会解析,也不会创建将其转换为切面的代理。

这时可以在配置类上通过使用@EnableAspectJAutoProxy注解启用自动代理功能

@Configuration
@EnableAspectJAutoProxy
@ComponentScan(basePackages = "com.juiceRelated")
public class JuicingConfig {

}

现在切面相关的东西也写好了。马上就来测试一下。
不出意外的话,运行会报错:
org.springframework.beans.factory.NoSuchBeanDefinitionException: No matching bean of type [com.juiceRelated.Apple] found for dependency......
说是找不到Apple这个Bean

这是需要在Apple类上面加上这句话@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS),表明使用类代理。

@Component
@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)
public class Apple implements IFruits {
    @Override
    public void getJuice() {
        System.out.println("Get some apple juice...");
    }
}

再次运行测试类,打印结果:

Wash the fruit and press the switch...
Get some apple juice...
Drink fruit juice...

结果表明我们的切面应用成功了。

7、环绕通知

环绕通知与其他类型的通知有所不同,因此值得花点时间来介绍如何进行编写。

环绕通知是最为强大的通知类型。它能够让你所编写的逻辑将被通知的目标方法完全包装起来。实际上就像在一个通知方法中同时编写前置通知和后置通知。

为了阐述环绕通知,我们重写 Master 切面。这次,我们使用一个环绕通知来代替之前多个不同的前置通知和后置通知。

@Aspect
@Component
public class Master {

    @Pointcut("execution(* com.juiceRelated.IFruits.*(..))")
    public void juice(){}

    @Around("juice()")
    public void processFruit(ProceedingJoinPoint jp){
        try{
            System.out.println("Wash the fruit and press the switch...");
            jp.proceed();
            System.out.println("Drink fruit juice...");
        }catch (Throwable e){
            System.out.println("Return of goods...");
        }
    }
}

其他地方不需要改,运行测试类,打印结果:

Wash the fruit and press the switch...
Get some apple juice...
Drink fruit juice...

结果是一样的。

在这里,@Around注解表明 processFruit()方法会作为 juice()切点的环绕通知。在这个通知中,榨汁之前会清洗水果并按下榨汁机开关,榨汁结束后会喝果汁。像前面一样,如果榨汁失败的话,会要求退货。

关于这个新的通知方法,需要注意到的是它接受ProceedingJoinPoint作为参数。这个对象是必须要有的,因为你要在通知中通过它来调用被通知的方法。通知方法中可以做任何的事情,当要将控制权交给被通知的方法时,它需要调用ProceedingJoinPoint的proceed()方法。

特别注意的是,别忘记调用proceed()方法。如果不调这个方法的话,那么你的通知实际上会阻塞对被通知方法的调用。有可能这就是你想要的效果,但更多的情况是你希望在某个点上执行被通知的方法。


技 术 无 他, 唯 有 熟 尔。
知 其 然, 也 知 其 所 以 然。
踏 实 一 些, 不 要 着 急, 你 想 要 的 岁 月 都 会 给 你。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值