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 层:包括
EmployeeDao
和UserDao
,分别用于对员工表和用户表的数据增删改查。 -
Service 层:包括
EmployeeService
和UserService
,分别提供了员工相关的业务逻辑和用户管理的业务逻辑。-
EmployeeService
:提供entry
方法,模拟员工入职操作。 -
UserService
:提供createUser
方法(创建用户)和generateRandomPassword
方法(生成随机密码)。
-
-
DAO 层:包括
这些类的业务逻辑非常常规,但本节课我们将通过 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(); // 模拟创建用户的过程
}
}
运行代码后,可以看到控制台输出显示了 UserService
和 UserDao
中各个方法的执行情况。
2. 6. Spring AOP - 方法执行时间打印需求实现
课程目标
通过 AOP 实现对 Service
或 DAO
层中任意方法的执行时间进行打印,并避免在每个方法中手动增加日志打印代码。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 配置,使得在调用 Service
或 DAO
方法时,自动打印方法的执行时间。
配置 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 运行效果
- 在运行程序时,任何
Service
或DAO
层方法执行前,控制台都会打印方法执行的时间、类名和方法名。 - 示例输出:
-----
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
,返回值可以是void
或Object
,具体取决于通知类型。 - 需包含
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 目标类和目标方法
-
目标类和目标方法:指真正执行业务逻辑的类和方法,例如
Service
或DAO
层中的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. 方法结构与切点表达式
一个完整的方法结构包含以下部分:
- 修饰符(如
public
、private
等)。 - 返回值类型(如
void
、String
)。 - 类的完整路径(如
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))" />
- 匹配参数为
String
和int
类型的方法。
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. 环绕通知的关键点
-
ProceedingJoinPoint:
ProceedingJoinPoint
是JoinPoint
的升级版,除了获取目标方法的信息,还可以控制目标方法的执行。- 关键方法:
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"