Spring基本使用及原理剖析之AOP

Spring 是个轻量级开源框架,主要以 IoC(Inverse Of Control: 控制反转)和 AOP(Aspect Oriented Programming:面向切面编程)为内核的容器框架。作为业界使用框架中的基础框架,我一直只是简单应用而未曾有过深入挖掘,这次有些时间就做一个学习总结, 对 Spring 内部原理进行剖析并分享出来~

分享内容大致分为:AOP 的基本使用,AOP 的配置技巧,AOP 基本原理剖析

一、AOP 的基本使用

AOPAspect Oriented Programming:面向切面编程)是通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOPOOP的延续,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。简单讲就是提取重复代码在执行时使用动态代理的技术,在不修改源码的情况下对已有方法进行增强。下面我们来看看AOP是如何使用的~

1. 新建工程并导入jar包

为了简单方便,我们通过Maven来管理jar包。spring-context中包含了spring-corespring-beansspring-expressionspring-aop,因此仅配置spring-context即可

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.3.4</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.8.7</version>
</dependency>

2. 编写相关接口及实现

业务层接口。随便写个save方法作为测试方法

/**
 * 业务层接口
 *
 * @author Wenx
 * @date 2021/3/27
 */
public interface ISpringService {

    /**
     * 保存数据
     */
    void save();
}

业务层接口实现类。这里通过打印结果来模拟业务流程处理

/**
 * 业务层接口实现
 *
 * @author Wenx
 * @date 2021/4/4
 */
public class AopServiceImpl implements ISpringService {

    public void save() {
        System.out.println("AopServiceImpl的save方法执行了……");
    }
}

抽取公共代码制作成通知。假设每个业务方法都会执行相同的日志代码,抽取公共代码编写日志记录器作为AOP的环绕通知

/**
 * 日志记录器,提取业务方法中的日志公共代码
 *
 * @author Wenx
 * @date 2021/4/4
 */
public class Logger {

    /**
     * 环绕通知:一种可以在代码中手动控制增强方法何时执行的方式
     *
     * @param pjp Spring框架提供的ProceedingJoinPoint接口,该接口的proceed()方法相当于切入点方法。
     * @return
     */
    public Object aroundPrintLog(ProceedingJoinPoint pjp) {
        Object returnValue = null;
        try {
            System.out.println("前置通知:Logger的aroundPrintLog方法执行了……");

            // 获取方法的参数列表
            Object[] args = pjp.getArgs();
            // 调用业务层方法(切入点方法)
            returnValue = pjp.proceed(args);

            System.out.println("后置通知:Logger的aroundPrintLog方法执行了……");

            return returnValue;
        } catch (Throwable t) {
            System.out.println("异常通知:Logger的aroundPrintLog方法执行了……");
            throw new RuntimeException(t);
        } finally {
            System.out.println("最终通知:Logger的aroundPrintLog方法执行了……");
        }
    }
}

3. 配置文件的相关设置

配置文件AopConfig.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
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/aop
                           http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 配置业务Bean -->
    <bean id="aopService" class="com.wenx.demo.service.impl.AopServiceImpl"></bean>
    <!-- 配置日志通知 -->
    <bean id="logger" class="com.wenx.demo.aspect.Logger"></bean>

    <!--配置AOP -->
    <aop:config>
        <!--配置切面 -->
        <aop:aspect id="logAdvice" ref="logger">
            <!-- 配置环绕通知 -->
            <aop:around method="aroundPrintLog" pointcut="execution(* com.wenx.demo.service.impl.*.*(..))"></aop:around>
        </aop:aspect>
    </aop:config>
</beans>

4. 编写AOP测试代码

AOP会利用动态代理技术对配置的方法进行增强,我们使用XML文件配置了AOP的环绕通知,所以执行后会对配置方法进行环绕增强

/**
 * @author Wenx
 * @date 2021/4/4
 */
public class AopDemo {
    public static void main(String[] args) {
        // 1.获取IoC核心容器对象
        ApplicationContext ac = new ClassPathXmlApplicationContext("AopConfig.xml");
        // 2.获取bean对象
        ISpringService ss = ac.getBean("aopService", ISpringService.class);
        // 3.触发环绕通知
        ss.save();

        System.out.println(ss);
    }
}

二、AOP 的配置技巧

AOPIoC相比同样有两种配置方式,分别是基于XML配置和基于注解配置。XML配置文件会将通知方法和切入点方法的关联关系清晰展现但配置稍繁琐,注解方式配置较简便但AOP关联关系描述在各个切面类中较分散,使用XML配置还是注解配置需要根据实际情况选择

1. AOP相关术语

Joinpoint(连接点):指那些被拦截到的点。在Spring中这些点指的是方法,因为Spring只支持方法类型的连接点。

Pointcut(切入点):指我们要对哪些Joinpoint进行拦截的定义。

Advice(通知/增强):指拦截到Joinpoint之后所要做的事情就是通知。通知的类型:前置通知、后置通知、异常通知、最终通知、环绕通知。

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

Target(目标对象):指代理的目标对象。

Weaving(织入):指把增强应用到目标对象来创建新的代理对象的过程。Spring采用动态代理织入,而AspectJ采用编译期织入和类装载期织入。

Proxy(代理):指一个类被AOP织入增强后就产生一个结果代理类。

Aspect(切面):指切入点和通知(引介)的结合。

2. 基于XML的AOP配置

Spring通过XML配置文件来建立通知方法和切入点方法的关联关系,利用动态代理对方法进行增强。下面来了解下AOP是如何配置的~

1). 导入XML配置约束

XML配置约束可以从官网找到,然后直接在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
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/aop
                           http://www.springframework.org/schema/aop/spring-aop.xsd">  
</beans>
2). 配置AOP切面和切入点

配置AOP的步骤:

  1. 需配置AOP切面的业务方法(连接点)
  2. 抽取公共代码制作成通知(例如:日志、事务、结果、异常等)
  3. 配置SpringIoC,将业务类和通知类交给Spring来管理
  4. 配置切面,指定配置好的通知类
  5. 配置切入点表达式,指定对哪些业务类的哪些方法进行增强
  6. 配置对应的通知类型,选择通知类型(包括:前置通知、后置通知、异常通知、最终通知、环绕通知),并为通知方法和切入点方法建立关联
<?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
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/aop
                           http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 配置Spring的IoC -->
    <bean id="aopService" class="com.wenx.demo.service.impl.AopServiceImpl"></bean>
    <!-- 配置日志通知 -->
    <bean id="logger" class="com.wenx.demo.aspect.Logger"></bean>

    <!-- 配置AOP -->
    <aop:config>
        <!-- 配置切入点表达式,指定对哪些类的哪些方法进行增强。
             此标签写在aop:aspect标签外面所有切面可用,而写在aop:aspect标签内部只能当前切面使用。
             标签:aop:pointcut
             其属性:
                    id:用于给切入点表达式提供一个唯一标识。
                    expression:用于定义切入点表达式。
         -->
        <aop:pointcut id="pc1" expression="execution(* com.wenx.demo.service.impl.*.*(..))"></aop:pointcut>
        <!-- 配置切面
             标签:aop:aspect
             其属性:
                    id:用于给切面提供一个唯一标识。
                    ref:用于引用配置好的通知类bean的id。
        -->
        <aop:aspect id="logAdvice" ref="logger">
            <!-- 配置环绕通知
                 标签:aop:around
                 其属性:
                        method:指定通知中方法的名称。
                        pointcut:定义切入点表达式。
                        pointcut-ref:指定切入点表达式的引用。
            -->
            <aop:around method="aroundPrintLog" pointcut-ref="pc1"></aop:around>
        </aop:aspect>
    </aop:config>
</beans>

业务层接口。随便写个save方法作为测试方法

/**
 * 业务层接口
 *
 * @author Wenx
 * @date 2021/3/27
 */
public interface ISpringService {

    /**
     * 保存数据
     */
    void save();
}

业务层接口实现类。这里通过打印结果来模拟业务方法

/**
 * 业务层接口实现
 *
 * @author Wenx
 * @date 2021/4/4
 */
public class AopServiceImpl implements ISpringService {

    public void save() {
        System.out.println("AopServiceImpl的save方法执行了……");
    }
}

抽取公共代码制作成通知。假设每个业务方法都会执行相同的日志代码,抽取公共代码编写日志记录器作为AOP的环绕通知

/**
 * 日志记录器,提取业务方法中的日志公共代码
 *
 * @author Wenx
 * @date 2021/4/4
 */
public class Logger {

    /**
     * 环绕通知:一种可以在代码中手动控制增强方法何时执行的方式
     *
     * @param pjp Spring框架提供的ProceedingJoinPoint接口,该接口的proceed()方法相当于切入点方法。
     * @return
     */
    public Object aroundPrintLog(ProceedingJoinPoint pjp) {
        Object returnValue = null;
        try {
            System.out.println("前置通知:Logger的aroundPrintLog方法执行了……");

            // 获取方法的参数列表
            Object[] args = pjp.getArgs();
            // 调用业务层方法(切入点方法)
            returnValue = pjp.proceed(args);

            System.out.println("后置通知:Logger的aroundPrintLog方法执行了……");

            return returnValue;
        } catch (Throwable t) {
            System.out.println("异常通知:Logger的aroundPrintLog方法执行了……");
            throw new RuntimeException(t);
        } finally {
            System.out.println("最终通知:Logger的aroundPrintLog方法执行了……");
        }
    }
}

测试代码

/**
 * @author Wenx
 * @date 2021/4/4
 */
public class AopDemo {
    public static void main(String[] args) {
        // 1.获取IoC核心容器对象
        ApplicationContext ac = new ClassPathXmlApplicationContext("AopConfig.xml");
        // 2.获取bean对象
        ISpringService ss = ac.getBean("aopService", ISpringService.class);
        // 3.触发环绕通知
        ss.save();

        System.out.println(ss);
    }
}
3). 切入点execution表达式

execution表达式用于指定通知生效的范围(切入点),也就是指定对哪些业务类的哪些方法进行增强

<?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
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/aop
                           http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 配置Spring的IoC -->
    <bean id="aopService" class="com.wenx.demo.service.impl.AopServiceImpl"></bean>
    <!-- 配置日志通知 -->
    <bean id="logger" class="com.wenx.demo.aspect.Logger"></bean>

    <!-- 配置AOP -->
    <aop:config>
        <!-- 配置切入点表达式,指定对哪些类的哪些方法进行增强。
             此标签写在aop:aspect标签外面所有切面可用,而写在aop:aspect标签内部只能当前切面使用。
             标签:aop:pointcut
             其属性:
                    id:用于给切入点表达式提供一个唯一标识。
                    expression:用于定义切入点表达式。
             表达式语法:execution([访问修饰符] 返回值类型 包名.类名.方法名(参数))
             全匹配方式:
                public void com.wenx.demo.service.impl.AopServiceImpl.save()
             省略访问修饰符:
                void com.wenx.demo.service.impl.AopServiceImpl.save()
             返回值类型使用通配符*号,表示任意返回值:
                * com.wenx.demo.service.impl.AopServiceImpl.save()
             包名使用通配符*号,表示任意包。但是有几级包,就需要写几个*:
                * *.*.*.*.*.AopServiceImpl.save()
             包名使用..来表示当前包及其子包:
                * com..AopServiceImpl.save()
             类名使用通配符*号,表示任意类:
                * com..*.save()
             方法名使用通配符*号,表示任意方法:
                * com..*.*()
             参数列表数据类型:
                基本类型可直接写名称:* com..*.*(int)
                引用类型写全限定类名:* com..*.*(java.lang.String)
             参数列表使用通配符*号,表示任意数据类型,但是必须有参数:
                * com..*.*(*)
             参数列表使用..,表示任意数据类型,有无参数均可:
                * com..*.*(..)
             全通配方式:
                * *..*.*(..)

             开发中常用写法:切到业务层实现类下的所有方法
                * com.wenx.demo.service.impl.*.*(..)
        -->
        <aop:pointcut id="pc1" expression="execution(* com.wenx.demo.service.impl.*.*(..))"></aop:pointcut>
        <!-- 配置切面 -->
        <aop:aspect id="logAdvice" ref="logger">
            <!-- 配置环绕通知 -->
            <aop:around method="aroundPrintLog" pointcut-ref="pc1"></aop:around>
        </aop:aspect>
    </aop:config>
</beans>
4). 通知的五种类型

Advice(通知/增强):指拦截到Joinpoint之后所要做的事情就是通知

通知的类型:前置通知、后置通知、异常通知、最终通知、环绕通知

<?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
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/aop
                           http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 配置Spring的IoC -->
    <bean id="aopService" class="com.wenx.demo.service.impl.AopServiceImpl"></bean>
    <!-- 配置日志通知 -->
    <bean id="logger" class="com.wenx.demo.aspect.Logger"></bean>

    <!-- 配置AOP -->
    <aop:config>
        <!-- 配置切入点表达式,指定对哪些类的哪些方法进行增强。
             此标签写在aop:aspect标签外面所有切面可用,而写在aop:aspect标签内部只能当前切面使用。
        -->
        <aop:pointcut id="pc1" expression="execution(* com.wenx.demo.service.impl.*.*(..))"></aop:pointcut>
        <!-- 配置切面 -->
        <aop:aspect id="logAdvice" ref="logger">
            <!-- 配置前置通知:指定增强的方法在切入点方法之前执行
            <aop:before method="beforePrintLog" pointcut-ref="pc1"></aop:before>
            -->

            <!-- 配置后置通知:在切入点方法正常执行之后执行,它与异常通知永远只能执行一个
            <aop:after-returning method="afterReturningPrintLog" pointcut-ref="pc1"></aop:after-returning>
            -->

            <!-- 配置异常通知:在切入点方法执行产生异常之后执行,它与后置通知永远只能执行一个
            <aop:after-throwing method="afterThrowingPrintLog" pointcut-ref="pc1"></aop:after-throwing>
            -->

            <!-- 配置最终通知:无论切入点方法是否正常执行它都会在其后面执行
            <aop:after method="afterPrintLog" pointcut-ref="pc1"></aop:after>
            -->

            <!-- 配置环绕通知
                 标签:aop:around
                 其属性:
                        method:用于指定通知类中增强方法的名称
                        pointcut:用于指定切入点的表达式
                        pointcut-ref:用于指定切入点表达式的引用
            -->
            <aop:around method="aroundPrintLog" pointcut-ref="pc1"></aop:around>
        </aop:aspect>
    </aop:config>
</beans>

五种通知类型的增强方法

/**
 * 日志记录器,提取业务方法中的日志公共代码
 *
 * @author Wenx
 * @date 2021/4/4
 */
public class Logger {

    /**
     * 前置通知
     */
    public void beforePrintLog() {
        System.out.println("前置通知:Logger的beforePrintLog方法执行了……");
    }

    /**
     * 后置通知
     */
    public void afterReturningPrintLog() {
        System.out.println("后置通知:Logger的afterReturningPrintLog方法执行了……");
    }

    /**
     * 异常通知
     */
    public void afterThrowingPrintLog() {
        System.out.println("异常通知:Logger的afterThrowingPrintLog方法执行了……");
    }

    /**
     * 最终通知
     */
    public void afterPrintLog() {
        System.out.println("最终通知:Logger的afterPrintLog方法执行了……");
    }

    /**
     * 环绕通知:一种可以在代码中手动控制增强方法何时执行的方式
     *
     * @param pjp Spring框架提供的ProceedingJoinPoint接口,该接口的proceed()方法相当于切入点方法。
     * @return
     */
    public Object aroundPrintLog(ProceedingJoinPoint pjp) {
        Object returnValue = null;
        try {
            System.out.println("前置通知:Logger的aroundPrintLog方法执行了……");

            // 获取方法的参数列表
            Object[] args = pjp.getArgs();
            // 调用业务层方法(切入点方法)
            returnValue = pjp.proceed(args);

            System.out.println("后置通知:Logger的aroundPrintLog方法执行了……");

            return returnValue;
        } catch (Throwable t) {
            System.out.println("异常通知:Logger的aroundPrintLog方法执行了……");
            throw new RuntimeException(t);
        } finally {
            System.out.println("最终通知:Logger的aroundPrintLog方法执行了……");
        }
    }
}

3. 基于注解的AOP配置

基于注解配置是通过使用注解的方式来达到使用XML文件配置相同的效果,相比基于XML配置来说基于注解的IoC配置更为简便,所以对于厌烦了通过XML文件进行配置的同学来讲基于注解配置也是个不错的选择

1). 开启注解AOP支持

基于注解配置是通过Spring自动扫描包中带注解的类来实现AOP功能的,除了要在XML文件中指定下Spring扫描包的范围,还需要开启注解AOP支持,这样就能使用注解的方式来达到使用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
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/aop
                           http://www.springframework.org/schema/aop/spring-aop.xsd
                           http://www.springframework.org/schema/context
                           http://www.springframework.org/schema/context/spring-context.xsd">

    <!-- 指定Spring在创建Bean时要扫描的包 -->
    <context:component-scan base-package="com.wenx.demo"></context:component-scan>

    <!-- 指定Spring开启注解AOP支持 -->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>

那么有同学就说了这不还是使用XML文件了吗?不使用XML文件的配置方式可以如下面这样通过配置类的方式实现

/**
 * Spring配置类
 *
 * @author Wenx
 * @date 2021/4/2
 */
@Configuration
@ComponentScan("com.wenx.demo")
@EnableAspectJAutoProxy
public class SpringConfiguration {
}
2). 配置AOP切面和切入点

@Component:用于把当前类对象交给Spring来管理,相当于<bean id="" class="">

@Aspect:把当前类声明为切面类,相当于<aop:aspect id="" ref="">

@Pointcut:指定切入点表达式,相当于`<aop:pointcut id="" expression="">

@Before:把当前方法看成是前置通知,value属性用于指定切入点表达式或切入点表达式的引用,相当于<aop:before method="" pointcut=""><aop:before method="" pointcut-ref="">

@AfterReturning:把当前方法看成是后置通知,value属性用于指定切入点表达式或切入点表达式的引用,相当于<aop:after-returning method="" pointcut=""><aop:after-returning method="" pointcut-ref="">

@AfterThrowing:把当前方法看成是异常通知,value属性用于指定切入点表达式或切入点表达式的引用,相当于<aop:after-throwing method="" pointcut=""><aop:after-throwing method="" pointcut-ref="">

@After:把当前方法看成是最终通知,value属性用于指定切入点表达式或切入点表达式的引用,相当于<aop:after method="" pointcut=""><aop:after method="" pointcut-ref="">

@Around:把当前方法看成是环绕通知,value属性用于指定切入点表达式或切入点表达式的引用,相当于<aop:around method="" pointcut=""><aop:around method="" pointcut-ref="">

/**
 * 日志记录器,提取业务方法中的日志公共代码
 *
 * @author Wenx
 * @date 2021/4/4
 */
@Component("logger")
@Aspect
public class AnnotationLogger {

    @Pointcut("execution(* com.wenx.demo.service.impl.*.*(..))")
    private void pc1() {
    }

    /**
     * 前置通知
     */
    @Before("pc1()")
    public void beforePrintLog() {
        System.out.println("前置通知:Logger的beforePrintLog方法执行了……");
    }

    /**
     * 后置通知
     */
    @AfterReturning("pc1()")
    public void afterReturningPrintLog() {
        System.out.println("后置通知:Logger的afterReturningPrintLog方法执行了……");
    }

    /**
     * 异常通知
     */
    @AfterThrowing("pc1()")
    public void afterThrowingPrintLog() {
        System.out.println("异常通知:Logger的afterThrowingPrintLog方法执行了……");
    }

    /**
     * 最终通知
     */
    @After("pc1()")
    public void afterPrintLog() {
        System.out.println("最终通知:Logger的afterPrintLog方法执行了……");
    }

    /**
     * 环绕通知:一种可以在代码中手动控制增强方法何时执行的方式
     *
     * @param pjp Spring框架提供的ProceedingJoinPoint接口,该接口的proceed()方法相当于切入点方法。
     * @return
     */
    //@Around("pc1()")
    public Object aroundPrintLog(ProceedingJoinPoint pjp) {
        Object returnValue = null;
        try {
            System.out.println("前置通知:Logger的aroundPrintLog方法执行了……");

            // 获取方法的参数列表
            Object[] args = pjp.getArgs();
            // 调用业务层方法(切入点方法)
            returnValue = pjp.proceed(args);

            System.out.println("后置通知:Logger的aroundPrintLog方法执行了……");

            return returnValue;
        } catch (Throwable t) {
            System.out.println("异常通知:Logger的aroundPrintLog方法执行了……");
            throw new RuntimeException(t);
        } finally {
            System.out.println("最终通知:Logger的aroundPrintLog方法执行了……");
        }
    }
}

业务层接口。随便写个save方法作为测试方法

/**
 * 业务层接口
 *
 * @author Wenx
 * @date 2021/3/27
 */
public interface ISpringService {

    /**
     * 保存数据
     */
    void save();
}

业务层接口实现类。这里通过打印结果来模拟业务流程处理

/**
 * 业务层接口实现
 *
 * @author Wenx
 * @date 2021/4/4
 */
@Service("aopService")
public class AopServiceImpl implements ISpringService {

    public void save() {
        System.out.println("AopServiceImpl的save方法执行了……");
    }
}

测试代码

/**
 * @author Wenx
 * @date 2021/4/4
 */
public class AopDemo {
    public static void main(String[] args) {
        // 1.获取IoC核心容器对象
        ApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfiguration.class);
        // 2.获取bean对象
        ISpringService ss = ac.getBean("aopService", ISpringService.class);
        // 3.触发环绕通知
        ss.save();

        System.out.println(ss);
    }
}

三、AOP 基本原理剖析

在上面我们讲了SpringAOP的基本使用和AOP的配置技巧,这里我们将对AOP的基本原理进行剖析。AOP是通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术,简单讲就是提取重复代码在执行时使用动态代理的技术,在不修改源码的情况下对已有方法进行增强。下面通过对源码断点跟踪进行简要分析,从而了解其内部的基本运行原理

1. 调用业务方法

没有什么是通过对源码断点跟踪解决不了的,没看明白?再来一遍,再来一遍,再来一遍,源码中的注释和本身的方法名称能给你很大的提示,真看不懂可以靠猜避过一些抽象类,把大致的原理弄清就好~ 下面我们直接断点定到ss.save(),进入JdkDynamicAopProxy类的invoke方法

/**
 * @author Wenx
 * @date 2021/4/4
 */
public class AopDemo {
    public static void main(String[] args) {
        // 1.获取IoC核心容器对象
        ApplicationContext ac = new ClassPathXmlApplicationContext("AopConfig.xml");
        // 2.获取bean对象
        ISpringService ss = ac.getBean("aopService", ISpringService.class);
        // 3.触发环绕通知
        ss.save();

        System.out.println(ss);
    }
}

2. JdkDynamicAopProxy类

看名字都能猜出JdkDynamicAopProxy类肯定和JDK的动态代理有关。断点走下来先获取了业务类,又获取了拦截器链(内部是通知方法集合),最后走到retVal = invocation.proceed()这行,注释上讲的很清楚要通过拦截器链进入连接点

@Override
@Nullable
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
	Object oldProxy = null;
	boolean setProxyContext = false;
	TargetSource targetSource = this.advised.targetSource;
	Object target = null;
	try {
        // 省略部分代码……
        
		// Get as late as possible to minimize the time we "own" the target,
		// in case it comes from a pool.
		target = targetSource.getTarget();
		Class<?> targetClass = (target != null ? target.getClass() : null);
		// Get the interception chain for this method.
		List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
		// Check whether we have any advice. If we don't, we can fallback on direct
		// reflective invocation of the target, and avoid creating a MethodInvocation.
		if (chain.isEmpty()) {
			// We can skip creating a MethodInvocation: just invoke the target directly
			// Note that the final invoker must be an InvokerInterceptor so we know it does
			// nothing but a reflective operation on the target, and no hot swapping or fancy proxying.
			Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
			retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
		}
		else {
			// We need to create a method invocation...
			MethodInvocation invocation =
					new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
			// Proceed to the joinpoint through the interceptor chain.
			retVal = invocation.proceed();
		}
        
        // 省略部分代码……
        
		return retVal;
	}
	finally {
        // 省略部分代码……
	}
}

3. ReflectiveMethodInvocation类

ReflectiveMethodInvocation类的proceed方法中,这里我们能够看出就是个拦截器链,通过递归方式调用动态代理的方法。拦截器链你可以理解为是个俄罗斯套娃,一层套着一层的,执行完这一层代码再进入下一层,拦截器链里是我们配置的通知方法和业务方法,断点到((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this)这行来进入拦截器执行方法

@Override
@Nullable
public Object proceed() throws Throwable {
	// We start with an index of -1 and increment early.
	if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
		return invokeJoinpoint();
	}
	Object interceptorOrInterceptionAdvice =
			this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
	if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
		// Evaluate dynamic method matcher here: static part will already have
		// been evaluated and found to match.
		InterceptorAndDynamicMethodMatcher dm =
				(InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;
		Class<?> targetClass = (this.targetClass != null ? this.targetClass : this.method.getDeclaringClass());
		if (dm.methodMatcher.matches(this.method, targetClass, this.arguments)) {
			return dm.interceptor.invoke(this);
		}
		else {
			// Dynamic matching failed.
			// Skip this interceptor and invoke the next in the chain.
			return proceed();
		}
	}
	else {
		// It's an interceptor, so we just invoke it: The pointcut will have
		// been evaluated statically before this object was constructed.
		return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
	}
}

4. ExposeInvocationInterceptor类

调用拦截器类,作为拦截器链的头结点开始执行

@Override
@Nullable
public Object invoke(MethodInvocation mi) throws Throwable {
	MethodInvocation oldInvocation = invocation.get();
	invocation.set(mi);
	try {
		return mi.proceed();
	}
	finally {
		invocation.set(oldInvocation);
	}
}

5. AspectJAroundAdvice类

环绕通知类,到这步会根据AOP配置的不同而进入不同的通知类,invokeAdviceMethod(pjp, jpm, null, null)这行调用通知方法

@Override
@Nullable
public Object invoke(MethodInvocation mi) throws Throwable {
	if (!(mi instanceof ProxyMethodInvocation)) {
		throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi);
	}
	ProxyMethodInvocation pmi = (ProxyMethodInvocation) mi;
	ProceedingJoinPoint pjp = lazyGetProceedingJoinPoint(pmi);
	JoinPointMatch jpm = getJoinPointMatch(pmi);
	return invokeAdviceMethod(pjp, jpm, null, null);
}

6. AbstractAspectJAdvice类

切面通知抽象类,this.aspectJAdviceMethod.invoke(this.aspectInstanceFactory.getAspectInstance(), actualArgs)这行进入环绕通知

// As above, but in this case we are given the join point.
protected Object invokeAdviceMethod(JoinPoint jp, @Nullable JoinPointMatch jpMatch,
		@Nullable Object returnValue, @Nullable Throwable t) throws Throwable {
	return invokeAdviceMethodWithGivenArgs(argBinding(jp, jpMatch, returnValue, t));
}

protected Object invokeAdviceMethodWithGivenArgs(Object[] args) throws Throwable {
	Object[] actualArgs = args;
	if (this.aspectJAdviceMethod.getParameterCount() == 0) {
		actualArgs = null;
	}
	try {
		ReflectionUtils.makeAccessible(this.aspectJAdviceMethod);
		return this.aspectJAdviceMethod.invoke(this.aspectInstanceFactory.getAspectInstance(), actualArgs);
	}
	catch (IllegalArgumentException ex) {
		throw new AopInvocationException("Mismatch on arguments to advice method [" +
				this.aspectJAdviceMethod + "]; pointcut expression [" +
				this.pointcut.getPointcutExpression() + "]", ex);
	}
	catch (InvocationTargetException ex) {
		throw ex.getTargetException();
	}
}

7. Logger类

提取业务方法中的日志公共代码的通知类,类似Proxy.newProxyInstance动态代理中InvocationHandlerinvoke方法,将代码执行下去环绕通知方法与业务方法也就执行完毕了

/**
 * 环绕通知:一种可以在代码中手动控制增强方法何时执行的方式
 *
 * @param pjp Spring框架提供的ProceedingJoinPoint接口,该接口的proceed()方法相当于切入点方法。
 * @return
 */
public Object aroundPrintLog(ProceedingJoinPoint pjp) {
    Object returnValue = null;
    try {
        System.out.println("前置通知:Logger的aroundPrintLog方法执行了……");
        // 获取方法的参数列表
        Object[] args = pjp.getArgs();
        // 调用业务层方法(切入点方法)
        returnValue = pjp.proceed(args);
        System.out.println("后置通知:Logger的aroundPrintLog方法执行了……");
        return returnValue;
    } catch (Throwable t) {
        System.out.println("异常通知:Logger的aroundPrintLog方法执行了……");
        throw new RuntimeException(t);
    } finally {
        System.out.println("最终通知:Logger的aroundPrintLog方法执行了……");
    }
}

8. 其他通知类

若配置AOP是其他类型的通知,则可能进入不同的处理环节。配置最终通知则拦截器会进入AspectJAfterAdvice类的invoke方法,无论切入点方法是否正常执行它都会在其后面执行最终通知方法

@Override
@Nullable
public Object invoke(MethodInvocation mi) throws Throwable {
	try {
		return mi.proceed();
	}
	finally {
		invokeAdviceMethod(getJoinPointMatch(), null, null);
	}
}

配置异常通知则拦截器会进入AspectJAfterThrowingAdvice类的invoke方法,在切入点方法执行产生异常之后执行异常通知方法,它与后置通知永远只能执行一个

@Override
@Nullable
public Object invoke(MethodInvocation mi) throws Throwable {
	try {
		return mi.proceed();
	}
	catch (Throwable ex) {
		if (shouldInvokeOnThrowing(ex)) {
			invokeAdviceMethod(getJoinPointMatch(), null, ex);
		}
		throw ex;
	}
}

配置后置通知则拦截器会进入AfterReturningAdviceInterceptor类的invoke方法,在切入点方法正常执行之后执行后置通知方法,它与异常通知永远只能执行一个

@Override
@Nullable
public Object invoke(MethodInvocation mi) throws Throwable {
	Object retVal = mi.proceed();
	this.advice.afterReturning(retVal, mi.getMethod(), mi.getArguments(), mi.getThis());
	return retVal;
}

配置前置通知则拦截器会进入MethodBeforeAdviceInterceptor类的invoke方法,前置通知方法会在切入点方法之前执行

@Override
@Nullable
public Object invoke(MethodInvocation mi) throws Throwable {
	this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis());
	return mi.proceed();
}

9. AOP与动态代理的关系

动态代理有别于静态代理不需要事先创建代理类,而是在程序运行时运用反射机制动态创建而成,字节码随用随创建,随用随加载,在不修改源码的基础上对方法进行增强。动态代理可分为基于接口的动态代理,使用JDK官方的Proxy类中的newProxyInstance方法进行创建,被代理类要求最少实现一个接口;还有基于子类的动态代理,使用第三方cglib库的Enhancer类中的create方法进行创建,被代理类要求不能是最终类。SpringAOP则会根据被代理类是否实现了接口,而自动选择是使用JDK官方的Proxy类,还是使用第三方cglib库的Enhancer类来创建代理。下面我们来介绍下这几种代理~

JDK官方的Proxy类的使用

public static void main(String[] args) {
    // 被代理类,需要增强的方法所属的类
    final AopServiceImpl service = new AopServiceImpl();
    // 基于接口的动态代理,使用JDK官方的Proxy类中的newProxyInstance方法,被代理类最少实现一个接口
    ISpringService serviceProxy = (ISpringService) Proxy.newProxyInstance(
            // ClassLoader(类加载器)用于加载代理对象的字节码,和被代理对象使用相同的类加载器(固定写法)
            service.getClass().getClassLoader(),
            // Class[](字节码数组)用于让代理对象和被代理对象有相同方法(固定写法)
            service.getClass().getInterfaces(),
            // InvocationHandler(调用处理程序)用于提供增强的代码
            new InvocationHandler() {
                /**
                 * 作用:执行被代理对象的任何接口方法都会经过该方法
                 *
                 * @param proxy  代理对象的引用
                 * @param method 当前执行的方法
                 * @param args   当前执行方法所需的参数
                 * @return 和被代理对象方法有相同的返回值
                 * @throws Throwable
                 */
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    if (!"save".equals(method.getName())) {
                        return method.invoke(service, args);
                    }
                    Object returnValue = null;
                    try {
                        System.out.println("前置通知……");
                        returnValue = method.invoke(service, args);
                        System.out.println("后置通知……");
                        return returnValue;
                    } catch (Throwable t) {
                        System.out.println("异常通知……");
                        throw new RuntimeException(t);
                    } finally {
                        System.out.println("最终通知……");
                    }
                }
            });
    // 执行代理的方法
    serviceProxy.save();
}

第三方cglib库的Enhancer类的使用

public static void main(String[] args) {
    // 被代理类,需要增强的方法所属的类
    final AopServiceImpl service = new AopServiceImpl();
    // 基于子类的动态代理,使用第三方cglib库的Enhancer类中的create方法,被代理类不能是最终类
    ISpringService serviceCglib = (ISpringService) Enhancer.create(
            // Class(字节码)用于指定被代理对象的字节码
            service.getClass(),
            // Callback(回调函数)用于提供增强的代码
            new MethodInterceptor() {
                /**
                 * 作用:执行被代理对象的任何方法都会经过该方法
                 *
                 * @param proxy       代理对象的引用
                 * @param method      当前执行的方法
                 * @param args        当前执行方法所需的参数
                 * @param methodProxy 当前执行方法的代理对象
                 * @return 和被代理对象方法有相同的返回值
                 * @throws Throwable
                 */
                public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
                    if (!"save".equals(method.getName())) {
                        return method.invoke(service, args);
                    }
                    Object returnValue = null;
                    try {
                        System.out.println("前置通知……");
                        returnValue = method.invoke(service, args);
                        System.out.println("后置通知……");
                        return returnValue;
                    } catch (Throwable t) {
                        System.out.println("异常通知……");
                        throw new RuntimeException(t);
                    } finally {
                        System.out.println("最终通知……");
                    }
                }
            });
    // 执行代理的方法
    serviceCglib.save();
}

SpringAOP环绕通知的使用

/**
 * 环绕通知:一种可以在代码中手动控制增强方法何时执行的方式
 *
 * @param pjp Spring框架提供的ProceedingJoinPoint接口,该接口的proceed()方法相当于切入点方法。
 * @return
 */
public Object aroundPrintLog(ProceedingJoinPoint pjp) {
    Object returnValue = null;
    try {
        System.out.println("前置通知……");
        // 获取方法的参数列表
        Object[] args = pjp.getArgs();
        // 调用业务层方法(切入点方法)
        returnValue = pjp.proceed(args);
        System.out.println("后置通知……");
        return returnValue;
    } catch (Throwable t) {
        System.out.println("异常通知……");
        throw new RuntimeException(t);
    } finally {
        System.out.println("最终通知……");
    }
}

总结: 上一篇讲解了IoC的基本使用、配置技巧和基本原理剖析,本篇讲解了AOP的基本使用、配置技巧和基本原理剖析,至此Spring的基本使用及原理剖析讲解完毕,感谢大家能看将本篇文章看完~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值