当前位置: 首页 > article >正文

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

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" />
<aop:aspectj-autoproxy />
  • component-scan:扫描指定包下的组件。
  • aspectj-autoproxy:启用 Spring AOP 注解模式。

8. 3. 在源代码中使用注解

3.1 Service 和 DAO 注解

在 Service 和 DAO 类上使用 @Service@Repository 注解,将它们注册到 Spring IOC 容器中:

@Service
public class UserService {
    @Resource
    private UserDao userDao;
    
    // Service 方法
}
@Repository
public class UserDao {
    // DAO 方法
}

3.2 切面类配置

创建切面类 MethodChecker,并使用注解定义环绕通知:

@Component
@Aspect
public class MethodChecker {

    @Around("execution(* com.example..*Service.*(..))")
    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;
    }
}
  • @Component:将 MethodChecker 注册为 Spring IOC 容器的组件。
  • @Aspect:表明该类是一个切面类。
  • @Around:定义环绕通知,并通过 execution 表达式指定切点。

8. 4. 其他通知注解

除了 @Around,还有其他几种通知注解:

  • @Before:前置通知,在目标方法执行前调用。
  • @After:后置通知,在目标方法执行后调用。
  • @AfterReturning:返回后通知,在目标方法成功返回后调用。
  • @AfterThrowing:异常通知,在目标方法抛出异常时调用。

8. 5. 总结

  • 基于注解的 Spring AOP 配置大大简化了开发过程,只需在代码中添加注解即可完成配置。
  • @Aspect@Around 等注解可以帮助我们灵活地实现切面逻辑,而无需繁琐的 XML 配置。
  • 通过组件扫描和 AOP 自动代理,Spring 可以轻松管理切面和服务类的生命周期。

9. Spring AOP - 代理模式与实现原理

9. 1. Spring AOP 的底层实现原理

  • Spring AOP 的底层核心是基于设计模式中的 代理模式,用于实现功能的动态扩展。
  • Spring AOP 的实现方式有两种:
    1. JDK 动态代理:当目标类实现了接口时,使用 JDK 动态代理来实现。
    2. CGLIB:当目标类没有实现接口时,使用 CGLIB 进行代理。

9. 2. 代理模式的基本概念

  • 代理模式 是通过代理对象对原对象进行功能扩展的设计模式。
  • 代理对象:代表真正执行业务逻辑的对象,并在其基础上添加额外的功能。
  • 生活中的例子:租房时,中介作为代理人帮助客户(租客)和房东进行交易,完成租房过程。

9. 3. 静态代理的实现

3.1 接口定义

创建一个 UserService 接口,定义用户服务的基础功能:

public interface UserService {
    void createUser();
}

3.2 委托类实现

创建 UserServiceImpl 类,作为接口的具体实现类,负责用户创建的实际业务逻辑:

public class UserServiceImpl implements UserService {
    @Override
    public void createUser() {
        System.out.println("执行创建用户业务逻辑");
    }
}

3.3 代理类实现

通过创建代理类 UserServiceProxy 来扩展 UserService 接口的功能:

public class UserServiceProxy implements UserService {
    private UserService userService;

    // 构造方法接受委托类的对象
    public UserServiceProxy(UserService userService) {
        this.userService = userService;
    }

    @Override
    public void createUser() {
        // 扩展功能:打印方法执行前的时间
        System.out.println("执行前时间: " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));

        // 调用委托类的实际业务逻辑
        userService.createUser();

        // 扩展功能:可以在这里添加更多逻辑
    }
}

9. 4. 动态代理的嵌套

通过创建多个代理类,可以实现代理嵌套:

public class UserServiceProxy1 implements UserService {
    private UserService userService;

    public UserServiceProxy1(UserService userService) {
        this.userService = userService;
    }

    @Override
    public void createUser() {
        // 调用委托类的方法
        userService.createUser();

        // 扩展功能:在方法执行后进行额外处理
        System.out.println("方法后扩展功能");
    }
}
  • 可以使用多个代理类进行嵌套代理,通过一层层扩展功能。

9. 5. 示例代码:静态代理的调用

Application 类中调用代理类,实现方法的功能扩展:

public class Application {
    public static void main(String[] args) {
        // 实例化委托类
        UserService userService = new UserServiceImpl();

        // 使用代理类对委托类进行扩展
        UserService proxy = new UserServiceProxy(new UserServiceProxy1(userService));
        proxy.createUser();
    }
}

9. 6. 静态代理的缺点

  • 每增加一个业务类,都需要手动创建相应的代理类,导致代码冗余和复杂度增加。
  • 如果系统中有大量的业务类,则需要编写大量的代理类,这样会使系统变得笨重。

10. JDK动态代理课程笔记

10. 1. 引言

  • 静态代理: 手动创建代理类,代理类持有目标对象的引用并在实现方法中扩展业务逻辑。

    • 要点:
      1. 代理类和委托类必须实现相同的接口。
      2. 代理类需要持有委托类的对象。
  • 问题: 手动创建大量代理类会导致工作量庞大。

10. 2. JDK动态代理概述

  • 引入: JDK 12后,使用反射机制自动生成代理类。
  • 动态代理: 代理类在运行时根据接口结构生成,而不需要手动书写。
  • 区别: 静态代理手动编写,动态代理则在内存中动态生成。

10. 3. 代码实现步骤

3.1 创建工程

  • 创建新的工程,设置 group IDartifact,工程名为 s05

3.2 编写代码

  • 复制之前的 UserService 接口和 UserServiceImpl 实现类。

3.3 创建 ProxyInvocationHandler

  • service 包中创建 ProxyInvocationHandler 类,实现 InvocationHandler 接口。
  • 实现 invoke 方法:
    • 参数:
      1. proxy: 代理类对象(由JDK动态代理生成)。
      2. method: 目标方法信息。
      3. args: 目标方法的实际参数。
    • 实现逻辑:
      • 输出当前时间。
      • 调用目标方法并返回结果。
public class ProxyInvocationHandler implements InvocationHandler {
    private Object target;

    public ProxyInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("当前时间: " + System.currentTimeMillis());
        return method.invoke(target, args);
    }
}

3.4 使用 ProxyInvocationHandler

  • main 方法中:
    1. 创建目标对象 UserService userService = new UserServiceImpl()
    2. 实例化 ProxyInvocationHandler,传入目标对象。
    3. 使用 Proxy.newProxyInstance 创建代理类。
public static void main(String[] args) {
    UserService userService = new UserServiceImpl();
    ProxyInvocationHandler handler = new ProxyInvocationHandler(userService);
    
    UserService proxy = (UserService) Proxy.newProxyInstance(
        userService.getClass().getClassLoader(),
        userService.getClass().getInterfaces(),
        handler
    );
    
    proxy.createUser();
}

10. 4. 注意事项

  • 接口要求: JDK动态代理必须实现接口,如果类不实现接口,则无法使用此机制。
  • Spring解决方案: 对于不实现接口的类,Spring提供CGlib库实现类的增强。

11. JDK动态代理原理与实现细节课程笔记

11. 1. 引言

  • 本课程通过源代码的回顾,深入探讨JDK动态代理的底层原理和实现细节。

11. 2. 重要概念

  • Proxy.newProxyInstance: JDK 反射包中的一个方法,用于根据接口生成对应的代理类。
  • 由于JDK底层实现过程不可见,通过图示化理解其执行过程将更加有助于掌握。

# 11. 3. 执行过程

3.1 创建代理类

  • 执行 Proxy.newProxyInstance 时,会经历三个主要步骤:
    1. 生成字节码文件: 在本地硬盘上创建一个 .class 文件,默认存放在 com.sun.proxy 包下。类名格式为 $Proxy0(以 $ 开头,加上数字)。
    2. 生成代理类: 代理类实现了原有的接口。
    3. 定义类: 通过 defineClass 方法将字节码文件加载到JVM的方法区中。

3.2 伪代码示例

public class $Proxy0 implements UserService {
    private UserService target;

    public $Proxy0(UserService target) {
        this.target = target;
    }

    public void createUser() {
        System.out.println("执行前置逻辑");
        target.createUser();
    }
}
  • 该伪代码展示了生成的代理类结构,持有目标对象的引用。

11. 4. JVM内存操作

  • 代理对象被创建并保存到JVM的堆内存中,持有被代理类的引用。
  • 当调用 userServiceProxy.createUser() 时,实际上会执行生成的代理类中的逻辑。

12. Spring AOP与CGlib课程笔记

12. 1. 引言

  • 上一节课使用代码演示了如何通过JDK动态代理实现Spring AOP功能。
  • JDK动态代理要求目标类必须实现接口,但在实际应用中,许多类并不实现接口,这时需要其他解决方案。

12. 2. CGlib介绍

  • CGlib: Code Generation Library的缩写,是一种运行时字节码增强技术。
  • Spring AOP使用CGlib来扩展没有实现接口的类。
  • 当一个类没有实现接口时,AOP会在运行时生成目标类的继承字节码,从而进行行为扩展。

12. 3. CGlib原理

  • 假设有一个Service类,包含一个findById方法,但该类没有实现接口。
  • 由于无法使用JDK动态代理,Spring会自动使用CGlib通过继承来对类进行扩展。
  • 生成的子类类名格式为:OriginalClassName$$EnhancerByCGLIB

3.1 继承与方法重写

  • 子类会重写父类中的findById方法,逻辑如下:
    1. 执行自定义前置处理代码。
    2. 调用父类的findById方法(使用super.findById())。
    3. 执行自定义后置处理代码。

12. 4. 示例演示

  • 通过之前写好的实例来展示CGlib的使用。
  • S03工程中,UserService类没有实现任何接口。
  • 断点调试程序,可以看到生成的类名包含$$EnhancerBySpringCGLIB,表明使用CGlib进行增强。

12. 5. 变更为接口实现

  • UserService类修改为实现一个接口IUserService
  • 修改引用为IUserService,保持方法调用部分不变。
  • 断点调试,生成的对象类名变为proxy,表示使用了JDK动态代理。

12. 6. AOP实现原理总结

  • 两种情况:
    1. 如果目标类实现了接口,Spring优先使用JDK动态代理生成代理类。
    2. 如果目标类没有实现接口,则使用CGlib通过继承对目标类进行扩展。
  • 了解这些细节在面试中回答Spring AOP实现原理时非常重要。

http://www.kler.cn/news/354207.html

相关文章:

  • 潜水定位通信系统的功能和使用方法_鼎跃安全
  • SpringBoot+Vue+Uniapp智能社区服务小程序系统(源码+lw+部署文档+讲解等)
  • 前缀和--一维和二维模板
  • 【MySQL】索引的机制、使用
  • 机器学习—特性缩放
  • 执行 start.sh 脚本时打开一个单独的运行窗口
  • pdf内容三张以上转图片,使用spire.pdf.free
  • 【选择C++游戏开发技术】
  • 自动驾驶TPM技术杂谈 ———— 惯性导航定位技术
  • 速盾:高防 cdn 提供 cc 防护?
  • 双回路防静电监控仪安全保护生产全流程
  • Linux基础项目开发day2:量产工具——输入系统
  • 【存储设备专栏 2.6 -- linux 启动盘制作详细介绍】
  • Vert.x,Web - Restful API
  • 记EDU某人社局的漏洞挖掘复盘
  • 四种隔离级别是如何逐步加强事务隔离的,以及在底层如何使用锁机制和多版本控制(MVCC)来实现
  • HCIP--1
  • 新媒体优势
  • Spring Boot驱动的在线考试系统:JavaWeb技术实战
  • Scala入门基础(12)抽象类