MyBatis支持通过<collection>或者<association>标签关联一个外部的查询Mapper,当通过MyBatis配置开启懒加载机制时,执行查询操作不会触发关联的查询Mapper,而通过Getter方法访问实体属性时才会执行一次关联的查询Mapper,然后为实体属性赋值。本节我们就来了解MyBatis懒加载机制的实现原理。
在DefaultResultSetHandler类的handleRowValues()方法中处理结果集时,对嵌套的ResultMap和非嵌套ResultMap做了不同处理,代码如下:
public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
// 是否有嵌套ResultMap
if (resultMap.hasNestedResultMaps()) {
// 嵌套查询校验RowBounds,可以通过设置safeRowBoundsEnabled=false参数绕过校验
ensureNoRowBounds();
// 校验ResultHandler,可以设置safeResultHandlerEnabled=false参数绕过校验
checkResultHandler();
// 如果有嵌套的ResultMap,调用handleRowValuesForNestedResultMap处理嵌套ResultMap
handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
} else {
// 如果无嵌套的ResultMap,调用handleRowValuesForSimpleResultMap处理简单非嵌套ResultMap
handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
}
}
在介绍ResultMap时已经介绍过,当使用<association>和<collection>标签关联一个外部的查询Mapper时,ResultMap对象的hasNestedResultMaps属性值为false,hasNestedQueries属性值为true。因此,MyBatis框架在开启懒加载机制后,handleRowValues()方法会调用handleRowValuesForSimpleResultMap()方法处理ResultMap映射。该方法实现如下:
private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
throws SQLException {
DefaultResultContext<Object> resultContext = new DefaultResultContext<Object>();
skipRows(rsw.getResultSet(), rowBounds);
// 遍历处理每一行记录
while (shouldProcessMoreRows(resultContext, rowBounds) && rsw.getResultSet().next()) {
// 对<discriminator>标签配置的鉴别器进行处理,获取实际映射的ResultMap对象
ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(rsw.getResultSet(), resultMap, null);
// 调用getRowValue()把一行数据转换为Java实体对象
Object rowValue = getRowValue(rsw, discriminatedResultMap);
storeObject(resultHandler, resultContext, rowValue, parentMapping, rsw.getResultSet());
}
}
如上面的代码所示,在handleRowValuesForSimpleResultMap()方法中,首先调用skipRows()方法跳过RowBounds对象指定偏移的行,然后遍历结果集中所有的行,对<discriminator>标签配置的鉴别器进行处理,获取实际映射的ResultMap对象,接着调用getRowValue()方法处理一行记录,将数据库行记录转换为Java实体对象。getRowValue()方法实现如下:
// 处理非嵌套ResultMap
private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap) throws SQLException {
// 创建ResultLoaderMap对象,用于存放懒加载属性信息
final ResultLoaderMap lazyLoader = new ResultLoaderMap();
// 创建ResultMap指定的类型实例,通常为<resultMap>标签的type属性指定的类型
Object rowValue = createResultObject(rsw, resultMap, lazyLoader, null);
// 判断该类型是否注册了TypeHandler
if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
final MetaObject metaObject = configuration.newMetaObject(rowValue);
boolean foundValues = this.useConstructorMappings;
// 判断是否需要处理自动映射
if (shouldApplyAutomaticMappings(resultMap, false)) {
// 调用applyAutomaticMappings()方法处理自动映射的字段
foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, null) || foundValues;
}
// 处理<result>标签配置映射的字段
foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, null) || foundValues;
foundValues = lazyLoader.size() > 0 || foundValues;
rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
}
return rowValue;
}
如上面的代码所示,在getRowValue()方法中主要做了下面几件事情:
(1)创建ResultLoaderMap对象,该对象用于存放懒加载的属性及对应的ResultLoader对象,MyBatis中的ResultLoader用于执行一个查询Mapper,然后将执行结果赋值给某个实体对象的属性。
(2)调用createResultObject()方法创建ResultMap对应的Java实体对象,我们需要重点关注该方法的实现,代码如下:
// 初始化返回的实体对象,并处理构造方法映射
private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
this.useConstructorMappings = false; // reset previous mapping result
final List<Class<?>> constructorArgTypes = new ArrayList<Class<?>>();
final List<Object> constructorArgs = new ArrayList<Object>();
// 调用createResultObject()方法创建结果对象
Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix);
if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
// 获取<result>结果集映射信息
final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
for (ResultMapping propertyMapping : propertyMappings) {
// 如果映射中配置了懒加载,则创建代理对象
if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {
// 调用ProxyFactory实例的createProxy()方法创建代理对象
resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
break;
}
}
}
this.useConstructorMappings = resultObject != null && !constructorArgTypes.isEmpty(); // set current mapping result
return resultObject;
}
如上面的代码所示,createResultObject()方法中,首先调用重载的createResultObject()方法使用ObjectFactory对象创建Java实体对象,然后判断ResultMap中是否有嵌套的查询,如果有嵌套的查询并且开启了懒加载机制,则通过MyBatis中的ProxyFactory创建实体对象的代理对象。ProxyFactory接口有两种不同的实现,分别为CglibProxyFactory和JavassistProxyFactory。也就是说,MyBatis同时支持使用Cglib和Javassist创建代理对象,具体使用哪种策略创建代理对象,可以在MyBatis主配置文件中通过proxyFactory属性指定。
(3)调用applyAutomaticMappings()方法处理自动映射,该方法在介绍MyBatis级联映射原理时已经介绍过,可自行参考MyBatis源码。
(4)调用applyPropertyMappings()方法处理<result>标签配置的映射字段,该方法中除了为Java实体属性设置值外,还将指定了懒加载的属性添加到ResultLoaderMap对象中。
当我们开启懒加载时,执行查询Mapper返回的实际上是通过Cglib或Javassist创建的动态代理对象。假设我们指定了使用Cglig创建动态代理对象,调用动态代理对象的Getter方法时会执行MyBatis中定义的拦截逻辑,代码如下:
public Object intercept(Object enhanced, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
final String methodName = method.getName();
try {
synchronized (lazyLoader) {
if (WRITE_REPLACE_METHOD.equals(methodName)) {
Object original;
if (constructorArgTypes.isEmpty()) {
original = objectFactory.create(type);
} else {
original = objectFactory.create(type, constructorArgTypes, constructorArgs);
}
PropertyCopier.copyBeanProperties(type, enhanced, original);
if (lazyLoader.size() > 0) {
return new CglibSerialStateHolder(original, lazyLoader.getProperties(), objectFactory, constructorArgTypes, constructorArgs);
} else {
return original;
}
} else {
if (lazyLoader.size() > 0 && !FINALIZE_METHOD.equals(methodName)) {
if (aggressive || lazyLoadTriggerMethods.contains(methodName)) {
lazyLoader.loadAll();
} else if (PropertyNamer.isSetter(methodName)) {
final String property = PropertyNamer.methodToProperty(methodName);
lazyLoader.remove(property);
} else if (PropertyNamer.isGetter(methodName)) {
final String property = PropertyNamer.methodToProperty(methodName);
if (lazyLoader.hasLoader(property)) {
lazyLoader.load(property);
}
}
}
}
}
return methodProxy.invokeSuper(enhanced, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
}
如上面的代码所示,EnhancedResultObjectProxyImpl是CglibProxyFactory类中的一个内部类,EnhancedResultObjectProxyImpl的intercept()方法中定义了调用动态代理对象方法的拦截逻辑。也就是说,当我们调用代理实体对象的Getter方法获取属性时,会执行EnhancedResultObjectProxyImpl类的intercept()方法中的拦截逻辑。
在EnhancedResultObjectProxyImpl类的intercept()方法中,获取Getter方法对应的属性名称,然后调用ResultLoaderMap对象的hasLoader()方法判断该属性是否是懒加载属性,如果是,则调用ResultLoaderMap对象的load()方法加载该属性,ResultLoaderMap对象的load()方法最终会调用LoadPair对象的load()方法,代码如下:
public void load(final Object userObject) throws SQLException {
if (this.metaResultObject == null || this.resultLoader == null) {
if (this.mappedParameter == null) {
throw new ExecutorException("Property [" + this.property + "] cannot be loaded because "
+ "required parameter of mapped statement ["
+ this.mappedStatement + "] is not serializable.");
}
final Configuration config = this.getConfiguration();
final MappedStatement ms = config.getMappedStatement(this.mappedStatement);
if (ms == null) {
throw new ExecutorException("Cannot lazy load property [" + this.property
+ "] of deserialized object [" + userObject.getClass()
+ "] because configuration does not contain statement ["
+ this.mappedStatement + "]");
}
this.metaResultObject = config.newMetaObject(userObject);
this.resultLoader = new ResultLoader(config, new ClosedExecutor(), ms, this.mappedParameter,
metaResultObject.getSetterType(this.property), null, null);
}
/* We are using a new executor because we may be (and likely are) on a new thread
* and executors aren't thread safe. (Is this sufficient?)
*
* A better approach would be making executors thread safe. */
if (this.serializationCheck == null) {
final ResultLoader old = this.resultLoader;
this.resultLoader = new ResultLoader(old.configuration, new ClosedExecutor(), old.mappedStatement,
old.parameterObject, old.targetType, old.cacheKey, old.boundSql);
}
this.metaResultObject.setValue(property, this.resultLoader.loadResult());
}
如上面的代码所示,该方法中创建了一个ResultLoader对象,然后ResultLoader对象的loadResult()方法执行查询操作,将查询结果赋值给对应的Java实体属性。ResultLoader类的loadResult()方法实现如下:
/
* @author Clinton Begin
*/
public class ResultLoader {
protected final Configuration configuration;
protected final Executor executor;
protected final MappedStatement mappedStatement;
protected final Object parameterObject;
protected final Class<?> targetType;
protected final ObjectFactory objectFactory;
protected final CacheKey cacheKey;
protected final BoundSql boundSql;
protected final ResultExtractor resultExtractor;
protected final long creatorThreadId;
protected boolean loaded;
protected Object resultObject;
public ResultLoader(Configuration config, Executor executor, MappedStatement mappedStatement, Object parameterObject, Class<?> targetType, CacheKey cacheKey, BoundSql boundSql) {
this.configuration = config;
this.executor = executor;
this.mappedStatement = mappedStatement;
this.parameterObject = parameterObject;
this.targetType = targetType;
this.objectFactory = configuration.getObjectFactory();
this.cacheKey = cacheKey;
this.boundSql = boundSql;
this.resultExtractor = new ResultExtractor(configuration, objectFactory);
this.creatorThreadId = Thread.currentThread().getId();
}
public Object loadResult() throws SQLException {
List<Object> list = selectList();
resultObject = resultExtractor.extractObjectFromList(list, targetType);
return resultObject;
}
private <E> List<E> selectList() throws SQLException {
Executor localExecutor = executor;
if (Thread.currentThread().getId() != this.creatorThreadId || localExecutor.isClosed()) {
localExecutor = newExecutor();
}
try {
return localExecutor.<E> query(mappedStatement, parameterObject, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER, cacheKey, boundSql);
} finally {
if (localExecutor != executor) {
localExecutor.close(false);
}
}
}
private Executor newExecutor() {
final Environment environment = configuration.getEnvironment();
if (environment == null) {
throw new ExecutorException("ResultLoader could not load lazily. Environment was not configured.");
}
final DataSource ds = environment.getDataSource();
if (ds == null) {
throw new ExecutorException("ResultLoader could not load lazily. DataSource was not configured.");
}
final TransactionFactory transactionFactory = environment.getTransactionFactory();
final Transaction tx = transactionFactory.newTransaction(ds, null, false);
return configuration.newExecutor(tx, ExecutorType.SIMPLE);
}
public boolean wasNull() {
return resultObject == null;
}
}
如上面的代码所示,在ResultLoader类的loadResult()方法中调用selectList()方法完成查询操作,selectList()方法中最终会调用Executor对象的query()方法完成查询操作。
最后我们来总结一下,MyBatis中的懒加载实际上是通过动态代理来实现的。当我们通过MyBatis的配置开启懒加载后,执行第一次查询操作实际上返回的是通过Cglig或者Javassist创建的代理对象。因此,调用代理对象的Getter方法获取懒加载属性时,会执行动态代理的拦截方法,在拦截方法中,通过Getter方法名称获取Java实体属性名称,然后根据属性名称获取对应的LoadPair对象,LoadPair对象中维护了Mapper的Id,有了Mapper的Id就可以获取对应的MappedStatement对象,接着执行一次额外的查询操作,使用查询结果为懒加载属性赋值。