Spring AOP + JoinPoint + 反射 实战案例
这是工作中最常用 的组合:AOP 切面获取连接点 (JoinPoint) → 用反射拿到目标对象、方法、参数、字段 → 动态操作。
我直接给你3 个最实用的场景,代码可直接复制到 Spring 项目中运行!
先说明核心关系
- JoinPoint :AOP 提供的连接点对象,能拿到目标方法、目标对象、参数
- 反射 :通过 JoinPoint 拿到的
Method/Object,用反射动态执行、赋值、获取信息 - 组合作用:不修改业务代码,统一做日志、参数校验、权限、字段赋值等
第一步:统一准备环境
1. 引入 AOP 依赖(Maven)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2. 启动类开启 AOP
java
@SpringBootApplication
@EnableAspectJAutoProxy // 开启AOP
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
3. 测试业务类(目标方法)
java
@Service
public class UserService {
// 目标方法:AOP + 反射 作用在这里
public UserDTO updateUser(String id, UserDTO userDTO) {
System.out.println("执行业务方法:更新用户");
userDTO.setId(id);
return userDTO;
}
}
// 用户DTO
class UserDTO {
private String id;
private String username;
private Integer age;
// getter/setter/toString
}
案例 1:最常用 → AOP + JoinPoint + 反射 打印完整请求日志
场景:自动打印目标方法名、参数名、参数值、返回值(反射解析参数)
java
@Component
@Aspect
@Order(1)
public class LogAspect {
@Around("execution(* com.example.service..*.*(..))") // 切入service包
public Object aroundLog(ProceedingJoinPoint joinPoint) throws Throwable {
// ========== 1. 从JoinPoint获取核心信息 ==========
Object targetObj = joinPoint.getTarget(); // 目标对象
String methodName = joinPoint.getSignature().getName(); // 方法名
Object[] args = joinPoint.getArgs(); // 方法参数
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod(); // 目标方法
System.out.println("===== AOP 环绕通知开始 =====");
System.out.println("目标对象:" + targetObj.getClass().getName());
System.out.println("目标方法:" + methodName);
// ========== 2. 反射获取参数名 + 参数值 ==========
System.out.println("===== 反射解析参数 =====");
Parameter[] parameters = method.getParameters();
for (int i = 0; i < parameters.length; i++) {
String paramName = parameters[i].getName(); // 参数名
Object paramValue = args[i]; // 参数值
System.out.println("参数名:" + paramName + ",参数值:" + paramValue);
}
// 执行目标方法
Object result = joinPoint.proceed();
// ========== 3. 反射打印返回值类型和内容 ==========
System.out.println("===== 反射解析返回值 =====");
System.out.println("返回值类型:" + result.getClass().getSimpleName());
System.out.println("返回值:" + result);
return result;
}
}
运行效果
java
===== AOP 环绕通知开始 =====
目标对象:com.example.service.UserService
目标方法:updateUser
===== 反射解析参数 =====
参数名:id,参数值:1001
参数名:userDTO,参数值:UserDTO(id=null, username=张三, age=20)
执行业务方法:更新用户
===== 反射解析返回值 =====
返回值类型:UserDTO
返回值:UserDTO(id=1001, username=张三, age=20)
案例 2:AOP + JoinPoint + 反射 动态修改方法参数
场景 :不改动业务代码,切面统一给参数对象自动赋值字段
java
@Component
@Aspect
@Order(2)
public class ParamFillAspect {
@Before("execution(* com.example.service..*.*(..))")
public void beforeFillParam(JoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
// 遍历参数,找到 UserDTO 类型
for (Object arg : args) {
if (arg instanceof UserDTO) {
UserDTO userDTO = (UserDTO) arg;
// ========== 反射:动态给字段赋值 ==========
Field ageField = UserDTO.class.getDeclaredField("age");
ageField.setAccessible(true);
ageField.set(userDTO, 99); // 强制把年龄改成99
Field usernameField = UserDTO.class.getDeclaredField("username");
usernameField.setAccessible(true);
usernameField.set(userDTO, "AOP自动修改");
System.out.println("AOP+反射:已自动修改参数对象字段");
}
}
}
}
效果
业务方法拿到的 UserDTO:
plaintext
UserDTO(id=null, username=AOP自动修改, age=99)
案例 3:AOP + JoinPoint + 反射 调用目标对象的任意方法
场景 :切面中反射执行目标对象的私有 / 公有方法
java
@Component
@Aspect
public class ReflectInvokeAspect {
@AfterReturning("execution(* com.example.service..*.*(..))")
public void afterInvoke(JoinPoint joinPoint) throws Throwable {
// 1. 获取目标对象
Object target = joinPoint.getTarget();
// 2. 反射获取方法(假设UserService有一个私有方法)
Method logMethod = UserService.class.getDeclaredMethod("logExecute");
logMethod.setAccessible(true);
// 3. 反射调用
logMethod.invoke(target);
}
}
核心 API 速记(面试 + 工作必背)
1. JoinPoint 常用方法
表格
| 方法 | 作用 |
|---|---|
joinPoint.getTarget() |
获取目标对象(反射的源头) |
joinPoint.getSignature() |
获取方法签名 |
joinPoint.getArgs() |
获取方法参数数组 |
((MethodSignature) signature).getMethod() |
获取Method 对象(反射核心) |
2. 反射 + AOP 固定套路
java
// 1. 获取方法
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
// 2. 获取参数
Object[] args = joinPoint.getArgs();
// 3. 获取目标对象
Object target = joinPoint.getTarget();
// 4. 反射操作(字段/方法)
Field field = XXX.class.getDeclaredField("name");
field.setAccessible(true);
field.set(对象, 值);
总结
- JoinPoint 是 AOP 与反射的桥梁,能拿到目标对象、方法、参数
- 环绕通知 @Around 最常用,能在方法执行前后用反射做任何操作
- 私有成员必须用:
getDeclaredXxx()+setAccessible(true) - 三大实战场景:统一日志、自动赋值、动态调用方法
你现在看到的这段代码,是 Spring AOP + 反射 最经典、最真实的企业级用法 ------公共字段自动填充(比如 createTime、updateTime、createUser、updateUser)。
我一句一句给你讲明白:
核心结论(先记住)
这个 @Before 通知不需要返回任何东西!!!
它也不用返回!因为它直接修改了 "参数对象本身"!
1. 为什么不需要返回值?
因为:
java
Object[] args = joinPoint.getArgs();
Object entity = args[0]; // 拿到方法的第一个参数
这里的 entity 是一个对象 ,是引用类型。
你在切面里修改它,等于直接修改了目标方法里的那个对象!
举个超级好懂的比喻:
目标方法:
java
public void update(User user) {
// 这里的 user 和切面里的 entity 是同一个对象!
}
切面里:
java
entity.setUpdateTime(now);
相当于:你直接把传进去的 user 对象改了!
所以 根本不需要 return!因为对象是同一个,你改它,目标方法马上就能看到!
2. 它到底是怎么生效的?(完整流程)
我给你走一遍流程,你瞬间就懂:
-
调用 mapper 方法
javauserMapper.insert(user); -
AOP 拦截到这个方法
java@Before("autoFillPointCut()") -
拿到方法参数:user 对象
javaObject[] args = joinPoint.getArgs(); Object entity = args[0]; // 就是那个 user -
用反射调用 set 方法,给 user 赋值
javasetCreateTime.invoke(entity, now); -
赋值完直接结束,不需要返回 因为 user 对象已经被改好了!
-
目标方法继续执行此时 user 已经有:
- createTime
- createUser
- updateTime
- updateUser
3. 关键:为什么不用 return?
因为 Java 是引用传递!
切面里拿到的 entity和目标方法里的 参数对象是 同一个内存地址、同一个对象!
你在切面里改它,目标方法里的对象自动变了!
所以 不需要返回任何东西!
4. 这段代码的核心原理(一句话总结)
@Before 不返回值,它直接修改方法参数里的实体对象,目标方法直接使用修改后的对象。
5. 我再给你看一眼关键代码
java
// 拿到实体对象(引用!)
Object entity = args[0];
// 反射调用 set 方法,修改这个对象
setCreateTime.invoke(entity, now);
invoke 不返回新对象,它是修改原对象!
最终超级大白话解释
你可以把它理解成:
你进房间(目标方法),手里拿着一个包(entity)。AOP 切面在门口拦住你,把你的包拿过来,塞进去一些东西(赋值)。然后把包还给你,你继续进房间。你根本不知道包被动过,但包里已经有东西了。
不需要返回,因为包还是那个包,只是里面东西变了。
总结(最重要的 3 句话)
- @Before 通知不需要返回值
- entity 是参数对象,引用传递,修改它 = 直接修改目标方法的参数
- 反射 invoke 是直接给对象赋值,不是创建新对象
在 Java 中,当你把一个对象作为方法参数传递时,传递的是对象引用的副本 ,但这个副本和原始引用指向的是同一个堆内存中的对象。切面里通过反射修改了那个对象的字段,其实就是在修改调用方持有的那个对象本身,所以不需要返回。
具体结合你的代码解释:
1. 对象是引用类型,修改直接生效
你的切面拦截的是 com.sky.mapper.* 包下的方法,这些方法通常长这样:
int insert(Entity entity);
int update(Entity entity);
调用方(比如 Service 层)会先创建一个实体对象,然后传给 Mapper 方法:
Entity e = new Entity();
// ... 设置一些业务字段
mapper.insert(e); // 此时 e 中的公共字段还是 null
在 mapper.insert(e) 执行时,e 的引用会被传递到 Mapper 方法内部,同时切面的 @Before 通知会先触发。在切面中:
Object entity = args[0]; // 拿到的是 e 的引用(副本),但指向同一个对象
setCreateTime.invoke(entity, now); // 修改的还是调用方的那个 e 对象
因此,当切面执行完毕后,调用方的 e 对象里面的 createTime、updateUser 等字段已经有值了。后续目标方法 insert(e) 运行时用的就是这个已经填充好的对象。
2. 为什么不需要返回新对象?
如果在切面里你创建了一个全新的对象 并赋值,那确实需要某种方式把新对象传回去(比如把 args[0] 替换掉,但 @Before 做不到直接替换参数引用后让目标方法用新的,因为参数绑定在代理调用时已确定)。但你的代码是直接在原对象上修改,并没有新建对象,所以没有"返回"的必要。
3. 如果是不可变对象,才需要返回
假如实体对象的字段都是 final 的、或者你操作的是基本类型/String 这种不可变对象,那你无法原地修改,就必须返回一个新值。但这里 entity 的 set 方法都可以通过反射调用,是典型的可变对象,所以原地修改是最自然的方式。
4. 结合 AOP 的执行流程
-
调用方持有对象引用
ref1→ 传入代理方法(参数引用是ref2,但指向同一个对象) -
@Before切面拿到ref2,通过反射修改对象内部状态 -
然后执行目标方法,目标方法里的参数引用
ref3还是指向同一个对象,此时对象已经被修改 -
目标方法返回后,调用方通过
ref1看到的就是修改后的对象
整个过程操作的都是同一个对象,因此你不需要做任何返回,调用方自然就能看到填充后的字段。