CGLIB 代理对象属性赋值不一致问题分析与解决方案
侧边栏壁纸
  • 累计撰写 11 篇文章
  • 累计收到 7 条评论

CGLIB 代理对象属性赋值不一致问题分析与解决方案

子衿
2026-01-16 / 0 评论 / 10 阅读 / 正在检测是否收录...

前言

在使用 Spring 等框架进行 Java 开发时,我们经常会遇到动态代理相关的问题。本文将详细分析一个由 CGLIB 代理机制引起的属性赋值不一致问题,并提供解决方案和深入的原理解释。

问题描述

在使用 IDEA 调试工具时,发现了一个奇怪的现象:

  • 问题 1:通过 pageDTO.setCreateUserNo(employeeNo) 赋值后,值被存储到了 $cglib_prop_createUserNo 字段中,而不是直接存储到 createUserNo 字段
  • 问题 2:通过 pageDTO.setBaseCodes(codes.get("baseCode")) 赋值时,值被存储到了 baseCodes 字段,但没有赋值到 $cglib_prop_baseCodes
  • 最终结果:后续代码通过 getCreateUserNo() 可以正常获取值,但通过 getBaseCodes() 却获取不到赋值内容

相关代码如下:

EqmsEqRepairOrderPageDTO pageDTO = (EqmsEqRepairOrderPageDTO) PageUtils.handleQuery(params.getModel());

// 设置用户编号 - 可以正常获取
pageDTO.setCreateUserNo(employeeNo);

// 设置基地代码 - 无法获取
pageDTO.setBaseCodes(codes.get("baseCode"));

// 后续调用
baseMapper.selectPageList(pageDTO); // 此时 baseCodes 为 null

DTO 类定义:

@Data
@EqualsAndHashCode(callSuper = false)
@Builder
@ApiModel(value = "EqmsEqRepairOrderPageDTO", description = "设备维修单")
public class EqmsEqRepairOrderPageDTO extends BaseDto implements Serializable {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "单据状态")
    private String orderStatus;

    private List<String> baseCodes;
    
    private List<String> factoryCodes;
    
    private List<String> workshopCodes;
}

问题根源分析

CGLIB 动态代理机制

要理解这个问题,首先需要了解 CGLIB 动态代理的工作原理:

  1. 代理对象的创建:CGLIB 通过在运行时动态生成被代理类的子类来创建代理对象
  2. 方法拦截:通过重写父类的方法来实现拦截,在方法执行前后加入自定义逻辑
  3. 属性管理:CGLIB 代理对象内部会维护一套自己的属性存储机制(如 $cglib_prop_xxx

继承层次导致的行为差异

在本案例中,问题的关键在于:

  • createUserNo:定义在父类 BaseDto

    • CGLIB 会正确重写父类的 setCreateUserNo()getCreateUserNo() 方法
    • 赋值和取值都通过代理对象的拦截器,行为一致
  • baseCodes:定义在子类 EqmsEqRepairOrderPageDTO

    • CGLIB 可能不会自动重写仅在子类中新增的方法
    • 导致 setter 和 getter 的代理行为不一致

赋值与取值的"错位"

赋值过程:
pageDTO.setBaseCodes(codes)
    ↓
未被代理拦截(直接调用原始方法)
    ↓
值存储到真实对象的 baseCodes 字段

取值过程:
pageDTO.getBaseCodes()
    ↓
被代理拦截
    ↓
尝试从 $cglib_prop_baseCodes 中取值
    ↓
返回 null(因为这个字段从未被赋值)

解决方案

通过在子类中显式重写 getter 和 setter 方法,强制 CGLIB 将这些方法纳入代理拦截范围:

@Data
@EqualsAndHashCode(callSuper = false)
@Builder
@ApiModel(value = "EqmsEqRepairOrderPageDTO", description = "设备维修单")
public class EqmsEqRepairOrderPageDTO extends BaseDto implements Serializable {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty(value = "单据状态")
    private String orderStatus;

    private List<String> baseCodes;
    
    private List<String> factoryCodes;
    
    private List<String> workshopCodes;

    // 显式重写 getter 和 setter
    @Override
    public List<String> getBaseCodes() {
        return baseCodes;
    }

    @Override
    public void setBaseCodes(List<String> baseCodes) {
        this.baseCodes = baseCodes;
    }
}

为什么这样能解决问题?

当您显式重写这些方法时:

  1. Java 编译器和 CGLIB 会识别到这些方法在当前类中被明确声明
  2. CGLIB 在生成代理子类时,必须重写这些方法以实现拦截
  3. 赋值和取值操作都会被代理一致地处理
  4. 确保数据在代理对象内部能够被一致地存取

深入理解:CGLIB 代理的工作流程

正常情况(父类属性)

实际对象:EqmsEqRepairOrderPageDTO (extends BaseDto)
    ↓
CGLIB 生成:EqmsEqRepairOrderPageDTO$$EnhancerByCGLIB$$xxxxx
    ↓
重写父类方法:setCreateUserNo(), getCreateUserNo()
    ↓
调用时都经过代理拦截器
    ↓
数据一致性 ✓

问题情况(子类新增属性,未重写)

实际对象:EqmsEqRepairOrderPageDTO
    ↓
CGLIB 生成:EqmsEqRepairOrderPageDTO$$EnhancerByCGLIB$$xxxxx
    ↓
未重写子类新增的方法(取决于具体实现)
    ↓
setter 可能不被拦截 / getter 可能被拦截
    ↓
数据不一致 ✗

解决后(显式重写)

实际对象:EqmsEqRepairOrderPageDTO (显式声明 @Override)
    ↓
CGLIB 生成:EqmsEqRepairOrderPageDTO$$EnhancerByCGLIB$$xxxxx
    ↓
强制重写:setBaseCodes(), getBaseCodes()
    ↓
调用时都经过代理拦截器
    ↓
数据一致性 ✓

最佳实践建议

  1. 了解框架的代理机制:在使用 Spring、Mybatis 等框架时,要意识到对象可能被代理
  2. 保持一致性:如果父类属性可能被代理,子类新增的属性也应该采用相同的处理方式
  3. 显式声明:对于可能被代理的 DTO 类,建议在子类中显式重写 getter/setter 方法,即使看起来"多余"
  4. 调试技巧

    • 使用 IDEA 的"Evaluate Expression"功能查看对象的实际类型
    • 观察是否有 $$EnhancerByCGLIB$$ 等特征
    • 检查字段中是否存在 $cglib_prop_xxx 这样的内部字段
  5. 避免直接访问字段:在可能存在代理的情况下,始终通过 getter/setter 访问属性

总结

CGLIB 代理对父类和子类方法的处理方式可能存在差异。当遇到代理对象属性赋值/取值不一致的问题时,通过在子类中显式重写相关的 getter 和 setter 方法,可以强制 CGLIB 将这些方法纳入代理拦截范围,从而解决数据存取不一致的问题。

Lombok在该问题中扮演着重要角色,抛开其节省大量重复的代码不谈,Lombok是导致问题“隐藏”得更深的原因。

  • 从源码层面看:在您的 EqmsEqRepairOrderPageDTO.java 文件中,你看不到 getBaseCodes 和 setBaseCodes 方法。这会给人一种错觉,即这些方法和父类 BaseDto 中的方法(如 getCreateUserNo)在继承和重写方面没有区别。
  • 从 CGLIB 代理层面看:CGLIB 在创建代理子类时,需要决定重写哪些方法来实现拦截。它的分析机制很可能依赖于类在源码级别的继承和方法重写(@Override)关系。
    它能识别出 createUserNo 的 getter/setter 是继承自 BaseDto 的。
    但对于 baseCodes,由于在 EqmsEqRepairOrderPageDTO 的源码中没有显式的方法声明或 @Override 注解,CGLIB 可能认为这些方法只是普通的新增方法,从而采取了不同的代理策略,最终导致了赋值和取值行为的不一致。

Lombok隐藏了方法实现的细节,使得在与 AOP、动态代理等底层技术结合时,可能会出现这种难以发现的“黑盒”问题。因为代理工具看到的是源码结构,而 Lombok 的魔法发生在编译期。

参考资料

  • CGLIB 官方文档
  • Spring AOP 代理机制
  • Java 动态代理技术详解
3

评论 (0)

取消