精通Mybatis之结果集处理流程与映射体系(无死角懒加载讲解)(三)

本文深入探讨 MyBatis 的懒加载机制,包括配置、应用场景和源码分析。通过示例代码展示如何设置懒加载,以及在并发场景下如何优化性能。同时,解释了对象序列化和反序列化对懒加载的影响,以及如何处理相关问题。最后,文章通过源码解析了懒加载的内部实现,包括代理对象创建、懒加载触发条件和执行流程。
摘要由CSDN通过智能技术生成

前言

开始上班了,小编继上篇博客精通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());
        }
    }
}

好了小编根据示例先讲一下懒加载的配置和细节:

  1. 在嵌套子查询中指定 fetchType=“lazy” 即可设置懒加载。在调用company.getDepartmentList时才会真正加载。此外调用company的:“equals”, “clone”, “hashCode”, “toString” 均会触发当前对象所有未执行的懒加载。(“equals”, “clone”, “hashCode”, “toString” 这些方法是在configuration中lazyLoadTriggerMethods的属性,类型为Set,上面小编为了不让company直接懒加载[因为debug下会执行toString方法]从而设置了空hashSet)
  2. 通过设置全局参数aggressiveLazyLoading=true ,也可指定调用对象任意方法触发所有懒加载。
参数描述
lazyLoadingEnabled全局懒加载开关 默认false
aggressiveLazyLoading任意方法触发加载 默认false。
fetchType加载方式 eager实时 lazy懒加载。默认eager
  1. set方法覆盖,当调用setXXX方法手动设置属性之后,对应的属性懒加载将会被移除,不会覆盖手动设置的值。
  2. 当对象经过序列化和反序列化之后,默认不在支持懒加载。但如果在全局参数中设置了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程序。最后留下几个问题给大家:在使用过程中,如果会话关闭、跨线程、序列化等情况下,是否能够继续加载?看懂今天的博客想必大家肯定是能够轻松愉快的回答出来,小编原先以为三篇就够了,估计要讲第四篇了,哎!

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

木兮君

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值