前言
在使用 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 为 nullDTO 类定义:
@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 动态代理的工作原理:
- 代理对象的创建:CGLIB 通过在运行时动态生成被代理类的子类来创建代理对象
- 方法拦截:通过重写父类的方法来实现拦截,在方法执行前后加入自定义逻辑
- 属性管理:CGLIB 代理对象内部会维护一套自己的属性存储机制(如
$cglib_prop_xxx)
继承层次导致的行为差异
在本案例中,问题的关键在于:
createUserNo:定义在父类BaseDto中- CGLIB 会正确重写父类的
setCreateUserNo()和getCreateUserNo()方法 - 赋值和取值都通过代理对象的拦截器,行为一致
- CGLIB 会正确重写父类的
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;
}
}为什么这样能解决问题?
当您显式重写这些方法时:
- Java 编译器和 CGLIB 会识别到这些方法在当前类中被明确声明
- CGLIB 在生成代理子类时,必须重写这些方法以实现拦截
- 赋值和取值操作都会被代理一致地处理
- 确保数据在代理对象内部能够被一致地存取
深入理解: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()
↓
调用时都经过代理拦截器
↓
数据一致性 ✓最佳实践建议
- 了解框架的代理机制:在使用 Spring、Mybatis 等框架时,要意识到对象可能被代理
- 保持一致性:如果父类属性可能被代理,子类新增的属性也应该采用相同的处理方式
- 显式声明:对于可能被代理的 DTO 类,建议在子类中显式重写 getter/setter 方法,即使看起来"多余"
调试技巧:
- 使用 IDEA 的"Evaluate Expression"功能查看对象的实际类型
- 观察是否有
$$EnhancerByCGLIB$$等特征 - 检查字段中是否存在
$cglib_prop_xxx这样的内部字段
- 避免直接访问字段:在可能存在代理的情况下,始终通过 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 动态代理技术详解
评论 (0)