Spring 框架学习9 - Spring 中的 AOP

Spring 中的 AOP

1. Spring 中 AOP 的细节

1.1 AOP 相关术语

  • Joinpoint (连接点):

    • 所谓连接点是指那些被拦截到的点。在 Spring 中,这些点指的是方法,因为 spring 只支持方法类型的连接点。
      • 比如之前写的小例子中的业务层的 dao 接口中的所有方法,就是jointpoint,因为这些方法是连接业务和增强方法之间的桥梁
  • Pointcut(切入点):

    • 所谓切入点是指我们要对哪些 Jointpoint 进行拦截的定义。
      • 还是之前写的小例子,如果业务层中存在一种方法,并不需要被代理类增强方法,那么这个方法便不是切入点。那么这个方法是不是连接点?一定是。
      • 所以,所有的切入点都是连接点,而连接点,不一定都是切入点。
  • Advice(通知/增强):

    • 所谓通知是指拦截到 Jointpoint 之后所要做的事情就是通知。
      • 动态代理类中的 invoke 方法具有拦截方法的功能,在拦截方法之后,需要对方法进行增强,那么对方法的增强,就称为通知。
    • 通知的类型:前置通知,后置通知,异常通知,最终通知,环绕通知。
      • 在动态代理中,整个 invoke 方法就是环绕通知,在环绕通知中,有明确的切入点方法调用
      • 在环绕通知中,找到对切入点方法调用(增强):
        • 在该方法前,是前置通知
        • 在该方法后,是后置通知
        • 在对异常的处理中,是异常通知
        • 在 finally 语句块中,是最终通知
      • 通知方法,就是我们用来增强目标对象的类中的方法。
  • Introduction(引介):

    • 引介是一种特殊的通知,在不修改类代码的前提下,Introduction 可以在运行期为类动态地添加一些方法或 Field。
  • Target(目标对象):

    • 代理的目标对象
  • Weaving(织入):

    • 是指把增强应用到目标对象来创建新的代理类对象的过程。
      • 我们在新增业务层的事务的支持的时候,采用动态代理的方式。在代理过程中,对业务新增了事务的支持,这个过程,就叫做织入。
    • spring 采用动态代理织入,而 AspectJ 采用编译期织入和类装载期织入
  • Proxy(切面):

    • 一个类被 AOP 织入增强后,就产生一个结果代理类。
  • Aspect

    • 是切入点和通知(引介)的结合。
      • 通知方法和切入点之间的关系,通知方法何时执行,怎样执行,如何去配置他们之间的关系,而配置好的他们之间的关系,就叫做切面。

1.2 学习 spring 中的 AOP 要明确的事

  • 开发阶段(我们做的)
    • 编写核心业务代码(开发主线):大部分程序员来做,要求熟悉业务需求。
    • 把公共代码抽取出来,制成通知(开发阶段最后再做):团队中拥有技术比较牛的人员来做。
    • 在配置文件中,声明切入点与通知间的关系,即切面:团队中拥有技术比较牛的人员来做。
  • 运行阶段(Spring 框架完成的)
    • Spring 框架监控切入点方法的执行。一旦监控到切入点方法被运行,使用代理机制,动态创建目标对象的代理对象,根据通知类别,在代理对象的对应位置,将通知对象的功能织入,完成完整的代码逻辑运行。

2. spring 基于 XML 的 AOP

2.1 前提代码:

  1. 创建一个新的 maven 工程。

  2. 在maven工程下,创建以下几个源包:

    在这里插入图片描述

  3. 创建以下几个类:

    在这里插入图片描述

  4. IAccountService 类,用来模拟业务层:

    package com.self.learning.service;
    
    /**
     * 账户的业务层接口
     */
    public interface IAccountService {
    
        /**
         * 模拟保存账户, 无返回值无参
         */
        void saveAccount();
    
        /**
         * 模拟更新账户, 无返回值有参
         * @param i
         */
        void updateAccount(int i);
    
        /**
         * 模拟删除账户, 有返回值无参
         * @return
         */
        int deleteAccount();
    
    }
    
    
  5. AccountServiceImpl 类,用来模拟业务层的实现类:

    package com.self.learning.service.impl;
    
    import com.self.learning.service.IAccountService;
    
    /**
     * 账户的业务层实现类
     */
    public class AccountServiceImpl implements IAccountService {
        @Override
        public void saveAccount() {
            System.out.println("执行了保存");
        }
    
        @Override
        public void updateAccount(int i) {
            System.out.println("执行了更新");
        }
    
        @Override
        public int deleteAccount() {
            System.out.println("执行了删除");
            return 0;
        }
    }
    
  6. 模拟一个业务层的日志文件:

    package com.self.learning.utils;
    
    /**
     * 由于记录日志的工具类,它里面提供了公共的代码
     */
    public class Logger {
    
        /**
         * 用于打印日志:计划让其在切入点方法在执行前执行(切入点方法就是业务层方法)
         */
        public void printLog(){
            System.out.println("Logger 类中的 pringLog 方法开始执行日志了。");
        }
    
    }
    
  7. 测试类,使用 Spring 整合了 junit4,我使用的Spring的版本是Spring5,因此要求junit的版本大于 4.12:

    package com.self.learning.spring.test;
    
    import com.self.learning.service.IAccountService;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
    
    /**
     * 测试 AOP 的配置
     */
    @RunWith(value = SpringJUnit4ClassRunner.class)
    @ContextConfiguration(locations = "classpath:bean.xml")
    public class AOPTest {
    
        @Autowired
        IAccountService accountService;
    
        @Test
        public void test() {
            accountService.saveAccount();
            accountService.updateAccount(4);
            accountService.deleteAccount();
        }
    
    }
    

2.2 Spring 中基于 XML 的 AOP 配置步骤

  1. 把通知的 bean 也交给 Spring 来管理

  2. 使用 aop:config 标签表名开始 AOP 配置

  3. 使用 aop:aspect 标签名配置切面

    • id属性:是给切面提供一个唯一标识
    • ref属性:是指定通知类 bean 的 Id。
  4. 在 aop:aspect 标签的内部使用对应的标签来配置通知的类型。

    • aop:before 标签:

      • 表示配置前置通知

      • method 属性:用于指定 Logger 类中,哪个方法是前置通知

      • pointcut属性:用于指定切入点表达式,该表达式的含义是对业务层中哪些方法增强。

      • 切入点表达式的写法:

        • 关键字:execution(表达式)

        • 表达式:

          • 访问修饰符 返回值 包名.包名.包名…类名.方法名(参数列表)

          • 标准的表达式写法:

            public void com.self.learning.service.impl.AccountServiceImpl.saveAccount()
            
          • 访问修饰符可以省略:

            void com.self.learning.service.impl.AccountServiceImpl.saveAccount()
            
          • 返回值可以使用通配符表示任意返回值:

            * com.self.learning.service.impl.AccountServiceImpl.saveAccount()
            
          • 包名可以使用通配符,表示任意包。但是有几级包,就需要写几个 *:

            * *.*.*.*.*.AccountServiceImpl.saveAccount()
            
            
          • 包名可以使用 … 表示当前包和子包:

            * *..AccountServiceImpl.saveAccount()
            
          • 类名和方法名都可以使用 * 来实现通配:

            * *..*.*()
            
          • 参数列表可以直接写数据类型:

            • 基本类型直接写名称,

            • 引用类型写包名.类名的方式:java.lang.String

              * *..*.*(int)
              
            • 类型可以使用通配符表示任意类型,但是必须要有参数

            • 可以使用 … 表示有无参数均可,有参数可以是任意类型

          • 全通配写法 —— 实际开发不适用,因为类都符合这种通配类型,在运行时,代码都会被增强一次:

            * *..*.*(..)
            
          • 实际开发中切入点表达式的通常写法:

            切到业务层实现类下的所有的方法:

            * com.self.learning.service.impl.*.*(..)
            

2.3 Spring 配置文件的配置工作:

我这里的 Spring 的配置文件名字是 bean.xml。

首先,以下是 bean 的约束:

<?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">
</beans>
  1. 首先是配置 Spring 的 IOC,把 Service 对象配置进来:

        <bean id="accountService" class="com.self.learning.service.impl.AccountServiceImpl"></bean>
    
    
  2. 然后是,配置 Logger 类:

        <bean id="logger" class="com.self.learning.utils.Logger"></bean>
    
    
  3. 接下来是配置 AOP:

        <!-- 配置 AOP -->
        <aop:config>
            <!-- 配置切面 -->
            <aop:aspect id="logAdvice" ref="logger">
                <!-- 配置通知类型,并且建立通知方法和切入点方法的关联 -->
                <aop:before method="printLog" pointcut="execution(public void com.self.learning.service.impl.AccountServiceImpl.saveAccount())"></aop:before>
            </aop:aspect>
        </aop:config>
    
  4. 最后,在测试类中运行,以下是结果

    Logger 类中的 pringLog 方法开始执行日志了。
    执行了保存
    Logger 类中的 pringLog 方法开始执行日志了。
    执行了更新
    Logger 类中的 pringLog 方法开始执行日志了。
    执行了删除
    

2.4 四种常见的通知类型和通用化表达式

四种常见的通知类型明别为:

前置通知,后置通知,异常通知,最终通知:

package com.self.learning.utils;

/**
 * 由于记录日志的工具类,它里面提供了公共的代码
 */
public class Logger {

    /**
     * 前置通知:在切入点方法执行之前执行
     */
    public void beforePrintLog(){
        System.out.println("前置通知 Logger 类中的 beforePrintLog 方法开始执行日志了。");
    }

    /**
     * 后置通知:在切入点方法正常执行之后执行。它和异常通知永远只能执行一个。
     */
    public void afterReturningPrintLog(){
        System.out.println("后置通知 Logger 类中的 afterReturningPrintLog 方法开始执行日志了。");
    }

    /**
     * 异常通知:在切入点方法执行产生异常之后,执行。它和后置通知永远只能执行一个。
     */
    public void afterThrowingLog(){
        System.out.println("异常通知 Logger 类中的 afterThrowingLog 方法开始执行日志了。");
    }

    /**
     * 最终通知:无论切入点方法是否正常执行,都会在其后执行
     */
    public void afterPringLog(){
        System.out.println("最终通知 Logger 类中的 afterPringLog 方法开始执行日志了。");
    }

}

其中:

  • 前置通知:在切入点方法执行之前执行
  • 后置通知:在切入点方法正常执行之后执行。它和异常通知永远只能执行一个。
  • 异常通知:在切入点方法执行产生异常之后,执行。它和后置通知永远只能执行一个。
  • 最终通知:无论切入点方法是否正常执行,都会在其后执行

在 Spring 配置文件中的配置为:

    <!-- 配置 AOP -->
    <aop:config>
        <!-- 配置切入点表达式 id 属性用于指定表达式的唯一标志, expression 用于指定表达式内容
            -->
        <aop:pointcut id="pt1" expression="execution(* com.self.learning.service.impl.*.*(..))"/>
        <!-- 配置切面 -->
        <aop:aspect id="logAdvice" ref="logger">
            <!-- 配置前置通知 -->
            <aop:before method="beforePrintLog" pointcut-ref="pt1"></aop:before>

            <!-- 配置后置通知 -->
            <aop:after-returning method="afterReturningPrintLog" pointcut-ref="pt1"></aop:after-returning>

            <!-- 配置异常通知 -->
            <aop:after-throwing method="afterThrowingLog" pointcut-ref="pt1"></aop:after-throwing>

            <!-- 配置最终通知 -->
            <aop:after method="afterPringLog" pointcut-ref="pt1"></aop:after>
        </aop:aspect>
    </aop:config>

其中,asp:pointcut 为通用化表达式,用来替换 execute 表达式。

以下是运行结果(同样只是运行 accountService 方法):

前置通知 Logger 类中的 beforePrintLog 方法开始执行日志了。
执行了保存
后置通知 Logger 类中的 afterReturningPrintLog 方法开始执行日志了。
最终通知 Logger 类中的 afterPringLog 方法开始执行日志了。

这里要注意:

aop:pointcut 可以写在 aop:aspect 内部,也可以写在 aop:aspect 外部。

如果写在内部,只能当前的 aop:aspect 标签使用。如果写在外部,根据约束,只能写在 aop:config 标签内部,同时写在 aop:aspect 标签前面

2.5 环绕通知

首先,创建一个环绕通知:

public voidaroundLog(ProceedingJoinPoint pjp) {
		System.out.println("Logger 类中的 aroundPringLog 方法开始执行日志了。");	
    }

接下来在配置文件中进行配置:

        <aop:pointcut id="pt1" expression="execution(* com.self.learning.service.impl.*.*(..))"/>
            <!-- 配置环绕通知 -->
            <aop:around method="aroundLog" pointcut-ref="pt1"></aop:around>
        </aop:aspect>
    </aop:config>

然后,在测试类中进行测试:

import com.self.learning.service.IAccountService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

/**
 * 测试 AOP 的配置
 */
@RunWith(value = SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:bean.xml")
public class AOPTest {

    @Autowired
    IAccountService accountService;

    @Test
    public void test() {
        accountService.saveAccount();
    }

}

结果为:

Logger 类中的 aroundPringLog 方法开始执行日志了。

发现并不会打印:执行了保存。也就是并没有执行业务层的代码

这是什么原因呢?

我们都知道 Spring 中的方法增强实际上就是使用代理类,看一下下面的图:

在这里插入图片描述

我们会发现,实际上,我们的环绕方法中,缺少一个明确的切入点方法。

这个问题,是通过 Spring 框架为我们提供的一个接口来解决的,这个接口是 ProceedingJoinPoint,这个接口中有一个方法 proceed,此方法就相当于明确调用切入点方法。

所以,我们的环绕通知方法应该改成这个样子:

    public Object aroundLog(ProceedingJoinPoint pjp) {
        Object rtValue = null;
        try {
            Object[] args = pjp.getArgs(); // 得到方法执行所需的参数
            System.out.println("Logger 类中的 aroundPringLog 方法开始执行日志了。前置");
            rtValue = pjp.proceed(args); // 明确调用业务层方法,也叫切入点方法
            System.out.println("Logger 类中的 aroundPringLog 方法开始执行日志了。后置");
            return rtValue;
        } catch (Throwable throwable) {
            System.out.println("Logger 类中的 aroundPringLog 方法开始执行日志了。异常");
            throw new RuntimeException(throwable);
        } finally {
            System.out.println("Logger 类中的 aroundPringLog 方法开始执行日志了。最终");
        }

    }

由于 proceed 方法是有 Object 返回值的,所以需要创建一个 Object 对象来接受,并返回。而调用 proceed 方法,也就明确了切入点方法。

然后:

  • 当增强方法出现在 proceed 方法前,就是前置通知
  • 出现在 proceed 方法后,就是后置通知
  • 出现在 catch 语句块中,就是一场方法。这里注意,它抛出的异常是一个 throwable,使用 Exception 捕获不住。
  • 出现在 final 语句块中,就是最终方法

以下是运行结果:

Logger 类中的 aroundPringLog 方法开始执行日志了。前置
执行了保存
Logger 类中的 aroundPringLog 方法开始执行日志了。后置
Logger 类中的 aroundPringLog 方法开始执行日志了。最终

2.6 Spring 基于注解 AOP 配置

首先,看一下当前的 Spring 配置文件:

<?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">

    <!-- 配置 Spring 的 IOC,把 Service 对象配置进来 -->
    <bean id="accountService" class="com.self.learning.service.impl.AccountServiceImpl"></bean>

    <!-- 配置 Logger 类 -->
    <bean id="logger" class="com.self.learning.utils.Logger"></bean>

    <!-- 配置 AOP -->
    <aop:config>
        <!-- 配置切入点表达式 id 属性用于指定表达式的唯一标志, expression 用于指定表达式内容
                    此标签写在 aop:aspect 内部,只能当前切面使用。
                 它还可以写在 aop:aspect 外部,此时就变成了所有切面可用。
            -->
        <aop:pointcut id="pt1" expression="execution(* com.self.learning.service.impl.*.*(..))"/>
        <!-- 配置切面 -->
        <aop:aspect id="logAdvice" ref="logger">
            <!-- 配置前置通知 -->
<!--            <aop:before method="beforePrintLog" pointcut-ref="pt1"></aop:before>-->

            <!-- 配置后置通知 -->
<!--            <aop:after-returning method="afterReturningPrintLog" pointcut-ref="pt1"></aop:after-returning>-->

            <!-- 配置异常通知 -->
<!--            <aop:after-throwing method="afterThrowingLog" pointcut-ref="pt1"></aop:after-throwing>-->

            <!-- 配置最终通知 -->
<!--            <aop:after method="afterLog" pointcut-ref="pt1"></aop:after>-->
            <!-- 配置环绕通知 -->
            <aop:around method="aroundLog" pointcut-ref="pt1"></aop:around>
        </aop:aspect>
    </aop:config>
</beans>

里面配置了:

  • 切入点表达式
  • 配置了切面
  • 配置了环绕通知
  • 配置了4中常用通知
  • 配置了 id 为 accountService 的 bean 对象
  • 配置了 Logger 类

所以,现在一个个使用注解来代替其中的配置:

首先是 AccountService 接口类的 bean 对象:

@Component("accountService")
public class AccountServiceImpl implements IAccountService {

然后是 Logger 类的 bean 对象:

@Component("logger")
@Aspect // 表示当前类是一个切面类
public class Logger {

然后配置切入点表达式(在 Logger 类中):

    @Pointcut("execution(* com.self.learning.service.impl.*.*(..))")
    private void pt() {}

最后配置 4 中常用通知和环绕通知(在 Logger 类中):

    /**
     * 前置通知:在切入点方法执行之前执行
     */
//    @Before("pt()")
    public void beforePrintLog(){
        System.out.println("前置通知 Logger 类中的 beforePrintLog 方法开始执行日志了。");
    }

    /**
     * 后置通知:在切入点方法正常执行之后执行。它和异常通知永远只能执行一个。
     */
//    @AfterReturning("pt()")
    public void afterReturningPrintLog(){
        System.out.println("后置通知 Logger 类中的 afterReturningPrintLog 方法开始执行日志了。");
    }

    /**
     * 异常通知:在切入点方法执行产生异常之后,执行。它和后置通知永远只能执行一个。
     */
//    @AfterThrowing("pt()")
    public void afterThrowingLog(){
        System.out.println("异常通知 Logger 类中的 afterThrowingLog 方法开始执行日志了。");
    }

    /**
     * 最终通知:无论切入点方法是否正常执行,都会在其后执行
     */
//    @After("pt()")
    public void afterLog(){
        System.out.println("最终通知 Logger 类中的 afterPringLog 方法开始执行日志了。");
    }

    @Around("pt()")
    public Object aroundLog(ProceedingJoinPoint pjp) {
        Object rtValue = null;
        try {
            Object[] args = pjp.getArgs(); // 得到方法执行所需的参数
            System.out.println("Logger 类中的 aroundPringLog 方法开始执行日志了。前置");
            rtValue = pjp.proceed(args); // 明确调用业务层方法,也叫切入点方法
//            int j = 1/0;
            System.out.println("Logger 类中的 aroundPringLog 方法开始执行日志了。后置");
            return rtValue;
        } catch (Throwable throwable) {
            System.out.println("Logger 类中的 aroundPringLog 方法开始执行日志了。异常");
            throw new RuntimeException(throwable);
        } finally {
            System.out.println("Logger 类中的 aroundPringLog 方法开始执行日志了。最终");
        }

    }

最后不要忘记,因为使用的是注解代替xml配置,需要在配置文件中开启注解配置通知:

<aop:aspectj-autoproxy></aop:aspectj-autoproxy>

如果使用的是注解来标注 4 种常用的通知,那么执行顺序会产生问题:

前置通知 Logger 类中的 beforePrintLog 方法开始执行日志了。
最终通知 Logger 类中的 afterPringLog 方法开始执行日志了。
后置通知 Logger 类中的 afterReturningPrintLog 方法开始执行日志了。

当我们执行之后,会发现,最终通知在后置通知之上,这时因为 Spring 框架在使用AOP注解进行事务管理时,在调用顺序上,后置通知后与最终通知。而如果使用环绕通知则没有这个问题。

如果要使用注解类而不是配置文件,在注解类上加上以上的注解:

@Configuration
@ComponenetScan(basePackage = "com.self.learning")
@EnableAspectJAutoProxy
public class SpringConfiguration {}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值