前言
开始上班了,小编继上篇博客精通Mybatis之结果集处理流程与映射体系(重点mybatis嵌套子查询,循环依赖解决方案)(二),今天讲结果集处理与映射的倒数第二篇,本来以为三篇能结束的结果发现不够用了,先讲懒加载吧。话不多说进入主题。
懒加载
懒加载是为改善解析对象属性时,大量的嵌套子查询的并发问题。设置懒加载后,只有在使用指定属性时才会加载,从而分散SQL请求。mybatis懒加载原理是基于动态代理实现的。
基本配置以及代码示例(小编依旧使用公司下多个部门的嵌套查询,设置部门查询的时候使用懒加载):
<resultMap id="DepartmentMap" type="entity.Department">
<id property="id" column="id"/>
<result property="companyId" column="company_id" />
<result property="departmentName" column="department_name"/>
</resultMap>
<resultMap id="CompanyMap2" type="entity.Company">
<id property="id" column="id"/>
<result property="companyName" column="company_name" jdbcType="VARCHAR"/>
<collection property="departmentList" column="id" select="selectDepartmentByCompanyId" fetchType="lazy"/>
</resultMap>
<select id="selectDepartmentByCompanyId" resultMap="DepartmentMap">
select * from department where company_id = #{companyId}
</select>
<select id="selectCompanyById" resultMap="CompanyMap2" parameterType="java.lang.Long">
select * from company where id = #{id}
</select>
测试代码:
public class LazyLoadTest {
SqlSessionFactory sqlSessionFactory;
Configuration configuration;
@Before
public void init() throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
configuration = sqlSessionFactory.getConfiguration();
configuration.setLazyLoadTriggerMethods(new HashSet<>());
}
@Test
public void collectionLazyTest() {
try(SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.REUSE)){
CompanyMapper companyMapper = sqlSession.getMapper(CompanyMapper.class);
Company company = companyMapper.selectCompanyById(1L);
System.out.println(company.getDepartmentList().size());
}
}
}
好了小编根据示例先讲一下懒加载的配置和细节:
- 在嵌套子查询中指定 fetchType=“lazy” 即可设置懒加载。在调用company.getDepartmentList时才会真正加载。此外调用company的:“equals”, “clone”, “hashCode”, “toString” 均会触发当前对象所有未执行的懒加载。(“equals”, “clone”, “hashCode”, “toString” 这些方法是在configuration中lazyLoadTriggerMethods的属性,类型为Set,上面小编为了不让company直接懒加载[因为debug下会执行toString方法]从而设置了空hashSet)
- 通过设置全局参数aggressiveLazyLoading=true ,也可指定调用对象任意方法触发所有懒加载。
参数 | 描述 |
---|---|
lazyLoadingEnabled | 全局懒加载开关 默认false |
aggressiveLazyLoading | 任意方法触发加载 默认false。 |
fetchType | 加载方式 eager实时 lazy懒加载。默认eager |
- set方法覆盖,当调用setXXX方法手动设置属性之后,对应的属性懒加载将会被移除,不会覆盖手动设置的值。
- 当对象经过序列化和反序列化之后,默认不在支持懒加载。但如果在全局参数中设置了configurationFactory类,而且采用JAVA原生序列化是可以正常执行懒加载的。其原理是将懒加载所需参数以及配置一起进行序列化,反序列化后在通过configurationFactory获取configuration构建执行环境。需要配置:
public static class ConfigurationFactory {
public static Configuration getConfiguration() {
return configuration; }
}
第三与第四点小编通过代码再次说明一下:
set方法覆盖
//set方法覆盖
@Test
public void collectionLazySetTest() {
try(SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.REUSE)){
CompanyMapper companyMapper = sqlSession.getMapper(CompanyMapper.class);
Company company = companyMapper.selectCompanyById(1L);
company.setDepartmentList(new ArrayList<>());
System.out.println(company.getDepartmentList().size());
}
}
测试结果:
set方法覆盖
没set方法覆盖:
对象序列化反序列化
@Test
public void collectionLazySerializableTest() throws Exception{
try(SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.REUSE)){
CompanyMapper companyMapper = sqlSession.getMapper(CompanyMapper.class);
Company company = companyMapper.selectCompanyById(1L);
byte[] bytes = writeObject(company);
Company companySer = (Company)readObject(bytes);
System.out.println(companySer.getDepartmentList());
}
}
private byte[] writeObject(Object obj) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
ObjectOutputStream outputStream = new ObjectOutputStream(out);
outputStream.writeObject(obj);
return out.toByteArray();
}
private Object readObject(byte[] bytes) throws IOException, ClassNotFoundException {
ByteArrayInputStream in = new ByteArrayInputStream(bytes);
ObjectInputStream inputStream = new ObjectInputStream(in);
return inputStream.readObject();
}
测试结果:
修改完毕后:
<settings>
//这里是内部静太类用$标识
<setting name="configurationFactory" value="test.LazyLoadTest$ConfigurationFactory"/>
</settings>
@Test
public void collectionLazySerializableTest() throws Exception{
try(SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.REUSE)){
CompanyMapper companyMapper = sqlSession.getMapper(CompanyMapper.class);
Company company = companyMapper.selectCompanyById(1L);
byte[] bytes = writeObject(company);
Company companySer = (Company)readObject(bytes);
System.out.println(companySer.getDepartmentList());
}
}
public static class ConfigurationFactory {
public static Configuration getConfiguration() {
return configuration; }
}
测试结果:
懒加载原理及内部结构
讲完了懒加载的结论,那小编肯定得证明原理,首先根据上面的示例代码,查看一下company的结构:
代理之后Bean会包含一个MethodHandler,内部在包含一个Map用于存放待执行懒加载,执行前懒加载前会移除。LoadPair用于针对反序列化的Bean准备执行环境。ResultLoader用于执行加载操作,执行前原执行器如果关闭会创建一个新的执行器(这里会引出一个小编之前没有讲解的Executor => ClosedExecutor)。特定属性如果加载失败,不会在进行二次加载。
内部结构图以及执行流程:
代理bean的创建过程
代理过程发生在结果集解析,创建对象之后(DefaultResultSetHandler.createResultObject),如果对应的属性设置了懒加载,则会通过ProxyFactory 创建代理对象,该对象继承自原对象,然后将对象的值全部拷贝到代理对像。并设置相应MethodHandler(原对象直接抛弃)
懒加载源码阅读
接下来小编带大家来一起阅读一下源码:
先是代理对象的创建:
DefaultResultSetHandler#createResultObject方法
private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
...
//创建company对象
Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix);
if (resultObject != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
//三个映射 ,再department的时候为子查询且是懒加载
for (ResultMapping propertyMapping : propertyMappings) {
// issue gcode #109 && issue #149
if (propertyMapping.getNestedQueryId() != null && propertyMapping.isLazy()) {
//对company对象进行代理
resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
break;
}
}
}
this.useConstructorMappings = resultObject != null && !constructorArgTypes.isEmpty(); // set current mapping result
return resultObject;
}
JavassistProxyFactory#createProxy方法
public static Object createProxy(Object target, ResultLoaderMap lazyLoader, Configuration configuration, ObjectFactory objectFactory, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
final Class<?> type = target.getClass();
//创建EnhancedResultObjectProxyImpl
EnhancedResultObjectProxyImpl callback = new EnhancedResultObjectProxyImpl(type, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
//根据EnhancedResultObjectProxyImpl 创建compay的代理对象
Object enhanced = crateProxy(type, callback, constructorArgTypes, constructorArgs);
//之后是将原对象的属性copy进入代理对象
PropertyCopier.copyBeanProperties(type, target, enhanced);
return enhanced;
}
创建完代理对象后,然后获取:
JavassistProxyFactory#invoke方法
@Override
public Object invoke(Object enhanced, Method method, Method methodProxy, Object[] args) throws Throwable {
//原对象的method方法
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 JavassistSerialStateHolder(original, lazyLoader.getProperties(), objectFactory, constructorArgTypes, constructorArgs);
} else {
return original;
}
} else {
//懒加载大于0并且不是finalize方法
if (lazyLoader.size() > 0 && !FINALIZE_METHOD.equals(methodName)) {
//是否任意方法就加载或者在"equals", "clone", "hashCode", "toString"方法里面
if (aggressive || lazyLoadTriggerMethods.contains(methodName)) {
lazyLoader.loadAll();
//如果是set方法 则将懒加载移除,直接赋值
} else if (PropertyNamer.isSetter(methodName)) {
final String property = PropertyNamer.methodToProperty(methodName);
lazyLoader.remove(property);
//为get方法,根据属性名称获取懒加载器有就加载
} else if (PropertyNamer.isGetter(methodName)) {
final String property = PropertyNamer.methodToProperty(methodName);
if (lazyLoader.hasLoader(property)) {
lazyLoader.load(property);
}
}
}
}
}
return methodProxy.invoke(enhanced, args);
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
}
ResultLoaderMap#load
public boolean load(String property) throws SQLException {
//取出LoadPair ,并且移除懒加载属性(懒加载之前所以如果pair.load()失败则不会进行二次加载)
LoadPair pair = loaderMap.remove(property.toUpperCase(Locale.ENGLISH));
if (pair != null) {
//开始加载
pair.load();
return true;
}
return false;
}
ResultLoaderMap$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);
//这里有个ClosedExecutor 继承BaseExecutor,只是将isClosed设置为true
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.resultLoader.loadResult()
this.metaResultObject.setValue(property, this.resultLoader.loadResult());
}
ResultLoader#selectList
public Object loadResult() throws SQLException {
//通过executor 直接查询库
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.query(mappedStatement, parameterObject, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER, cacheKey, boundSql);
} finally {
if (localExecutor != executor) {
localExecutor.close(false);
}
}
}
这边小编还落下一个ResultLoaderMap lazyLoader这里面的LoadPair是怎么放进去的呢
private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
//lazyLoader 为空map
final ResultLoaderMap lazyLoader = new ResultLoaderMap();
//这边调用了创建代理对象
Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
final MetaObject metaObject = configuration.newMetaObject(rowValue);
boolean foundValues = this.useConstructorMappings;
if (shouldApplyAutomaticMappings(resultMap, false)) {
foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
}
//在手动映射的时候
foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
foundValues = lazyLoader.size() > 0 || foundValues;
rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
}
return rowValue;
}
lazerLoader最终在嵌套子查询的时候会加入LoadPair 然后LoadPair 值里面会有ResultLoader
private Object getNestedQueryMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix)
throws SQLException {
...
if (executor.isCached(nestedQuery, key)) {
executor.deferLoad(nestedQuery, metaResultObject, property, key, targetType);
value = DEFERRED;
} else {
final ResultLoader resultLoader = new ResultLoader(configuration, executor, nestedQuery, nestedQueryParameterObject, targetType, key, nestedBoundSql);
//是否是懒加载
if (propertyMapping.isLazy()) {
//放入lazyLoader下面那段代码
lazyLoader.addLoader(property, metaResultObject, resultLoader);
value = DEFERRED;
} else {
value = resultLoader.loadResult();
}
}
}
return value;
}
public void addLoader(String property, MetaObject metaResultObject, ResultLoader resultLoader) {
String upperFirst = getUppercaseFirstProperty(property);
if (!upperFirst.equalsIgnoreCase(property) && loaderMap.containsKey(upperFirst)) {
throw new ExecutorException("Nested lazy loaded result property '" + property
+ "' for query id '" + resultLoader.mappedStatement.getId()
+ " already exists in the result map. The leftmost property of all lazy loaded properties must be unique within a result map.");
}
loaderMap.put(upperFirst, new LoadPair(property, metaResultObject, resultLoader));
}
懒加载额外讲解
这边在对象序列化反序列化完毕的时候调用懒加载则是使用AbstractEnhancedDeserializationProxy的invoke,使用ResultLoaderMap$LoadPair#load,这里有两个load方法首先他不会进入无参load,只会进入有参方法,然后会进入if判断
if (this.metaResultObject == null || this.resultLoader == null)
因为 private transient MetaObject metaResultObject; 和 private transient ResultLoader resultLoader;被设置成不能序列化的。
然后必须 获取Configuration ,final Configuration config = this.getConfiguration();
这个获取方法里面有 需要配置configurationFactory,里面必须有个静态方法:getConfiguration
final Method factoryMethod = this.configurationFactory.getDeclaredMethod(FACTORY_METHOD);
所以小编在序列化反序列化的示例中配置了configurationFactory,重写了一个getConfiguration方法
然后可以顺利懒加载了
在JavassistProxyFactory的invoke方法中首先会判断,是否为writeReplace方法,小编这边写个示例大家就明白了
if (WRITE_REPLACE_METHOD.equals(methodName))
@Test
public void serializableTest() throws IOException, ClassNotFoundException {
WhateverBean bean = new WhateverBean("0000", "aaaa");
System.out.println(bean);
byte[] bytes = writeObject(bean);
Object obj = readObject(bytes);
System.out.println(obj);
}
public class WhateverBean implements Serializable {
private String id;
private String name;
/**
* 在序列化时 重写对象
*/
protected final Object writeReplace() throws ObjectStreamException {
System.out.println("writeReplace");
return new WhateverBean("8888", "Tom");
}
/**
* 在反序列化时 转换对象
*/
protected final Object readResolve() throws ObjectStreamException {
System.out.println("readResolve");
this.name = "Bob";
return this;
}
public WhateverBean(String id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return "WhateverBean{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
'}';
}
}
测试结果
WhateverBean{id='0000', name='aaaa'}
writeReplace
readResolve
WhateverBean{id='8888', name='Bob'}
大家看到这儿懂了不,就是对象序列化的时候,可以使用writeReplace方法对对象进行改变可以是属性也可以对象本身发生变化,当反序列化的时候readResolve方法改变对象。所以懒加载中当发现是writeReplace方法进行包装,当然反序列化的时候当然有readResolve方法。结合看上面在反序列化后的代理对象也就变了EnhancedDeserializationProxyImpl这个方法。
总结
到此为止懒加载所有细节就讲解完毕了,源码阅读其实还是挺痛苦的,大家可以直接跳过然后根据小编的结论直接debug程序。最后留下几个问题给大家:在使用过程中,如果会话关闭、跨线程、序列化等情况下,是否能够继续加载?看懂今天的博客想必大家肯定是能够轻松愉快的回答出来,小编原先以为三篇就够了,估计要讲第四篇了,哎!