Mybatis返回类型为Map时遇到的类型转化的异常问题
作者:猩猩丶灯
一、问题背景
为了满足通用化的设计,项目中通过入参来指定查询的数据表并以Map
的形式获取所有数据,同时希望所有取得的数据都是String
的格式,因此使用List<Map<String, String>>
来存储取得的数据。
Mapper.java
List<Map<String, String>> selectAllByStatement(...)
Mapper.xml
<select id="selectAllByStatement" parameterType="java.lang.String" resultType="java.util.Map"> ... </select>
二、遇到的问题
从数据库中取得的数据无法被正常使用↓
List<Map<String, String>> maps = mapper.selectAllByStatement(tableName, request, statement); Double val1 = Double.valueOf(maps.get(0).get("gmv")); // java.lang.ClassCastException: java.lang.Double cannot be cast to java.lang.String Double val2 = Double.valueOf(maps.get(0).get("gmv").toString()); // java.lang.ClassCastException: java.lang.Double cannot be cast to java.lang.String Double val3 = maps.get(0).get("gmv"); // 编译报错
由于gmv
字段在数据库中的数据类型是double
结果导致maps.get(0)
中的数据,编译时看上去是Map定义的类型String
,运行时是数据库中对应的类型Double
,然后出现以上错误。具体错误原因没有细究
三、解决思路
方法一
Mapper中返回类型定义为 Object,即:
List<Map<String, Object>> selectAllByStatement(...)
每次使用的时候,可以toString()
转化再使用
方法二
本文主要讨论这种方式
mybatis从数据库中读取数据,并以反射的方式生成Object的对象
然后使用对应类型的TypeHandler对其进行数据类型的转化
因此,我们需要在TypeHandler上做手脚,自定义TypeHandler并将其注册到TypeHandlerMap中,让特定的数据类型转化走我们的TypeHandler
在此次问题中,我们以gmv
字段为例,
首先逻辑走到 6.1的24行 handler = typeHandlerRegistry.getTypeHandler(propertyType, jdbcType);
propertyType
是Object
,jdbcType
是Double.class
但是mybatis没有定义Object
→ Double
的handler
所以会走到 6.1的32行 handler = typeHandlerRegistry.getTypeHandler(javaType, jdbcType);
javaType
是从Double
,jdbcType
是Double.class
然后会匹配到org.apache.ibatis.type.DoubleTypeHandler
,把数据转换成Double
所以,我们该怎么做呢,我们可以定义一个Object
→ Double
的Handler,让他在6.1的24行的时候匹配到,里面转化的逻辑我们定义成Object.toString()
,具体操作看《五、具体实现》
四、mybatis处理查询结果的源码
本节是对mybatis查询数据时,对查询结果的处理过程的个人理解
1、查询
@Override public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException { PreparedStatement ps = (PreparedStatement) statement; // 执行查询 ps.execute(); // 处理查询结果 --> 2.1 return resultSetHandler.handleResultSets(ps); }
2、准备处理查询结果
2.0.1
public class ResultSetWrapper { private final ResultSet resultSet; // 保存着原始数据 private final TypeHandlerRegistry typeHandlerRegistry; private final List<String> columnNames = new ArrayList<>(); // 数据库列名 private final List<String> classNames = new ArrayList<>(); // 对应的Java类型 private final List<JdbcType> jdbcTypes = new ArrayList<>(); // 对应的数据库类型 private final Map<String, Map<Class<?>, TypeHandler<?>>> typeHandlerMap = new HashMap<>(); private final Map<String, List<String>> mappedColumnNamesMap = new HashMap<>(); private final Map<String, List<String>> unMappedColumnNamesMap = new HashMap<>(); }
2.1 org.apache.ibatis.executor.resultset.DefaultResultSetHandler#handleRowValuesForSimpleResultMap
逐行处理并存储数据
// rsw 保存着查询结果的各种相关数据 --> 2.0.1 private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException { DefaultResultContext<Object> resultContext = new DefaultResultContext<>(); ResultSet resultSet = rsw.getResultSet(); skipRows(resultSet, rowBounds); // 一行行处理 while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) { ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null); // 1. 处理:数据对象的生成、类型转化等等 Object rowValue = getRowValue(rsw, discriminatedResultMap, null); // 2. 存储处理的结果 storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet); } }
3、每一行的处理
解析数据库中取得的数据rsw
,反射生成Java的类型
3.0.1
public class MetaObject { private final Object originalObject; // 指向数据对象 private final ObjectWrapper objectWrapper; private final ObjectFactory objectFactory; private final ObjectWrapperFactory objectWrapperFactory; private final ReflectorFactory reflectorFactory; }
3.1 将数据库「一行」数据转化为对象,并逐字段解析成对应的数据类型
private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException { final ResultLoaderMap lazyLoader = new ResultLoaderMap(); // 1. 创建结果对象map - 数据实例化BaseTypeHandler#getNullableResult Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix); if (rowValue != nmonomialull && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) { // 2. 创建MetaObject对象,「并将成员变量originalObject指向rowValue」,之后在操作直接对metaObject进行操作; MetaObject --> 3.0.2 final MetaObject metaObject = configuration.newMetaObject(rowValue); boolean foundValues = this.useConstructorMappings; // 3. 是否应用自动映射,也就是通过resultType进行映射 if (shouldApplyAutomaticMappings(resultMap, false)) { // 3.1 主要处理流程 --> 4.1 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; } // 解析完后返回metaObject.originalObject return rowValue; }
4、数据类型自动映射
4.0.1
private static class UnMappedColumnAutoMapping { private final String column; // 列名 private final String property; // 属性名 private final TypeHandler<?> typeHandler; // 该列数据的数据类型转化Handler private final boolean primitive; }
4.1 为每一列字段找到他的转换Handler
- 通过Handler反射生成他们在Java中的类型
- 将反射生成的对象放入
metaObject.originalObject
中
private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException { // 包含每一列数据类型对应的转化Handler --> 5.1 List<UnMappedColumnAutoMapping> autoMapping = createAutomaticMappings(rsw, resultMap, metaObject, columnPrefix); boolean foundValues = false; if (!autoMapping.isEmpty()) { for (UnMappedColumnAutoMapping mapping : autoMapping) { // 数据转换;表面为Object实际为MySQL对应的类型 final Object value = mapping.typeHandler.getResult(rsw.getResultSet(), mapping.column); if (value != null) { foundValues = true; } if (value != null || (configuration.isCallSettersOnNulls() && !mapping.primitive)) { // 将反射生成的对象放入metaObject.originalObject中 metaObject.setValue(mapping.property, value); } } } return foundValues; }
5、为每个字段匹配转换Handler
5.1 得到每个字段的对应的Handler
的List(Handler
包在UnMappedColumnAutoMapping
里)
private List<UnMappedColumnAutoMapping> createAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException { autoMapping = new ArrayList<>(); final List<String> unmappedColumnNames = rsw.getUnmappedColumnNames(resultMap, columnPrefix); // 1. 对于每一列 分别去找他们的 Handler for (String columnName : unmappedColumnNames) { //... if (typeHandlerRegistry.hasTypeHandler(propertyType, rsw.getJdbcType(columnName))) { // 得到该类型 的 TypeHandler。 // 优先用属性类型property type匹配`TypeHandler`,如果没有再用column JDBC Type去匹配 final TypeHandler<?> typeHandler = rsw.getTypeHandler(propertyType, columnName); autoMapping.add(new UnMappedColumnAutoMapping(columnName, property, typeHandler, propertyType.isPrimitive())); } //... } return autoMapping; }
6、字段匹配Handler
6.1 优先用属性类型property type匹配TypeHandler
,如果没有再用column JDBC Type去匹配
public class ResultSetWrapper { private final Map<String, Map<Class<?>, TypeHandler<?>>> typeHandlerMap = new HashMap<>(); // 缓存 /** * Gets the type handler to use when reading the result set. * Tries to get from the TypeHandlerRegistry by searching for the property type. * If not found it gets the column JDBC type and tries to get a handler for it. */ public TypeHandler<?> getTypeHandler(Class<?> propertyType, String columnName) { TypeHandler<?> handler = null; // 先找缓存 // typeHandlerMap 作为缓存 Map<Class<?>, TypeHandler<?>> columnHandlers = typeHandlerMap.get(columnName); // Map<String, Map<Class<?>, TypeHandler<?>>> if (columnHandlers == null) { columnHandlers = new HashMap<>(); typeHandlerMap.put(columnName, columnHandlers); } else { handler = columnHandlers.get(propertyType); } // 没有缓存的话↓ if (handler == null) { // 获得列名对应的数据库中的类型JdbcType JdbcType jdbcType = getJdbcType(columnName); // 数据库中的类型 // ** 用属性类型propertyType去找 ** --> 7.1 handler = typeHandlerRegistry.getTypeHandler(propertyType, jdbcType); // ↑ 用属性类型propertyType没找到的话 就 ↓ if (handler == null || handler instanceof UnknownTypeHandler) { // 找该列在数据库的实际类型在Java中对应的javaType final int index = columnNames.indexOf(columnName); final Class<?> javaType = resolveClass(classNames.get(index)); if (javaType != null && jdbcType != null) { // ** 用数据库类型去找 ** --> 7.1 handler = typeHandlerRegistry.getTypeHandler(javaType, jdbcType); } // ... } if (handler == null || handler instanceof UnknownTypeHandler) { handler = new ObjectTypeHandler(); } // 缓存记录 columnHandlers.put(propertyType, handler); } return handler; } }
7、给定Type和jdbcType匹配Handler
7.1 org.apache.ibatis.type.TypeHandlerRegistry#getTypeHandler
根据type和jdbcType一起查符合的Handler
一个propertyType会对应多个jdbcType的Handler
所以存储Handler的变量类型是 Map<Type, Map<JdbcType, TypeHandler<?>>>
private <T> TypeHandler<T> getTypeHandler(Type type, JdbcType jdbcType) { if (ParamMap.class.equals(type)) { return null; } /** * 一个propertyType会对应多个jdbcType的Handler * 所以存储Handler的变量类型是 Map<Type, Map<JdbcType, TypeHandler<?>>> **/ // 先根据propertyType去找 --> 7.2 Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = getJdbcHandlerMap(type); TypeHandler<?> handler = null; if (jdbcHandlerMap != null) { // 找到之后,第二层Map再用jdbcType去找 handler = jdbcHandlerMap.get(jdbcType); if (handler == null) { // 如果没找到走默认的,UnknownTypeHandler handler = jdbcHandlerMap.get(null); } if (handler == null) { // #591 handler = pickSoleHandler(jdbcHandlerMap); } } // type drives generics here return (TypeHandler<T>) handler; }
7.2 给定Java中的类型,查找对应的Handler
// 给定Java中的类型,查找对应的Handler private Map<JdbcType, TypeHandler<?>> getJdbcHandlerMap(Type type) { Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = typeHandlerMap.get(type); // typeHandlerMap在初始化时,JDBC Handler被手动注册上的 --> 8.1 //... return jdbcHandlerMap; }
8、Handler的初始化
8.1 org.apache.ibatis.type.TypeHandlerRegistry
TypeHandlerRegistry
的构造函数会把所有的类型和他对应的Handler都注册到Map上
public final class TypeHandlerRegistry { // 储存所有的Handler private final Map<Type, Map<JdbcType, TypeHandler<?>>> typeHandlerMap = new ConcurrentHashMap<>(); public TypeHandlerRegistry() { register(Boolean.class, new BooleanTypeHandler()); register(boolean.class, new BooleanTypeHandler()); register(JdbcType.BOOLEAN, new BooleanTypeHandler()); register(JdbcType.BIT, new BooleanTypeHandler()); register(Integer.class, new IntegerTypeHandler()); register(int.class, new IntegerTypeHandler()); register(JdbcType.INTEGER, new IntegerTypeHandler()); //... 把所有常规的映射情况都注册上了 register(Reader.class, new ClobReaderTypeHandler()); register(String.class, new StringTypeHandler()); register(String.class, JdbcType.CHAR, new StringTypeHandler()); register(String.class, JdbcType.CLOB, new ClobTypeHandler()); register(String.class, JdbcType.VARCHAR, new StringTypeHandler()); register(String.class, JdbcType.LONGVARCHAR, new StringTypeHandler()); } private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) { if (javaType != null) { Map<JdbcType, TypeHandler<?>> map = typeHandlerMap.get(javaType); if (map == null || map == NULL_TYPE_HANDLER_MAP) { map = new HashMap<>(); typeHandlerMap.put(javaType, map); // typeHandlerMap: Map<Type, Map<JdbcType, TypeHandler<?>>> } map.put(jdbcType, handler); } allTypeHandlersMap.put(handler.getClass(), handler); } }
五、具体实现
实现Handler
自定义TypeHanlder需要继承BaseTypeHandler
用@MappedJdbcTypes
,@MappedTypes
来定义此Handler应用于哪些类型的转换
@MappedTypes
指定字段在Java的数据类型
@MappedJdbcTypes
指定字段在数据库中的类型
import org.apache.ibatis.type.BaseTypeHandler; import org.apache.ibatis.type.JdbcType; import org.apache.ibatis.type.MappedJdbcTypes; import org.apache.ibatis.type.MappedTypes; import java.sql.CallableStatement; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Date; @MappedTypes({Object.class}) @MappedJdbcTypes({JdbcType.DOUBLE, JdbcType.DATE, JdbcType.BIGINT, JdbcType.TIMESTAMP, JdbcType.INTEGER}) public class MapToStringTypeHandler extends BaseTypeHandler<Object> { @Override public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException { if (parameter instanceof Date){ ps.setDate(i, new java.sql.Date(((Date)parameter).getTime())); } else { ps.setString(i, parameter.toString()); } } @Override public String getNullableResult(ResultSet rs, String columnName) throws SQLException { return rs.getString(columnName); } @Override public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException { return rs.getString(columnIndex); } @Override public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { return cs.getString(columnIndex); } }
配置Handler
在Mybatis 的配置类中指定自定义Handler的包
com.wujie.pandora.repository.config.DataAnalysisMybatisConfig#buildSqlSessionFactory
public SqlSessionFactory buildSqlSessionFactory() { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); //... // 指定自定义Handler的包 bean.setTypeHandlersPackage("com.wujie.pandora.repository.handler"); return bean.getObject(); }
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。