深度分析Mybatis中org.apache.ibatis.executor.ExecutorException: No constructor found异常

1. 产生原因

该异常为实体类数据类型与数据库字段类型映射不匹配异常,当Mybatis执行完查询语句,开始获取返回对象并映射结果集时。如果在实体类中配置了所有参数的构造器,则会在获取返回对象时会通过实例该参数构造器的方式进行赋值,但Mybatis在使用构造器赋值处理中并没有调用到类型处理器,而是直接抛出了该异常。

在这里插入图片描述
由于实体类中只使用了含参构造器,所以Mybatis判断使用使用了有参构造器实例化对象并映射数据库字段值。
直接从Mybatis封装结果集对象开始深入,在getRowValue方法中记录了大致的执行流程。

package org.apache.ibatis.executor.resultset;

public class DefaultResultSetHandler implements ResultSetHandler {
...

private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap) throws SQLException {
    final ResultLoaderMap lazyLoader = new ResultLoaderMap();
    //获取返回对象
    Object rowValue = createResultObject(rsw, resultMap, lazyLoader, null);
    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, null) || foundValues;
      }
      foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, null) || foundValues;
      foundValues = lazyLoader.size() > 0 || foundValues;
      rowValue = (foundValues || configuration.isReturnInstanceForEmptyRow()) ? rowValue : null;
    }
    return rowValue;
  }

...

深入createResultObject(rsw, resultMap, lazyLoader, null)->createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix)方法。

  private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, List<Class<?>> constructorArgTypes, List<Object> constructorArgs, String columnPrefix)
      throws SQLException {
    final Class<?> resultType = resultMap.getType();
    final MetaClass metaType = MetaClass.forClass(resultType, reflectorFactory);
    final List<ResultMapping> constructorMappings = resultMap.getConstructorResultMappings();
    if (hasTypeHandlerForResultObject(rsw, resultType)) {
      return createPrimitiveResultObject(rsw, resultMap, columnPrefix);
    } else if (!constructorMappings.isEmpty()) {
      return createParameterizedResultObject(rsw, resultType, constructorMappings, constructorArgTypes, constructorArgs, columnPrefix);
    } else if (resultType.isInterface() || metaType.hasDefaultConstructor()) {
      //使用无参构造器实例化对象。
      return objectFactory.create(resultType);
    } else if (shouldApplyAutomaticMappings(resultMap, false)) {
      //使用了有参构造器实例化对象。
      return createByConstructorSignature(rsw, resultType, constructorArgTypes, constructorArgs, columnPrefix);
    }
    throw new ExecutorException("Do not know how to create an instance of " + resultType);
  }

该类中根据不同的情况返回不同的对象,此时为使用构造器映射,继续深入createByConstructorSignature(rsw, resultType, constructorArgTypes, constructorArgs, columnPrefix);

  private Object createByConstructorSignature(ResultSetWrapper rsw, Class<?> resultType, List<Class<?>> constructorArgTypes, List<Object> constructorArgs,
                                              String columnPrefix) throws SQLException {
    //通过反射的方式获取实体类中的构造函数
    //执行的值为public com.hnjd.entity.User(java.lang.Integer,java.lang.String,java.util.Date,java.lang.String,java.lang.String)
    final Constructor<?>[] constructors = resultType.getDeclaredConstructors();
    final Constructor<?> annotatedConstructor = findAnnotatedConstructor(constructors);
    if (annotatedConstructor != null) {
      return createUsingConstructor(rsw, resultType, constructorArgTypes, constructorArgs, columnPrefix, annotatedConstructor);
    } else {
      for (Constructor<?> constructor : constructors) {
  		//当判断为false时,走下方异常
        if (allowedConstructor(constructor, rsw.getClassNames())) {
          return createUsingConstructor(rsw, resultType, constructorArgTypes, constructorArgs, columnPrefix, constructor);
        }
      }
    }
    //可以发现Mybatis中org.apache.ibatis.executor.ExecutorException: No constructor found异常就是由此处抛出的
    throw new ExecutorException("No constructor found in " + resultType.getName() + " matching " + rsw.getClassNames());
  }

继续深入 if (allowedConstructor(constructor, rsw.getClassNames())),
constructor封装了实体类中的参数构造器,
rsw.getClassNames())将由数据库中读取的参数类型取出封装为List集合。
在这里插入图片描述
——了解了异常产生的原因后,找到了两种解决方式

2. 当前基于自动映射的解决方式

第一种解决方式是:使用有参构造器时,通过更改实体类属性的类型,使其与数据库字段类型完全一致。将实体类中Date类型改为java.sql.Timestamp类型(该文场景中的类型),这样在使用有参构造器实例化时,实体类和数据库的参数类型完全匹配。
第二张解决方式是:使用无参构造器,让其走Mybatis的类型处理器。在DefaultResultSetHandler的createResultObject中通过使用无参构造器实例化对象

第一种方式是对该异常的一种妥协,当类型一致时,异常自然不会抛出,大致流程与上述图一致,所以这里主要演示第二种解决方式的源码流程。

  private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, List<Class<?>> constructorArgTypes, List<Object> constructorArgs, String columnPrefix)
      throws SQLException {
    final Class<?> resultType = resultMap.getType();
    final MetaClass metaType = MetaClass.forClass(resultType, reflectorFactory);
    final List<ResultMapping> constructorMappings = resultMap.getConstructorResultMappings();
    if (hasTypeHandlerForResultObject(rsw, resultType)) {
      return createPrimitiveResultObject(rsw, resultMap, columnPrefix);
    } else if (!constructorMappings.isEmpty()) {
      return createParameterizedResultObject(rsw, resultType, constructorMappings, constructorArgTypes, constructorArgs, columnPrefix);
    } else if (resultType.isInterface() || metaType.hasDefaultConstructor()) {
      //metaType.hasDefaultConstructor(),判断是否有默认构造函数,由于删除了有参构造函数,默认走无参构造器,返回为true,此时便走create方法了,而不会去createByConstructorSignature中对有参构造器进行赋值。
      return objectFactory.create(resultType);
    } else if (shouldApplyAutomaticMappings(resultMap, false)) {
      return createByConstructorSignature(rsw, resultType, constructorArgTypes, constructorArgs, columnPrefix);
    }
    throw new ExecutorException("Do not know how to create an instance of " + resultType);
  }

从objectFactory.create(resultType)开始深入可以在org.apache.ibatis.reflection.factory.DefaultObjectFactory类中的instantiateClass方法中,发现底层通过反射的方式获取无参构造器实例化对象。

<T> T instantiateClass(Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
    try {
      Constructor<T> constructor;
      if (constructorArgTypes == null || constructorArgs == null) {
        constructor = type.getDeclaredConstructor();
        if (!constructor.isAccessible()) {
          constructor.setAccessible(true);
        }
        //获得实例对象
        return constructor.newInstance();
      }
      ...
    }
    ...
 }

获取的对象属性为null
在这里插入图片描述
以下为大致流程:通过无参构造器获取的rowValue对象被装载进MetaObject对象中,然后在applyAutomaticMappings方法中传入该对象引用,通过for循环遍历autoMapping,该集合中装载的类中封装了数据库列表,实体类属性名,类型处理器和一个判断值,底层通过反射调用Setter方法进行赋值。即每遍历一个类型,会先调用它所对应的类型处理器,将数据库中该字段类型的数据转换为实体类中相应的数据类型,用object类型的值接收,(若值为null,则foundValues为false,直接返回,导致最终返回对象rowValue为null),然后调用MetaObject对象的setValue方法进行赋值,(实际上修改的就是Object类型的rowValue对象的值,只不过该对象的引用被传入了MetaObject对象中进行了处理),论证请看下列源码。

//该方法正是Mybatis的自动映射方法
private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException {
    List<UnMappedColumnAutoMapping> autoMapping = createAutomaticMappings(rsw, resultMap, metaObject, columnPrefix);
    boolean foundValues = false;
    if (!autoMapping.isEmpty()) {
      for (UnMappedColumnAutoMapping mapping : autoMapping) {
        //通过调用该类下中TypeHandler的 T getResult(ResultSet rs, String columnName)方法进行类型处理
        final Object value = mapping.typeHandler.getResult(rsw.getResultSet(), mapping.column);
        if (value != null) {
          foundValues = true;
        }
        if (value != null || (configuration.isCallSettersOnNulls() && !mapping.primitive)) {
          // gcode issue #377, call setter on nulls (value is not 'found')
          //通过反射的方式调用实体类中的setter方法进行赋值操作
          metaObject.setValue(mapping.property, value);
        }
      }
    }
    return foundValues;
  }

参数处理的整体流程( final Object value = mapping.typeHandler.getResult(rsw.getResultSet(), mapping.column)😉
在这里插入图片描述
下列是Mybtais封装结果集,metaObject.setValue(mapping.property, value)的底层实现

private void setBeanProperty(PropertyTokenizer prop, Object object, Object value) {
    try {
    //Invoker底层有三个实现类GetFieldInvoker(即获取实体类字段),SetFieldInvoker(封装实体类字段),MethodInvoker(调用Method类下的invoke方法执行实体类中的方法)
      Invoker method = metaClass.getSetInvoker(prop.getName());
      Object[] params = {value};
      try {
        method.invoke(object, params);
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    } catch (Throwable t) {
      throw new ReflectionException("Could not set property '" + prop.getName() + "' of '" + object.getClass() + "' with value '" + value + "' Cause: " + t.toString(), t);
    }
  }

在这里插入图片描述
如上所述,参数处理和实体类属性的封装都是在一个for循环中,通过循环遍历每一个字段类型,依次进行类型处理并封装结果集。
该异常的出现便是因为在通过有参构造器实例化对象的同时也对其进行了初始化的操作,但由于数据库的字段类型与实体类的属性类型不匹配,且在通过构造器进行赋值的同时也并未调用类型处理器,所以会抛出该异常。而解决方式为,将实例化操作和初始化操作分开,通过无参构造器实例化对象,最后依次遍历该对象中的属性和数据库字段,进行类型处理,封装结果。

3. 发现问题

结合该异常的两种解决方式,发现了一个新的问题 ——效率问题。当实体类属性与数据库字段类型一致时,使用有参构造器进行实例化并初始化完成后得到的完整对象,发现该对象在所有属性有值的情况下,仍然会重复调用循环进行参数类型处理,通过反射调用setter方法进行重新赋值,一个无意义的覆盖操作。由于反射的性能很差,将Java加载期该做的事情,延后到了运行期做。相比来说,做的事情也更多。是不建议在频率高的情况下使用。这里必然会导致程序执行慢,效率低等问题。

在这里插入图片描述
很显然通过上述分析applyAutomaticMappings(rsw, resultMap, metaObject, null)方法就是自动映射的方法,Mybatis默认开启了自动映射,该方法是一个极耗性能的方法,反复的进行了反射操作,这也是为什么在平时不推荐使用Mybatis自动映射的原因。是否使用自动映射的关键正是if判断中shouldApplyAutomaticMappings(resultMap, false)方法的返回值。
在这里插入图片描述
了解到是否开启自动的映射的关卡后,最终可以得出最优解 ——通过配置文件手动映射数据库字段确保类型匹配,并在该resultMap中关闭autoMapping,使其不运行applyAutomaticMappings自动映射方法,进而减少性能损耗,若是需要类型处理,也可在resultMap下的arg标签中配置相应的类型处理器。

	<resultMap id="mapping" type="com.hnjd.entity.User" autoMapping="false">
        <constructor>
          <!-- 手动指定数据库中每列所对应的类型-->
            <arg column="id" javaType="java.lang.Integer"></arg>
            <arg column="username" javaType="java.lang.String"></arg>
            <arg column="birthday" javaType="java.sql.Timestamp"></arg>
            <arg column="sex" javaType="java.lang.String"></arg>
            <arg column="address" javaType="java.lang.String"></arg>
        </constructor>
    </resultMap>

通过测试不同映射方式,对比执行一条查询语句所消耗时间。

第一次第二次第三次平均值
自动映射279ms300ms306ms295ms
手动映射255ms289ms293ms279ms

4. 后言

当使用Mybatis时,且在实体类中使用了参数构造器,如果追求效率,最好还是不要使用自动映射的方式,底层循环调用反射机制过于消耗性能,通过手动映射的方式处理,更加高效。
该文是我由该异常一直debug观察流程,根据源码分析后所得的一些心得,谢谢阅读。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值