一、引言
在 Java 开发领域,Spring 框架凭借其强大的功能和丰富的特性,成为了众多开发者构建企业级应用的首选。其中,面向切面编程(AOP)作为 Spring 框架的核心技术之一,为开发者提供了一种全新的程序结构组织方式,能够在不修改原有业务逻辑代码的基础上,实现对程序功能的统一维护和增强。本文将深入探讨 Spring 框架中的 AOP 技术,从概念引入到实际应用,全面解析其原理和使用方法。
二、AOP 概念的引入
在实际的应用开发中,我们常常会遇到这样的场景:在已有的业务逻辑之上,需要添加一些额外的功能,比如权限校验、日志记录、事务管理等。以常见的登录功能为例,假设我们已经实现了基本的登录逻辑,如果要在登录过程中添加权限校验功能,传统的做法可能是直接修改登录相关的源代码。然而,这种方式存在诸多弊端,比如代码的可维护性降低,一旦后续需求发生变化,再次修改代码可能会引发一系列潜在的问题。
而 AOP 技术则提供了另一种解决方案,即不通过修改源代码的方式来添加新的功能。它能够将这些与业务逻辑无关,但又需要在多个地方重复使用的功能(如权限校验)进行横向抽取,独立于业务逻辑之外进行管理,从而实现对程序功能的增强。这种方式不仅提高了代码的可维护性和可重用性,还大大提升了开发效率。
三、AOP 相关的概念
(一)AOP 的概述
AOP,即 Aspect Oriented Programming,面向切面编程。它是一种编程范式,属于软件工程范畴,主要指导开发者如何组织程序结构。AOP 最早由 AOP 联盟提出,并制定了一套规范。Spring 框架将 AOP 思想引入其中,遵循 AOP 联盟的规范。
从技术实现角度来看,AOP 通过预编译方式或者运行期动态代理来实现程序功能的统一维护。它是 OOP(面向对象编程)的延续,在软件开发领域备受关注,也是 Spring 框架的重要组成部分,同时也是函数式编程的一种衍生范型。
利用 AOP,我们可以将业务逻辑的各个部分进行隔离,降低业务逻辑各部分之间的耦合度。例如,将日志记录、事务管理等功能从核心业务逻辑中分离出来,使得这些功能可以独立开发、测试和维护。这样不仅提高了程序的可重用性,还加快了开发进度。AOP 采取横向抽取机制,有效取代了传统纵向继承体系中重复性代码(如事务管理、安全检查、缓存等方面的代码)。学习 AOP 的最大好处在于,我们能够在不修改源代码的前提下,对程序进行增强。
(二)AOP 的优势
- 减少重复的代码:在传统的开发模式中,诸如日志记录、权限校验等功能往往需要在多个业务方法中重复编写代码。而使用 AOP,我们可以将这些通用功能集中实现,通过切面的方式应用到相关的业务方法上,避免了大量重复代码的编写。
- 提高开发的效率:由于 AOP 能够将通用功能与业务逻辑分离,开发人员可以更加专注于业务逻辑的实现,而无需在每个业务方法中都关注那些通用功能的代码编写。这使得开发过程更加高效,能够更快地完成项目开发。
- 维护方便:当通用功能的需求发生变化时,例如日志记录的格式需要调整或者权限校验的规则发生改变,只需要在切面中进行修改,而无需逐个修改涉及到这些功能的业务方法。这大大降低了维护成本,提高了系统的可维护性。
(三)AOP 的底层原理
- JDK 的动态代理技术
-
- 为接口创建代理类的字节码文件:JDK 动态代理会根据目标接口动态生成一个代理类的字节码文件。在这个过程中,代理类会实现目标接口,并在代理方法中调用实际目标对象的方法,同时可以在调用前后添加额外的逻辑,即我们所说的通知。
-
- 使用 ClassLoader 将字节码文件加载到 JVM:生成的代理类字节码文件需要被加载到 Java 虚拟机(JVM)中才能使用。ClassLoader 负责将字节码文件加载到 JVM 内存中,使得程序可以使用该代理类。
-
- 创建代理类实例对象,执行对象的目标方法:当需要使用代理对象时,通过反射机制创建代理类的实例对象。在调用代理对象的方法时,实际会执行代理类中重写的方法,在这个方法中,会先执行通知逻辑,然后调用目标对象的实际方法。
- cglib 代理技术
cglib 代理技术则是为类生成代理对象,无论被代理类是否有接口都可以使用。它的底层原理是通过生成被代理类的子类来实现代理功能。在子类中,对被代理类的方法进行拦截和增强,同样可以在方法调用前后添加通知逻辑。
四、Spring 的 AOP 技术 - 配置文件方式
(一)AOP 相关的术语
- Joinpoint (连接点):在一个类中,那些可以被增强的方法被称为连接点。简单来说,就是程序执行过程中能够插入额外逻辑的位置,通常是方法调用。
- Pointcut (切入点):切入点是对哪些连接点进行拦截的定义。它通过切入点表达式来指定,用于确定具体要对哪些方法进行增强操作。例如,我们可以通过切入点表达式指定只对某个类中的特定方法进行增强。
- Advice (通知 / 增强):通知是指拦截到连接点之后所要执行的操作。通知可以分为前置通知、后置通知、异常通知、最终通知和环绕通知等类型。这些通知代表了切面要完成的具体功能。比如,前置通知在目标方法执行前执行,后置通知在目标方法执行成功后执行等。
- Aspect (切面):切面是切入点和通知的结合。在实际开发中,我们需要自己编写和配置切面,将切入点表达式与相应的通知关联起来,以实现对特定方法的增强功能。
(二)基本准备工作
AspectJ 是一个面向切面的框架,它对 Java 语言进行了扩展,定义了 AOP 语法。实际上,AspectJ 是对 AOP 编程思想的一种实践。在使用 Spring 的 AOP 技术时,我们常常会借助 AspectJ 的相关功能。
(三)AOP 配置文件方式的入门
- 创建 maven 项目,添加坐标依赖:首先,我们需要创建一个 maven 项目,并在项目的 pom.xml 文件中添加相关的依赖坐标。这些依赖包括 Spring 的核心上下文依赖、日志依赖、测试依赖以及 AOP 相关的依赖,如 aopalliance、spring-aspects 和 aspectjweaver 等。具体的依赖配置如下:
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.12</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<!--AOP联盟-->
<dependency>
<groupId>aopalliance</groupId>
<artifactId>aopalliance</artifactId>
<version>1.0</version>
</dependency>
<!--Spring Aspects-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<!--aspectj-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.3</version>
</dependency>
</dependencies>
- 创建被增强的类:接下来,我们创建一个被增强的类。例如,创建一个名为 User 的类,其中包含一些方法,这些方法将作为连接点供我们进行增强操作。
// 被增强的类
public class User {
//连接点/切入点
public void add(){
System.out.println("add......");
}
public void update(){
System.out.println("update......");
}
}
- 将目标类配置到 Spring 中:在 Spring 的配置文件(applicationContext.xml)中,将 User 类配置为一个 Bean,以便 Spring 容器进行管理。
<bean id="user" class="com.aopImpl.User"></bean>
- 定义切面类:创建一个切面类,例如 UserProxy 类,在该类中定义各种通知方法。比如,定义一个前置通知方法 before ()。
public class UserProxy {
//增强/通知 ---》前置通知
public void before(){
System.out.println("before.............");
}
}
- 在配置文件中定义切面类:同样在 Spring 配置文件中,将 UserProxy 类也配置为一个 Bean。
<bean id="userProxy" class="com.aopImpl.UserProxy"></bean>
- 在配置文件中完成 aop 的配置:最后,在配置文件中进行 AOP 的核心配置。通过<aop:config>标签来配置切面,将切入点和通知关联起来。例如,配置一个前置通知,在 User 类的 add () 方法执行前进行增强。
<!--配置切面-->
<aop:config>
<!--配置切面 = 切入点 + 通知组成-->
<aop:aspect ref="userProxy">
<!--前置通知:UserServiceImpl的save方法执行前,会增强-->
<!--pointcut:后边是切入点表达式,作用是知道对对面的那个方法进行增强-->
<aop:before method="before" pointcut="execution(public void com.aopImpl.User.add())"/>
</aop:aspect>
</aop:config>
- 完成测试:编写测试类,通过 Spring 的 ApplicationContext 获取 User 类的实例,并调用其 add () 方法,观察前置通知是否生效。
public class DemoTest {
@Test
public void aopTest1(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
User user = (User) applicationContext.getBean("user");
user.add();
}
}
(四)切入点的表达式
在配置切入点时,需要使用切入点表达式来精确指定要增强的方法。切入点表达式的格式如下:
execution([修饰符] [返回值类型] [类全路径] [方法名 ( [参数] )])
- 修饰符:可以省略不写,不是必须出现的部分。
- 返回值类型:不能省略,需要根据实际方法的返回值类型编写,也可以使用*代替所有返回值类型。
- 包名、类名、方法名和参数的规则:
-
- 包名、类名和方法名通常不能省略,但可以使用*进行通配。例如,com.qcby.demo3.BookDaoImpl.save(),可以使用*来简化表示,如com.qcby.*.BookDaoImpl.save()表示com.qcby包下所有以BookDaoImpl结尾的类中的save方法。
-
- 中间的包名可以使用*号代替,类名也可以使用*号代替,还可以使用类似*DaoImpl的写法。方法同样可以使用*号代替。
-
- 参数部分,如果是一个参数可以使用*号代替,如果想代表任意参数则使用..。
以下是一些常见的切入点表达式示例:
- 比较通用的表达式:execution(* com.qcby.*.ServiceImpl.save(..)),表示对com.qcby包下所有以ServiceImpl结尾的类中的save方法进行增强,且方法可以接受任意类型和数量的参数。
- 对某个类中所有方法进行增强:execution(* com.qcby.*.ServiceImpl.*(..)),表示对com.qcby包下所有以ServiceImpl结尾的类中的所有方法进行增强,方法接受任意参数。
- 对某个包中所有方法进行增强:execution(* com.qcby.*.*.*(..)),表示对com.qcby包及其子包下的所有类中的所有方法进行增强,方法接受任意参数。
(五)AOP 的通知类型
- 前置通知:在目标方法执行前进行增强。如上述配置案例中,在 User 类的 add () 方法执行前,会执行 before () 方法中的逻辑。
- 环绕通知:在目标方法执行前后都可以进行增强。需要注意的是,在环绕通知方法中,目标对象的方法需要手动调用。例如:
// 环绕通知
public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("before.............");
// 执行被增强的方法
proceedingJoinPoint.proceed();
System.out.println("after.............");
}
在 xml 配置中,使用<aop:around>标签来配置环绕通知:
<aop:around method="around" pointcut="execution(* com.*.User.add(..))"/>
- 最终通知:无论目标方法执行成功还是失败,都会进行增强。例如:
// 最终通知
public void after() {
System.out.println("after.............");
}
xml 配置如下:
<aop:after method="after" pointcut="execution(* com.*.User.add(..))"/>
- 后置通知:在目标方法执行成功后进行增强。例如:
//后置通知
public void afterReturning() {
System.out.println("afterReturning.............");
}
xml 配置:
<aop:after-returning method="afterReturning" pointcut="execution(public void com.aopImpl.User.add())"/>
- 异常通知:在目标方法执行失败后进行增强,只有当目标方法发生异常时才会执行。例如:
//异常通知
public void afterThrowing() {
System.out.println("afterThrowing.............");
}
需要注意的是,为了触发异常通知,需要在目标方法中故意制造异常。例如:
//连接点/切入点
public void add(){
int a = 10 / 0;
System.out.println("add......");
}
xml 配置:
<aop:after-throwing method="afterThrowing" pointcut="execution(public void com.aopImpl.User.add())"/>
五、Spring 的 AOP 技术 - 注解方式
(一)AOP 注解方式入门程序
- 创建 maven 工程,导入坐标:与配置文件方式类似,首先创建一个 maven 项目,并导入相关的依赖坐标,包括 Spring 的核心依赖、AOP 相关依赖等。
- 编写接口,完成 IOC 的操作:根据实际业务需求,编写相关的接口,并通过 Spring 的 IOC 机制进行配置和管理,这里步骤略。
- 编写切面类:创建一个切面类,例如 UserProxy 类。给切面类添加@Aspect注解,表明该类是一个切面。然后在类中编写各种增强方法,并使用相应的通知类型注解来声明。
- 配置 xml 扫描注解:在 Spring 的配置文件(applicationContext.xml)中,配置注解扫描,确保 Spring 容器能够识别并处理这些注解。同时,需要引入相关的命名空间。
5.配置文件中开启自动代理:在 Spring 配置文件中,通过<aop:aspectj-autoproxy>标签开启 AspectJ 自动代理功能,使得 Spring 能够基于注解创建代理对象来实现 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: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">
<!--开启注解扫描-->
<context:component-scan base-package="com.aopImpl"></context:component-scan>
<!--开启Aspect生成代理对象-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
- 通知类型注解:在切面类中,使用不同的通知类型注解来定义增强逻辑。
-
- @Before -- 前置通知:用于在目标方法执行前执行增强逻辑。例如:
@Component
@Aspect //生成代理对象
public class UserProxy {
//增强/通知 ---》前置通知
@Before(value = "execution(* com.*.User.add(..))")
public void before(){
System.out.println("before.............");
}
}
- @AfterReturning -- 后置通知:在目标方法成功执行返回后执行增强逻辑。
//后置通知
@AfterReturning(value = "execution(* com.*.User.add(..))")
public void afterReturning() {
System.out.println("afterReturning.............");
}
- @Around -- 环绕通知:可以在目标方法执行前后都执行增强逻辑,且目标对象的方法默认不执行,需要手动通过ProceedingJoinPoint的proceed()方法来触发。
// 环绕通知
@Around(value = "execution(* com.*.User.add(..))")
public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("before.............");
// 执行被增强的方法
proceedingJoinPoint.proceed();
System.out.println("after.............");
}
- @After -- 最终通知:无论目标方法执行成功与否,都会执行该通知的增强逻辑。
// 最终通知
@After(value = "execution(* com.*.User.add(..))")
public void after() {
System.out.println("after.............");
}
- @AfterThrowing -- 异常抛出通知:当目标方法抛出异常时执行增强逻辑。
//异常通知
@AfterThrowing(value = "execution(* com.*.User.add(..))")
public void afterThrowing() {
System.out.println("afterThrowing.............");
}
- 测试类:编写测试类来验证 AOP 注解配置是否生效。通过ApplicationContext获取被增强的对象,并调用其方法,观察通知是否按照预期执行。
@Test
public void aopTest1(){
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
User user = applicationContext.getBean(User.class);
user.add();
}
六、AOP 在实际项目中的应用场景
(一)日志记录
在企业级应用中,日志记录是非常重要的功能。通过 AOP,我们可以在不修改业务逻辑代码的情况下,为所有需要记录日志的方法添加日志记录功能。例如,在方法执行前记录方法的开始时间和入参,在方法执行后记录方法的结束时间和返回值。这样可以方便我们进行系统调试、性能分析以及问题排查。
@Component
@Aspect
public class LoggingAspect {
@Before("execution(* com.example.service.*.*(..))")
public void logBefore(JoinPoint joinPoint) {
System.out.println("开始执行方法:" + joinPoint.getSignature().getName());
System.out.println("入参:" + Arrays.toString(joinPoint.getArgs()));
}
@AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result")
public void logAfterReturning(JoinPoint joinPoint, Object result) {
System.out.println("方法 " + joinPoint.getSignature().getName() + " 执行成功,返回值:" + result);
}
}
(二)事务管理
事务管理是保证数据一致性和完整性的关键。使用 AOP 可以将事务管理的逻辑从业务代码中分离出来。通过配置事务切面,在方法执行前开启事务,在方法执行成功后提交事务,在方法抛出异常时回滚事务。
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="save*" propagation="REQUIRED"/>
<tx:method name="update*" propagation="REQUIRED"/>
<tx:method name="delete*" propagation="REQUIRED"/>
<tx:method name="*" read-only="true"/>
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut id="serviceMethods" expression="execution(* com.example.service.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="serviceMethods"/>
</aop:config>
(三)权限校验
在系统中,不同的用户角色可能具有不同的操作权限。利用 AOP 可以在方法调用前进行权限校验,确保只有具有相应权限的用户才能执行特定的方法。例如,在一个电商系统中,只有管理员用户才能执行商品删除操作。
@Component
@Aspect
public class PermissionAspect {
@Before("execution(* com.example.controller.AdminController.deleteProduct(..))")
public void checkPermission(JoinPoint joinPoint) {
// 假设通过SecurityContext获取当前用户角色
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
if (!authorities.contains(new SimpleGrantedAuthority("ADMIN"))) {
throw new AccessDeniedException("没有权限执行该操作");
}
}
}
七、总结
Spring 框架中的 AOP 技术为开发者提供了一种强大而灵活的编程方式,能够有效地将通用功能与业务逻辑分离,提高代码的可维护性、可重用性和开发效率。通过配置文件方式和注解方式,我们可以轻松地实现 AOP 功能,包括定义切入点、通知和切面。在实际项目中,AOP 在日志记录、事务管理、权限校验等多个方面都有着广泛的应用。深入理解和熟练掌握 AOP 技术,将有助于我们构建更加健壮、高效的 Java 企业级应用。希望本文能够帮助读者全面掌握 Spring 框架中 AOP 技术的精髓,并在实际开发中灵活运用,提升项目的质量和开发效率。