AOP和反射

Spring AOP + JoinPoint + 反射 实战案例

这是工作中最常用 的组合:AOP 切面获取连接点 (JoinPoint) → 用反射拿到目标对象、方法、参数、字段 → 动态操作

我直接给你3 个最实用的场景,代码可直接复制到 Spring 项目中运行!

先说明核心关系

  1. JoinPoint :AOP 提供的连接点对象,能拿到目标方法、目标对象、参数
  2. 反射 :通过 JoinPoint 拿到的 Method/Object,用反射动态执行、赋值、获取信息
  3. 组合作用:不修改业务代码,统一做日志、参数校验、权限、字段赋值等

第一步:统一准备环境

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(对象, 值);

总结

  1. JoinPoint 是 AOP 与反射的桥梁,能拿到目标对象、方法、参数
  2. 环绕通知 @Around 最常用,能在方法执行前后用反射做任何操作
  3. 私有成员必须用:getDeclaredXxx() + setAccessible(true)
  4. 三大实战场景:统一日志、自动赋值、动态调用方法

你现在看到的这段代码,是 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. 它到底是怎么生效的?(完整流程)

我给你走一遍流程,你瞬间就懂:

  1. 调用 mapper 方法

    java 复制代码
    userMapper.insert(user);
  2. AOP 拦截到这个方法

    java 复制代码
    @Before("autoFillPointCut()")
  3. 拿到方法参数:user 对象

    java 复制代码
    Object[] args = joinPoint.getArgs();
    Object entity = args[0]; // 就是那个 user
  4. 用反射调用 set 方法,给 user 赋值

    java 复制代码
    setCreateTime.invoke(entity, now);
  5. 赋值完直接结束,不需要返回 因为 user 对象已经被改好了

  6. 目标方法继续执行此时 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 句话)

  1. @Before 通知不需要返回值
  2. entity 是参数对象,引用传递,修改它 = 直接修改目标方法的参数
  3. 反射 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 对象里面的 createTimeupdateUser 等字段已经有值了。后续目标方法 insert(e) 运行时用的就是这个已经填充好的对象。

2. 为什么不需要返回新对象?

如果在切面里你创建了一个全新的对象 并赋值,那确实需要某种方式把新对象传回去(比如把 args[0] 替换掉,但 @Before 做不到直接替换参数引用后让目标方法用新的,因为参数绑定在代理调用时已确定)。但你的代码是直接在原对象上修改,并没有新建对象,所以没有"返回"的必要。

3. 如果是不可变对象,才需要返回

假如实体对象的字段都是 final 的、或者你操作的是基本类型/String 这种不可变对象,那你无法原地修改,就必须返回一个新值。但这里 entityset 方法都可以通过反射调用,是典型的可变对象,所以原地修改是最自然的方式。

4. 结合 AOP 的执行流程

  • 调用方持有对象引用 ref1 → 传入代理方法(参数引用是 ref2,但指向同一个对象)

  • @Before 切面拿到 ref2,通过反射修改对象内部状态

  • 然后执行目标方法,目标方法里的参数引用 ref3 还是指向同一个对象,此时对象已经被修改

  • 目标方法返回后,调用方通过 ref1 看到的就是修改后的对象

整个过程操作的都是同一个对象,因此你不需要做任何返回,调用方自然就能看到填充后的字段。

相关推荐
西凉的悲伤1 小时前
java 使用PNG图片隐写文件
java·图片隐写·png
有梦想的小何1 小时前
Cursor AI 编程实战(篇一):Prompt 与案例总结
java·linux·prompt·ai编程
河阿里2 小时前
SpringBoot:Spring Task定时任务完整使用教学
java·spring boot·spring
jiayong232 小时前
Tool Permission 与 Sandbox 的安全流水线:Agent 工具系统的工程边界
java·数据库·安全·agent
rururunu2 小时前
Windows 下切换 Java 环境太复杂了,我做了个 cli 工具,可以快速安装,切换 Java 版本
java
qq_452396232 小时前
第十一篇:《性能压测基础:JMeter线程模型与压测策略设计》
java·开发语言·jmeter
澈2072 小时前
二叉搜索树:高效增删查的秘诀
java·开发语言·算法
青云计划2 小时前
Spring
java·后端·spring