MyBatis 经典踩坑:有参构造函数引发列顺序错位类型转换异常

发布时间:2026/7/24 11:22:40
MyBatis 经典踩坑:有参构造函数引发列顺序错位类型转换异常 一、现象日志执行数据库查询抛出异常org.springframework.dao.DataIntegrityViolationException: Error attempting to get column ret_msg from result set. Cause: java.sql.SQLDataException: Cannot determine value type from string 系统异常null Caused by: com.mysql.cj.exceptions.DataConversionException: Cannot determine value type from string 系统异常null堆栈特征at org.apache.ibatis.type.IntegerTypeHandler.getNullableResult at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.createUsingConstructor直观感受实体字段明明类型和数据库一致数据库ret_msg是varchar、实体retMsg是String却报字符串无法转为数字。很多人第一反应实体属性类型写错这是典型误判。根因不是字段定义是MyBatis构造函数实例化机制。二、环境还原数据库表CREATETABLEsys_account_deactivate_log(idvarchar(32),interface_namevarchar(255),emp_codevarchar(255),paramvarchar(255),ret_msgvarchar(255),ret_statusvarchar(255),call_timedatetime(3),PRIMARYKEY(id));实体代码DataTableName(sys_account_deactivate_log)publicclassAccountDeactivateLogEntity{privateStringid;privateStringinterfaceName;privateStringempCode;privateStringparam;privateStringretMsg;privateIntegerretStatus;privateDatecallTime;// 自定义有参构造【风险源头】publicAccountDeactivateLogEntity(StringinterfaceName,StringempCode,Stringparam,StringretMsg,IntegerretStatus){this.interfaceNameinterfaceName;this.empCodeempCode;this.paramparam;this.retMsgretMsg;this.retStatusretStatus;this.callTimenewDate();}}三、底层原理MyBatis DefaultResultSetHandler源码解读核心源码位置org.apache.ibatis.executor.resultset.DefaultResultSetHandler#createUsingConstructorprivateObjectcreateByConstructorSignature(ResultSetWrapperrsw,Class?resultType,ListClass?constructorArgTypes,ListObjectconstructorArgs)throwsSQLException{// 1. 获取实体所有构造函数finalConstructor?[]constructorsresultType.getDeclaredConstructors();// 2. 查找合适的构造函数// --- 如果实体只有唯一一个构造器直接返回它// --- 多个构造器时寻找带有AutomapConstructor注解的构造器finalConstructor?defaultConstructorfindDefaultConstructor(constructors);if(defaultConstructor!null){// 实体类存在构造returncreateUsingConstructor(rsw,resultType,constructorArgTypes,constructorArgs,defaultConstructor);}else{// ❌ 没有构造被迫遍历所有构造方法尝试匹配for(Constructor?constructor:constructors){// 判断当前构造器参数类型列表 是否能和查询结果JDBC类型兼容if(allowedConstructorUsingTypeHandlers(constructor,rsw.getJdbcTypes())){// 找到一个合适的有参构造进入创建逻辑returncreateUsingConstructor(rsw,resultType,constructorArgTypes,constructorArgs,constructor);}}}// 所有构造器都无法匹配抛出异常thrownewExecutorException(No constructor found in resultType.getName() matching rsw.getClassNames());}privateObjectcreateUsingConstructor(ResultSetWrapperrsw,Class?resultType,ListClass?constructorArgTypes,ListObjectconstructorArgs,Constructor?constructor)throwsSQLException{booleanfoundValuesfalse;// 循环遍历构造器每一个入参for(inti0;iconstructor.getParameterTypes().length;i){// 获取【构造器第i个参数】的Java类型Class?parameterTypeconstructor.getParameterTypes()[i];// 【致命一行】// rsw.getColumnNames()SQL查询返回的所有列名称有序列表// i循环下标【按下标顺序拿第i列不是按名称匹配】StringcolumnNamersw.getColumnNames().get(i);// // 根据参数Java类型获取对应的TypeHandler例如 Integer→IntegerTypeHandlerString→StringTypeHandlerTypeHandler?typeHandlerrsw.getTypeHandler(parameterType,columnName);// 使用TypeHandler从ResultSet读取这一列的值ObjectvaluetypeHandler.getResult(rsw.getResultSet(),columnName);// 收集参数类型与参数值后续反射调用构造器constructorArgTypes.add(parameterType);constructorArgs.add(value);foundValuesvalue!null||foundValues;}// 反射调用构造方法创建实体对象returnfoundValues?objectFactory.create(resultType,constructorArgTypes,constructorArgs):null;}}privatebooleanallowedConstructorUsingTypeHandlers(finalConstructor?constructor,finalListJdbcTypejdbcTypes){// 获取当前构造器所有入参Java类型finalClass?[]parameterTypesconstructor.getParameterTypes();// 条件1构造器参数个数 必须 SQL查询返回的列数量if(parameterTypes.length!jdbcTypes.size()){returnfalse;}// 条件2逐个位置校验Java类型 -- JDBC类型 是否存在可用TypeHandlerfor(inti0;iparameterTypes.length;i){// parameterTypes[i]构造器第i个参数Java类型// jdbcTypes.get(i)查询结果第i列的JDBC类型if(!typeHandlerRegistry.hasTypeHandler(parameterTypes[i],jdbcTypes.get(i))){returnfalse;}}// 两个条件全部满足允许使用该构造器实例化对象returntrue;}重中之重一条规则记住当MyBatis使用有参构造反射创建对象时构造器第N个参数 ← 查询结果集中第N列数据【只按下标顺序绑定不匹配列名】本次事故流程演示你的实体手写有参构造没有无参构造MyBatis 进入else分支匹配到你的有参构造进入循环构造器参数[0] ← 查询列[0] 构造器参数[1] ← 查询列[1] 构造器参数[2] ← 查询列[2] 构造器参数[3] ← 查询列[3] 构造器参数[4] ← 查询列[4]类型IntegerSQL select 字段顺序 和 构造器参数顺序不一致发生列错位查询第4列的值实际是字符串系统异常null当前参数类型是Integer启用IntegerTypeHandler底层调用resultSet.getInt(列名)MySQL驱动无法把文本转为数字抛出异常Cannot determine value type from string 系统异常null额外迷惑点很多人踩坑异常日志打印get column ret_msg会误导开发认为retMsg属性类型写错真实原因只是错位之后这一列刚好是 ret_msg实体本身字段定义完全没问题四、容易混淆的误区❌ 误区1实体字段和数据库字段名称一致就会按名称映射错有参构造实例化模式下只认顺序不认名称。只有「无参构造 setter赋值」才会根据列名映射。❌ 误区2报错信息写了get column ret_msg代表实体retMsg类型错误报错只是JDBC驱动执行getInt操作时当前游标对应的列恰好是ret_msg不代表实体retMsg定义错误非常具有迷惑性❌ 误区3MP自带自动映射手写SQL顺序无关紧要MP自动映射只作用于setter注入模式构造器模式不受MybatisPlus控制底层依然遵循MyBatis原生规则。五、解决方案推荐优先级从高到低方案1实体必须保留无参构造删除业务自定义有参构造DataTableName(sys_account_deactivate_log)publicclassAccountDeactivateLogEntity{privateStringid;privateStringinterfaceName;privateStringempCode;privateStringparam;privateStringretMsg;privateIntegerretStatus;privateDatecallTime;// Lombok Data 默认提供无参构造不要手动删除// 删除所有自定义有参构造}业务构建对象方式AccountDeactivateLogEntityentitynewAccountDeactivateLogEntity();entity.setInterfaceName(xxx);entity.setRetMsg(系统异常null);entity.setRetStatus(3);优势MyBatis优先选择无参构造实例化之后通过setter方法按数据库列名匹配赋值和SQL字段顺序完全无关从根源杜绝该类问题。方案2实体仍须保留有参构造定义一个无常构造函数DataTableName(sys_account_deactivate_log)publicclassAccountDeactivateLogEntity{privateStringid;privateStringinterfaceName;privateStringempCode;privateStringparam;privateStringretMsg;privateIntegerretStatus;privateDatecallTime;publicAccountDeactivateLogEntity(){}publicAccountDeactivateLogEntity(StringinterfaceName,StringempCode,Stringparam,StringretMsg,IntegerretStatus){this.interfaceNameinterfaceName;this.empCodeempCode;this.paramparam;this.retMsgretMsg;this.retStatusretStatus;this.callTimenewDate();}}