第21~22周Java主流框架入门-Spring 2.SpringAOP面向切面编程

时间:2024-10-18 11:17:51

1.Spring AOP (Aspect-Oriented Programming)


1. 1. 什么是 Spring AOP?

  • AOP(面向切面编程) 是 Spring 提供的一种可插拔的组件技术,允许我们在软件运行过程中添加额外的功能。
  • 场景:假设有两个模块,用户管理模块A和员工管理模块B。现在需要在业务处理过程中添加权限过滤功能。
    • 如果在两个模块中都添加权限判断代码,当权限需求变化时,需要再次修改代码,这样会增加开发和维护成本。
    • 使用 Spring AOP,我们可以将权限判断的代码独立为一个切面,在代码执行前进行权限过滤,而不需要修改原业务逻辑。

1. 2. 面向切面编程 (AOP) 的核心概念

  • 切面(Aspect):表示横切的功能模块,用于实现某些通用功能(如权限检查、日志记录等)。切面可以在方法执行前后插入。
  • 权限切面:在执行业务逻辑之前判断用户权限。
  • 日志切面:记录业务逻辑的执行时间、输入参数、输出结果等信息。
    • 通过切面技术,日志和权限判断代码可以在不修改业务代码的情况下被“织入”程序。
    • 如果业务需求发生变化,只需调整配置即可轻松移除切面,不影响核心业务逻辑。

1. 3. 切面与插件技术的类比

  • 切面类似于我们在浏览器中安装的插件,可以为现有的业务模块增加额外的功能。
    • 例如:安装翻译插件后,浏览器可以将英文网页自动翻译为中文。
    • 一旦不需要这些功能,卸载插件即可,还原浏览器的原始状态。
    • 切面也是如此,它为业务模块提供了额外的功能,但这些模块本身不会感知到切面的存在。

1. 4. 为什么叫“切面”?

  • 正常的软件执行流程是从上到下按照代码顺序执行的,而切面则像一个横切面,在执行过程中横插进入业务流程中。
    • 这些横切的功能模块就是所谓的“切面(Aspect)”,通过切面我们可以为现有的业务逻辑增加扩展功能。

1. 5. AOP 的最终目的

  • 不修改源码 的情况下扩展程序行为。
  • 通常将与业务无关的通用功能(如权限检查、日志记录)封装为切面类,通过配置来插入这些功能。
  • 切面可以配置在目标方法的执行前、执行后,达到真正的“即插即用”。

2.Spring AOP - 实战配置项目

课程简介

本节课程将通过实际项目配置,带领大家一步一步理解 Spring AOP(面向切面编程)的功能。我们将基于 XML 配置的形式来实现 AOP,并通过演示了解 AOP 如何对现有系统进行功能扩展,而无需修改源代码。


2. 1. 项目结构介绍

  • 本次演示基于 s01 工程,其中包含了两个主要部分:
    • DAO 层:包括 EmployeeDaoUserDao,分别用于对员工表和用户表的数据增删改查。
    • Service 层:包括 EmployeeServiceUserService,分别提供了员工相关的业务逻辑和用户管理的业务逻辑。
      • EmployeeService:提供 entry 方法,模拟员工入职操作。
      • UserService:提供 createUser 方法(创建用户)和 generateRandomPassword 方法(生成随机密码)。

这些类的业务逻辑非常常规,但本节课我们将通过 AOP 实现对方法执行时间的监控,解决手动添加代码带来的冗余和复杂度问题。


2. 2. 需求描述

我们希望在系统运行过程中,对所有 Service 层和 DAO 层的方法调用前打印执行时间,从而便于分析系统负载高峰时间。

问题:

  • 如果直接在每个方法中手动添加 System.out.println() 代码,维护和删除这些代码将变得非常麻烦。
  • AOP 可以在不修改原代码的情况下,灵活地添加或移除这些功能。

2. 3. 配置项目依赖

首先,我们需要在 pom.xml 文件中添加必要的依赖项:

<dependencies>
    <!-- Spring context dependency -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.2.6.RELEASE</version>
    </dependency>
    
    <!-- AspectJ Weaver (AOP 底层依赖) -->
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.5</version>
    </dependency>
</dependencies>

spring-context 是用来初始化 IOC 容器的基础依赖,而 aspectjweaver 则是 AOP 的底层依赖,负责切面功能的实现。


2. 4. 配置 applicationContext.xml

接下来,我们需要在 resources 目录下创建 applicationContext.xml 文件,这是 Spring IOC 的配置文件。

添加命名空间:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       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/context 
        http://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/aop 
        http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 配置 Beans -->
    <bean id="userDao" class="com.example.dao.UserDao" />
    <bean id="employeeDao" class="com.example.dao.EmployeeDao" />
    
    <bean id="userService" class="com.example.service.UserService">
        <property name="userDao" ref="userDao" />
    </bean>
    
    <bean id="employeeService" class="com.example.service.EmployeeService">
        <property name="employeeDao" ref="employeeDao" />
    </bean>
</beans>

引入 aop 命名空间:

该命名空间用于配置 AOP 所需的相关标签。它将帮助我们在不修改源代码的前提下,为现有方法添加执行时间打印功能。


2. 5. 初始化 IOC 容器并执行测试

接下来,我们在 aop 包下创建一个 Spring 应用的入口类:

public class SpringApplication {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        
        UserService userService = context.getBean("userService", UserService.class);
        userService.createUser();  // 模拟创建用户的过程
    }
}

运行代码后,可以看到控制台输出显示了 UserServiceUserDao 中各个方法的执行情况。


2. 6. Spring AOP - 方法执行时间打印需求实现

课程目标

通过 AOP 实现对 ServiceDAO 层中任意方法的执行时间进行打印,并避免在每个方法中手动增加日志打印代码。AOP 能够灵活地实现这些功能,且无需修改源代码。


1. 新增切面类 (Method Aspect)

在 AOP 配置中,我们需要创建一个切面类,用于扩展业务逻辑。在 aop 包下新增一个 aspect 包,创建切面类 MethodAspect,用于打印方法的执行时间。

切面类 MethodAspect

public class MethodAspect {
    public void printExecutionTime(JoinPoint joinPoint) {
        // 获取当前时间并格式化
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
        String now = sdf.format(new Date());
        
        // 获取目标类名和方法名
        String className = joinPoint.getTarget().getClass().getName();
        String methodName = joinPoint.getSignature().getName();
        
        // 打印执行时间信息
        System.out.println("-----");
        System.out.println("Time: " + now);
        System.out.println("Class: " + className);
        System.out.println("Method: " + methodName);
        System.out.println("-----");
    }
}
  • JoinPoint 参数用于获取目标类和目标方法的信息。
  • printExecutionTime() 方法会在目标方法执行前打印当前时间、类名和方法名。

2. AOP 配置文件 applicationContext.xml

applicationContext.xml 文件中进行 AOP 配置,使得在调用 ServiceDAO 方法时,自动打印方法的执行时间。

配置 AOP 切面

<bean id="methodAspect" class="com.example.aspect.MethodAspect" />

<aop:config>
    <!-- 定义切点,匹配 com.example 包下所有类的所有 public 方法 -->
    <aop:pointcut id="serviceMethods" expression="execution(public * com.example..*(..))" />
    
    <!-- 定义切面 -->
    <aop:aspect ref="methodAspect">
        <!-- 前置通知,在方法执行前打印执行时间 -->
        <aop:before method="printExecutionTime" pointcut-ref="serviceMethods" />
    </aop:aspect>
</aop:config>
  • aop:pointcut 定义了切点,匹配 com.example 包下的所有 public 方法。
  • aop:before 定义了前置通知,表示在目标方法执行之前调用 printExecutionTime() 方法。

3. AOP 运行效果

  • 在运行程序时,任何 ServiceDAO 层方法执行前,控制台都会打印方法执行的时间、类名和方法名。
  • 示例输出:
-----
Time: 2024-10-16 10:35:12.123
Class: com.example.service.UserService
Method: createUser
-----

关闭功能

如果项目经理不再需要打印时间信息,只需注释掉 AOP 的配置部分即可。


3. Spring AOP - 关键概念与配置解析

3. 1. Spring AOP 和 AspectJ 的关系

  • AspectJ:一种基于 Java 平台的面向切面编程(AOP)语言,提供完整的 AOP 编程体系。
  • Spring AOP:Spring 提供的 AOP 实现,部分依赖 AspectJ。AspectJ 主要用于类和方法的匹配(通过 aspectjweaver),而功能的增强由 Spring 本身通过代理模式实现。

3. 2. 关键概念

2.1 切面(Aspect)

  • 切面:具体的可插拔组件功能类,通常用于实现通用功能。
  • 切面类:一个标准的 Java 类,无需继承或实现其他类。可以包含多个切面方法,这些方法用于实现功能扩展。
  • 切面方法
    • 例如:printExecutionTime() 用于打印方法的执行时间。
    • 方法必须为 public,返回值可以是 voidObject,具体取决于通知类型。
    • 需包含 JoinPoint 参数,用于获取目标类和方法的信息。

2.2 连接点(JoinPoint)

  • 连接点:获取目标类和目标方法的元数据对象。可通过 joinPoint.getTarget() 获取目标对象,通过 joinPoint.getSignature().getName() 获取目标方法名。

2.3 切点(Pointcut)

  • 切点:用于定义切面要作用的范围。通过 execution 表达式指定切面应在哪些类的哪些方法上生效。
  • 切点表达式:在配置文件中使用 expression 属性指定作用范围。
    • 示例:execution(public * com.example..*(..)) 作用于 com.example 包下所有类的所有 public 方法。

2.4 通知(Advice)

  • 通知(Advice):指定切面方法在何时执行。Spring AOP 支持五种通知类型:
    • 前置通知(Before):在目标方法执行前执行。
    • 后置通知(After):在目标方法执行后执行。
    • 其他类型:包括返回后通知、异常通知、环绕通知。

2.5 目标类和目标方法

  • 目标类和目标方法:指真正执行业务逻辑的类和方法,例如 ServiceDAO 层中的 createUser()insert() 方法。

3. 3. AOP 配置步骤

通过 XML 配置 AOP 主要包含以下五个步骤:

3.1 引入 AspectJ 依赖

pom.xml 中引入 aspectjweaver 依赖:

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.5</version>
</dependency>

3.2 实现切面类和方法

切面类是一个标准的 Java 类,方法中需包含 JoinPoint 参数,用于获取目标类和方法的信息。

3.3 配置切面类

applicationContext.xml 中配置切面类:

<bean id="methodAspect" class="com.example.aspect.MethodAspect" />

3.4 定义切点

使用 pointcut 标签定义切点,指定切面的作用范围:

<aop:pointcut id="serviceMethods" expression="execution(public * com.example..*(..))" />

3.5 配置通知(Advice)

在目标方法执行前通过 before 标签调用切面方法:

<aop:before method="printExecutionTime" pointcut-ref="serviceMethods" />

4. Spring AOP - JoinPoint 连接点核心方法

4. 1. JoinPoint 连接点简介

  • JoinPoint 连接点用于获取目标类和目标方法的相关信息,能够在切面方法中通过 JoinPoint 参数访问这些信息。
  • JoinPoint 提供了三个核心方法,分别是:
    • getTarget():获取目标对象(由 IOC 容器管理的对象)。
    • getSignature():获取目标方法的签名信息。
    • getArgs():获取目标方法的实际参数。

4. 2. 核心方法介绍与演示

2.1 getTarget() 方法

  • 作用:获取由 IOC 容器管理的目标对象。
  • 演示
    • 在切面方法中调用 joinPoint.getTarget() 可以获取目标对象,再通过 getClass().getName() 获取该目标对象所属的类名。

2.2 getSignature() 方法

  • 作用:获取目标方法的签名。
  • 演示
    • 使用 joinPoint.getSignature().getName() 获取目标方法的名称。

2.3 getArgs() 方法

  • 作用:获取目标方法调用时传入的参数。
  • 演示
    • joinPoint.getArgs() 返回一个 Object 数组,表示传入的参数。可以对该数组进行遍历,并打印每个参数的值。

5. Spring AOP - Pointcut 切点表达式详解

5. 1. Pointcut 切点的作用

  • Pointcut 切点:用于告诉 AOP 哪些类的哪些方法应该应用切面逻辑。
  • 切点表达式的作用:定义切面生效的范围。

5. 2. 方法结构与切点表达式

一个完整的方法结构包含以下部分:

  • 修饰符(如 publicprivate 等)。
  • 返回值类型(如 voidString)。
  • 类的完整路径(如 com.example.service.UserService)。
  • 方法名及参数(如 createUser())。

切点表达式的作用是匹配这些方法结构中的各个部分。它与方法结构一一对应。


5. 3. 切点表达式 execution 详解

execution 表达式用于指定切面生效的范围。其格式为:

execution([修饰符] [返回值] [类路径].[类名].[方法名]([参数]))

3.1 常见的通配符

  • *:匹配任意返回值、类名、方法名等。
  • ..:包通配符,匹配当前包及子包中的所有类或任意数量的参数。

5. 4. 实例讲解

4.1 匹配所有类的所有公共方法

<aop:pointcut expression="execution(public * com.example..*(..))" />
  • 匹配 com.example 包及其子包下的所有类的 public 方法。
  • * 表示任意返回值。
  • .. 表示任意包路径的匹配。
  • *(..) 表示任意方法和任意参数。
    在这里插入图片描述

4.2 匹配特定类名结尾的类

<aop:pointcut expression="execution(* com.example..*Service.*(..))" />
  • 匹配 Service 结尾的类中的所有方法。

4.3 匹配返回 void 的方法

<aop:pointcut expression="execution(void com.example..*Service.*(..))" />
  • 匹配返回类型为 void 的方法。

4.4 匹配返回 String 的方法

<aop:pointcut expression="execution(String com.example..*Service.*(..))" />
  • 匹配返回类型为 String 的方法。

4.5 匹配以 create 开头的方法

<aop:pointcut expression="execution(* com.example..*Service.create*(..))" />
  • 匹配方法名以 create 开头的方法。

4.6 匹配无参数的方法

<aop:pointcut expression="execution(* com.example..*Service.*())" />
  • 匹配无参数的方法。

4.7 匹配有特定数量参数的方法

<aop:pointcut expression="execution(* com.example..*Service.*(String, int))" />
  • 匹配参数为 Stringint 类型的方法。

6. Spring AOP - 五种通知类型

6. 1. 通知(Advice)的概念

  • 通知 是指在什么时机去执行切面的方法。Spring AOP 提供了五种类型的通知,每种通知对应不同的执行时机。

6. 2. 五种通知类型详解

2.1 前置通知(Before Advice)

  • 作用:在目标方法运行前执行切面方法。
  • 示例:在用户创建方法前输出日志信息。

2.2 返回后通知(After Returning Advice)

  • 作用:在目标方法返回结果后执行切面方法。
  • 特点:可以获取目标方法的返回值。
  • 示例:在用户创建成功后输出返回结果或状态。

2.3 异常通知(After Throwing Advice)

  • 作用:在目标方法抛出异常后执行切面方法。
  • 特点:可以获取并处理目标方法抛出的异常。
  • 示例:捕获用户创建时的异常并输出相关信息。

2.4 后置通知(After Advice)

  • 作用:在目标方法执行完毕后(无论是否成功)执行切面方法。
  • 特点:类似于 finally 块,无论是否抛出异常,后置通知都会执行。
  • 示例:在用户创建操作结束后,输出日志。

2.5 环绕通知(Around Advice)

  • 作用:可以自定义通知的执行时机,并且决定目标方法是否执行。
  • 特点:功能最强大,可以完全控制方法的执行流程。
  • 示例:在用户创建方法前后执行额外的操作,并根据条件决定是否继续执行目标方法。

6. 3. After 类型通知的执行顺序

  • After Returning 和 After Throwing 是互斥的
    • After Returning 在目标方法成功返回后执行。
    • After Throwing 在目标方法抛出异常时执行。
  • After Advice:无论成功与否,都会执行,类似于 try-catch-finally 结构中的 finally

6. 4. 示例代码:后置通知

public void doAfter(JoinPoint joinPoint) {
    System.out.println("后置通知触发");
}
  • 配置后置通知:
<aop:after method="doAfter" pointcut-ref="servicePointCut" />

6. 5. 返回后通知与异常通知的示例

5.1 返回后通知

public void doAfterReturning(JoinPoint joinPoint, Object retVal) {
    System.out.println("返回后通知,返回值: " + retVal);
}
  • 配置返回后通知:
<aop:after-returning method="doAfterReturning" pointcut-ref="servicePointCut" returning="retVal" />

5.2 异常通知

public void doAfterThrowing(JoinPoint joinPoint, Throwable error) {
    System.out.println("异常通知,异常信息: " + error.getMessage());
}
  • 配置异常通知:
<aop:after-throwing method="doAfterThrowing" pointcut-ref="servicePointCut" throwing="error" />

6. 6. 特殊通知:引介增强(Introduction Advice)

  • 作用:可以为类动态添加新的属性或方法,类似于动态代理。
  • 特点:与其他通知不同,它作用于类的增强,而非方法的增强。
  • 场景:在运行时根据不同的环境动态改变类的行为。

6. 7. 结论

  • 前四种通知类型各有用途,了解它们的执行时机和特点非常重要,特别是在调试和监控时。
  • 环绕通知是最强大的通知类型,能够完全控制方法的执行流程。
  • 引介增强是高级应用,允许在运行时为类动态添加行为,使用场景较为特殊。

7. Spring AOP - 环绕通知案例

7. 1. 场景介绍

  • 在实际工作中,随着用户量和数据量的增长,系统可能会变慢。为了定位具体是哪个方法执行缓慢,我们可以利用环绕通知来记录每个方法的执行时间,并将超过预定时间阈值的方法记录下来,便于后续优化。
  • 环绕通知 是 Spring AOP 中最强大的通知类型,可以完整控制目标方法的执行周期。

7. 2. 环绕通知的使用方法

  • 环绕通知 可以在目标方法执行前、执行后获取时间,计算出方法的执行时长。
  • 通过 ProceedingJoinPoint 参数,可以控制目标方法是否执行。

示例代码:环绕通知

public Object checkExecutionTime(ProceedingJoinPoint pjp) throws Throwable {
    // 记录开始时间
    long startTime = new Date().getTime();

    // 执行目标方法
    Object retVal = pjp.proceed();

    // 记录结束时间
    long endTime = new Date().getTime();
    long executionTime = endTime - startTime;

    // 如果执行时间超过1秒,输出日志
    if (executionTime >= 1000) {
        System.out.println("方法执行时间过长:" + executionTime + " 毫秒");
    }

    // 返回目标方法的执行结果
    return retVal;
}

7. 3. 环绕通知的关键点

  • ProceedingJoinPointProceedingJoinPointJoinPoint 的升级版,除了获取目标方法的信息,还可以控制目标方法的执行。
    • 关键方法:proceed(),用于执行目标方法并返回结果。
  • 执行时间的记录:在方法执行前记录开始时间,执行后记录结束时间,然后计算执行时长。
  • 异常处理:环绕通知可以捕获并处理目标方法抛出的异常。

7. 4. 配置环绕通知

applicationContext.xml 中配置环绕通知:

<bean id="methodChecker" class="com.example.aspect.MethodChecker" />

<aop:config>
    <aop:pointcut id="servicePointCut" expression="execution(* com.example..*Service.*(..))" />
    
    <aop:aspect ref="methodChecker">
        <aop:around method="checkExecutionTime" pointcut-ref="servicePointCut" />
    </aop:aspect>
</aop:config>

7. 5. 环绕通知与其他通知的比较

  • 环绕通知 可以完成其他四种通知的所有工作:
    • 方法执行前相当于 前置通知
    • 方法执行后相当于 后置通知
    • 返回值可以通过 返回后通知 处理。
    • 异常处理则对应 异常通知
  • 因此,环绕通知是最为灵活和强大的通知类型。

7. 6. 总结

  • 环绕通知可以控制目标方法的完整生命周期,并通过 ProceedingJoinPoint 来决定是否执行目标方法。
  • 使用环绕通知,我们可以轻松捕捉方法的执行时间、处理返回值以及异常。
  • 了解环绕通知的工作原理后,你可以灵活运用它来解决复杂的系统性能问题。

8. Spring AOP - 基于注解的配置

8. 1. 基于注解的 AOP 简介

  • 之前我们通过 XML 配置 Spring AOP,虽然功能强大,但配置较为繁琐。
  • Spring 提供了基于注解的方式,简化了 AOP 的配置,将配置信息从 XML 移动到源代码中。

8. 2. 配置步骤

2.1 引入依赖

pom.xml 中引入 Spring 和 AspectJ 相关依赖:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.2.6.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.5</version>
</dependency>

2.2 配置 applicationContext.xml

applicationContext.xml 中启用注解扫描和 AOP 注解模式:

<context:component-scan base-package="com.example"