在Java面向对象编程中,解决代码重复是一个重要的目标,旨在提高代码的可维护性、可读性和复用性。你提到的两个步骤——抽取成方法和抽取类,是常见的重构手段。然而,正如你所指出的,即使抽取成类,有时仍然会面临代码重复的问题,尤其是当某些逻辑(如事务管理、日志记录等)需要跨多个业务类和方法时。
AOP(面向切面编程)的核心理念就是将那些散布在多个业务逻辑代码中的相同或相似(即横切关注点,cross-cutting concerns)的代码,通过横向切割(即非传统的纵向继承或组合方式)的方式抽取到一个独立的模块(称为切面,Aspect)中。这种方式使得这些横切关注点与业务逻辑代码分离,从而提高了代码的模块性、可维护性和复用性。
AOP使得开发者能够更专注于业务逻辑的实现,而不必在每个业务逻辑代码中重复编写那些与业务逻辑不直接相关的代码(如日志记录、事务管理、安全控制等)。同时,它也提供了一种灵活的方式来动态地添加或修改这些横切关注点,而无需修改业务逻辑代码本身。
AOP的首要解决问题不仅仅是将重复性的逻辑代码横切出来封装成一个类,更重要的是将这些横切关注点(cross-cutting concerns)以非侵入式的方式融合到业务逻辑代码中,而不需要修改业务逻辑代码本身。
AOP(面向切面编程)确实是对OOP(面向对象编程)的一种重要补充,特别是在处理那些与业务逻辑不直接相关但又必须跨越多个业务模块的横切关注点时。AOP通过提供一种非侵入式的方式来增强业务逻辑,使得开发者能够专注于业务逻辑的实现,而无需关心那些横切关注点的处理。
正如您所提到的,AOP在日志记录、性能统计、安全控制等场景中非常有用。这些功能通常是跨越多个业务模块的,如果直接在每个业务逻辑代码中添加这些功能的代码,不仅会导致代码冗余和难以维护,还会增加业务逻辑代码与这些非业务功能的耦合度。通过使用AOP,我们可以将这些功能封装成独立的切面,并通过声明式的方式在需要的地方应用这些切面,从而实现横切关注点的非侵入式融合。
AOP(面向切面编程)的实际应用案例非常广泛,包括但不限于事务管理、日志记录、安全控制、性能监控等。下面我将以日志记录为例,详细介绍一个基于注解的AOP实现方式。
AOP的代理
AOP(面向切面编程)的代理机制是AOP实现的核心技术之一,用于在目标对象执行方法时动态插入切面逻辑(如日志记录、权限校验、事务管理等)。以下是AOP代理机制的关键点解析:
1. AOP代理的基本概念
- 代理(Proxy):代理是目标对象的替代对象,客户端通过代理调用目标对象的方法。代理在方法调用前后或异常时,插入切面逻辑。
- 目标对象(Target Object):被代理的实际业务对象。
- 切面(Aspect):定义了横切关注点的模块,例如日志、事务等。
2. AOP代理的实现方式
AOP代理主要有两种实现方式:JDK动态代理和CGLIB代理。
(1)JDK动态代理
- 原理:基于Java的反射机制,通过
java.lang.reflect.Proxy
类动态生成代理类。 - 特点:
- 只能代理实现了接口的目标对象。
- 代理类在运行时动态生成,无需手动编写代理类。
- 适用场景:目标对象实现了接口时,优先使用JDK动态代理。
(2)CGLIB代理
- 原理:基于ASM字节码生成框架,通过继承目标类生成子类作为代理类。
- 特点:
- 可以代理未实现接口的目标对象。
- 代理类在运行时动态生成,通过字节码技术实现。
- 适用场景:目标对象未实现接口时,使用CGLIB代理。
3. Spring AOP的代理选择
Spring AOP默认使用JDK动态代理,但在以下情况下会切换为CGLIB代理:
- 目标对象未实现接口:Spring会自动使用CGLIB代理。
- 强制使用CGLIB:通过配置
proxy-target-class="true"
强制使用CGLIB。
示例配置:
<aop:config proxy-target-class="true"/>
或在Java配置中:
@EnableAspectJAutoProxy(proxyTargetClass = true)
4. 代理机制的工作流程
- 切面定义:开发者定义切面(Aspect),指定切入点(Pointcut)和通知(Advice)。
- 代理生成:Spring AOP根据目标对象是否实现接口,选择JDK动态代理或CGLIB代理生成代理对象。
- 方法调用:客户端调用代理对象的方法,代理对象在方法执行前后插入切面逻辑。
- 目标方法执行:代理对象调用目标对象的实际方法。
- 返回结果:代理对象将目标方法的返回值返回给客户端。
5. JDK动态代理 vs CGLIB代理
特性 | JDK动态代理 | CGLIB代理 |
---|---|---|
代理对象类型 | 接口的代理类 | 目标类的子类 |
依赖 | Java内置反射机制 | ASM字节码生成框架 |
性能 | 性能较低(反射调用) | 性能较高(直接调用子类方法) |
目标对象要求 | 必须实现接口 | 不需要实现接口 |
使用场景 | 优先使用,适用于接口代理 | 适用于未实现接口的类代理 |
6. 代理机制的注意事项
- 自我调用问题:如果目标对象内部调用自己的方法(如
this.method()
),代理不会生效,因为内部调用不会经过代理对象。- 解决方案:
- 将方法提取到外部,通过代理调用。
- 使用
AopContext.currentProxy()
获取当前代理对象。
- 解决方案:
- final类/方法:JDK动态代理和CGLIB代理都无法代理
final
类或final
方法,因为final
类无法继承,final
方法无法重写。 - 私有方法:代理机制无法访问私有方法,因为私有方法无法被重写或反射调用。
7. 总结
- AOP代理机制通过JDK动态代理或CGLIB代理,在目标对象方法执行前后插入切面逻辑。
- JDK动态代理适用于实现了接口的目标对象,CGLIB代理适用于未实现接口的目标对象。
- Spring AOP默认使用JDK动态代理,必要时会自动切换为CGLIB代理。
- 代理机制是AOP实现横切关注点的核心技术,广泛应用于日志记录、事务管理、权限校验等场景。
通过代理机制,AOP实现了横切关注点的模块化,降低了代码耦合度,提高了代码的可维护性和可扩展性。
AOP日志记录案例
1. 项目环境准备
首先,确保你的项目中已经包含了Spring框架和AspectJ的相关依赖。对于Maven项目,你可以在pom.xml
文件中添加如下依赖(注意替换为适合你项目的版本):
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.3.24</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.7</version>
</dependency>
2. 定义业务逻辑类
假设你有一个业务逻辑类UserService
,里面包含了一些用户操作的方法,如addUser
、deleteUser
等。
@Service
public class UserService {
public void addUser(User user) {
// 业务逻辑代码...
System.out.println("Adding user: " + user.getName());
}
public void deleteUser(Long userId) {
// 业务逻辑代码...
System.out.println("Deleting user with ID: " + userId);
}
// 其他方法...
}
3. 定义日志切面类
接下来,定义一个日志切面类LogAspect
,用于在业务方法执行前后记录日志。
@Aspect
@Component
public class LogAspect {
// 定义切点,匹配UserService类中的所有方法
@Pointcut("execution(* com.yourpackage.UserService.*(..))")
public void userServiceMethods() {}
// 前置通知,在目标方法执行前运行
@Before("userServiceMethods()")
public void logBefore(JoinPoint joinPoint) {
System.out.println("Before method: " + joinPoint.getSignature().getName());
}
// 后置通知,在目标方法执行后运行(无论是否抛出异常)
@After("userServiceMethods()")
public void logAfter(JoinPoint joinPoint) {
System.out.println("After method: " + joinPoint.getSignature().getName());
}
// 返回通知,在目标方法正常返回后运行
@AfterReturning(value = "userServiceMethods()", returning = "result")
public void logAfterReturning(JoinPoint joinPoint, Object result) {
System.out.println("Returning from method: " + joinPoint.getSignature().getName() + " with result: " + result);
}
// 异常通知,在目标方法抛出异常后运行
@AfterThrowing(value = "userServiceMethods()", throwing = "ex")
public void logAfterThrowing(JoinPoint joinPoint, Throwable ex) {
System.out.println("Exception in method: " + joinPoint.getSignature().getName() + " - " + ex.getMessage());
}
// 环绕通知,可以在目标方法执行前后自定义行为
@Around("userServiceMethods()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("Around before method: " + joinPoint.getSignature().getName());
Object result = joinPoint.proceed(); // 执行目标方法
System.out.println("Around after method: " + joinPoint.getSignature().getName());
return result;
}
}
4. 开启AOP自动代理
在你的Spring配置类中,或者通过XML配置,确保开启了AOP自动代理。如果使用Java配置,可以添加@EnableAspectJAutoProxy
注解到你的配置类上。
@Configuration
@EnableAspectJAutoProxy
@ComponentScan(basePackages = "com.yourpackage")
public class AppConfig {
// 配置类内容...
}
5. 测试
现在,当你调用UserService
中的任何:
@Test
void test() {
User user = new User();
user.setId(1L);
user.setUserName("zhaoshuai-lc");
userService.addUser(user);
}
Around before method: addUser
Before method: addUser
Adding user: zhaoshuai-lc
Returning from method: addUser with result: null
After method: addUser
Around after method: addUser
execute
表达式,在AOP(面向切面编程)的上下文中,通常指的是execution
表达式,它是用于定义切点(Pointcut)的一种表达式,用于指定哪些方法会被AOP增强(即应用通知)。execution
表达式是Spring AOP中最重要的Pointcut表达式之一,它允许你精确地指定哪些类的哪些方法应该被拦截。
execution表达式的基本语法
execution
表达式的基本语法如下:
execution(修饰符 返回值类型 声明类型? 方法名(参数类型列表) throws 异常类型列表?)
- 修饰符:方法的访问修饰符,如
public
、private
等,通常可以省略。 - 返回值类型:方法的返回类型,不能省略,可以使用
*
表示任意类型。 - 声明类型:方法的声明类,可以省略,也可以使用
*
表示任意类。 - 方法名:方法的名称,不能省略,可以使用
*
进行通配。 - 参数类型列表:方法的参数类型列表,括号内的
..
表示任意数量的参数,参数类型之间用逗号分隔,可以使用*
表示任意类型。 - 异常类型列表:方法声明抛出的异常类型列表,通常省略。
execution表达式的使用示例
-
拦截所有public方法
execution(public * *(..))
这个表达式会匹配所有public方法的执行。
-
拦截以set开头的任意方法
execution(* set*(..))
这个表达式会匹配所有以
set
开头的方法的执行。 -
拦截指定类中的方法
execution(* com.example.service.UserService.*(..))
这个表达式会匹配
com.example.service.UserService
类中所有方法的执行。 -
拦截指定包及其子包中所有类的所有方法
execution(* com.example.service..*.*(..))
这个表达式会匹配
com.example.service
包及其所有子包中所有类的所有方法的执行。
注意事项
- 在使用
execution
表达式时,需要确保表达式的语法正确,特别是参数类型列表和异常类型列表部分,因为它们是可选的,但如果不写,需要使用括号()
来占位。 execution
表达式是Spring AOP中用于定义切点的主要方式之一,但它并不是唯一的方式。Spring AOP还支持其他类型的Pointcut表达式,如within
、this
、target
等,这些表达式可以根据不同的需求进行组合使用。- 在定义切点时,可以根据需要选择是否包含修饰符、异常类型列表等部分,但返回值类型、声明类型(或省略)、方法名和参数类型列表是必需的。
综上所述,execution
表达式是Spring AOP中用于定义切点的一种强大工具,它允许你精确地指定哪些方法的执行应该被拦截,并通过AOP技术对这些方法进行增强。
AOP(Aspect Oriented Programming,面向切面编程)是一种编程范式,它旨在通过将关注点(如日志记录、事务管理、权限控制等)分离出来,使其独立于业务逻辑代码,从而提高代码的可维护性和可重用性。AOP的核心概念主要包括以下几个方面:
1. 切面(Aspect)
切面(Aspect)是面向切面编程(AOP)中的一个核心概念。在AOP中,切面代表了横切关注点的模块化,即将那些与业务逻辑不直接相关,但却会影响到多个业务组件的公共行为(如日志记录、事务管理、安全控制等)封装成一个独立的模块。这个模块就是切面。
切面主要由两部分组成:
-
切点(Pointcut):定义了哪些连接点(Join Point)会被切面所增强。连接点是程序执行中的某个点,比如方法调用、字段访问等。在Spring AOP中,连接点通常指的是方法的执行点。切点通过一种表达式(如execution表达式)来指定,它决定了切面中的增强逻辑将会应用到哪些具体的连接点上。
-
通知(Advice):定义了切面在特定连接点上要执行的动作。通知是切面的具体实现,它包含了要增强的逻辑。根据执行时机和方式的不同,通知可以分为多种类型,如前置通知(Before)、后置通知(After)、返回通知(After Returning)、异常通知(After Throwing)和环绕通知(Around)等。
切面通过切点和通知的组合,实现了对目标对象(Target Object)的增强。在运行时,AOP框架会为目标对象创建一个代理对象(AOP Proxy),这个代理对象会拦截对目标对象的调用,并根据切点的定义,在适当的时机执行通知中的增强逻辑。这样,就可以在不修改目标对象代码的情况下,为其添加额外的行为,实现了横切关注点的模块化。
在Spring AOP中,通知(Advice)是切面(Aspect)中的核心部分,用于定义在连接点(Joinpoint)处应该执行的操作。根据执行时机和方式的不同,Spring AOP支持五种类型的通知:前置通知(Before advice)、正常返回通知(After returning advice)、异常返回通知(After throwing advice)、后置通知(After (finally) advice)和环绕通知(Around advice)。这些通知的执行顺序在Spring AOP中是有明确规定的。
通知的执行顺序
当多个通知作用于同一个连接点时,它们的执行顺序如下(以方法调用为连接点为例):
-
环绕通知(Around advice):首先执行环绕通知的“前部分”,即在目标方法执行之前执行的逻辑。然后,它会调用
ProceedingJoinPoint
的proceed()
方法来执行目标方法。如果目标方法正常执行完成,环绕通知会接着执行其“后部分”,即目标方法执行之后的逻辑。如果目标方法抛出异常,则环绕通知的“后部分”会在捕获异常后执行(如果环绕通知中进行了异常处理)。 -
前置通知(Before advice):在环绕通知的“前部分”执行完毕后,紧接着执行前置通知。前置通知在目标方法执行之前执行,但它不能阻止目标方法的执行(除非它抛出了异常)。
-
目标方法(Target method):前置通知执行完毕后,执行目标方法本身。
-
正常返回通知(After returning advice):如果目标方法正常执行完成并返回结果,则在环绕通知的“后部分”执行完毕后(如果存在环绕通知的话),执行正常返回通知。如果目标方法抛出异常,则不会执行正常返回通知。
-
异常返回通知(After throwing advice):如果目标方法执行过程中抛出了异常,则在环绕通知的“后部分”执行完毕后(如果存在环绕通知,并且环绕通知中捕获了异常),执行异常返回通知。
-
后置通知(After (finally) advice):无论目标方法是正常返回还是抛出异常,后置通知都会执行。它类似于Java中的
finally
块,用于执行一些清理工作。