MyBatis延迟加载通过代理对象在首次调用getter方法时触发SQL查询,CGLIB通过继承生成子类重写方法,Javassist则直接修改字节码注入加载逻辑,二者均需满足全局开启、映射标记及类可代理三条件。

MyBatis 的延迟加载不是靠“自动扫描”或“注解反射”触发的,而是依赖代理对象在运行时拦截方法调用——核心就在这一步:当代码第一次调用某个关联属性的 getter 方法(比如 user.getOrders()),代理才真正去执行 SQL 并填充数据。CGLIB 和 Javassist 是实现这种代理的两种技术路径,它们干的是同一件事:生成目标类的子类(CGLIB)或改写字节码(Javassist),插入加载逻辑。
代理对象怎么被创建出来的
只要满足三个条件,MyBatis 就会在查询主对象后,为配置了延迟加载的关联属性(<association> 或 <collection>)生成代理:
-
全局开关开启:
lazyLoadingEnabled=true -
映射中明确标记:比如
fetchType="lazy"或全局默认生效时未显式设为eager -
实体类可被继承:不能是
final类,否则 CGLIB 无法生成子类;字段不能是private final,否则 Javassist 可能无法安全注入
此时 MyBatis 不直接返回真实对象,而是返回一个由 CGLIB 或 Javassist 动态构造的代理实例。这个代理对象保留了原始类的所有 public 方法,但关键 getter 方法被重写——里面塞了一段“加载检查 + 查询执行”的逻辑。
CGLIB 方式:子类覆盖 + 方法拦截
CGLIB 通过继承原类生成子类来实现代理。例如 User 类有 getOrders() 方法,CGLIB 就生成一个 User$$EnhancerByCGLIB$$xxx 子类,并重写该方法:
立即学习“Java免费学习笔记(深入)”;
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
- 首次调用时,检查 orders 是否为空或为占位代理对象
- 若未加载,就触发预先缓存的 SQL(如
select * from order where user_id = ?) - 查完结果后,把真实 List 赋值给原字段(通过反射绕过 private 限制),并替换代理引用
- 后续再调用
getOrders(),就直接返回已加载的数据,不再查库
这种方式要求目标类不能是 final,方法也不能是 final,否则无法被子类覆盖。
Javassist 方式:字节码增强 + 直接注入
Javassist 不走继承路线,而是直接修改原类的字节码,在 getter 方法开头插入一段逻辑(类似 AOP 的 before 增强):
- 它会定位到
getOrders()的字节码位置,在方法入口插入 if 判断:是否已加载?未加载则调用 MyBatis 内部的lazyLoad流程 - 加载完成后,把真实对象写入对应字段(利用字段名和对象引用,无需 setter)
- 之后再次进入该方法,判断为 true,跳过加载逻辑,直接返回字段值
相比 CGLIB,Javassist 对类的限制更少(支持 final 类,只要字段可访问),但需要 JVM 允许运行时字节码操作(某些安全策略下可能受限)。
触发时机只发生在 getter 调用上(默认行为)
MyBatis 默认只把以下方法视为“加载触发器”:
-
equals()、hashCode()、toString()、clone()
这些方法在 lazyLoadTriggerMethods 配置中定义,默认值就是这四个。也就是说,哪怕你调用 user.toString(),也会顺带把所有延迟属性全加载出来——除非你把 aggressiveLazyLoading 设为 false(推荐),这时就只有明确访问 getOrders() 这类业务 getter 才会触发对应属性的加载。

















