本文来给大家详细的分析下MyBatis的基础支持层中的类型转换模块。
类型转换模块
MyBatis是一个持久层框架ORM框架,实现数据库中数据和Java对象中的属性的双向映射,那么不可避免的就会碰到类型转换的问题,在PreparedStatement为SQL语句绑定参数时,需要从Java类型转换为JDBC类型,而从结果集中获取数据时,则需要从JDBC类型转换为Java类型,所以我们来看下在MyBatis中是如何实现类型的转换的。
String sql = "SELECT id,user_name from t_user where id = ?"; stmt = conn.prepareStatement(sql); // 占位符赋值 stmt.setInt(1,2); ResultSet rs = stmt.executeQuery(); // 获取结果集 结果集映射 while (rs.next()) { Integer id = rs.getInt("id"); String userName = rs.getString("user_name"); user.setId(id); user.setUserName(userName); }
MyBatis中就是通过类型转换模块来解决JDBC中的占位符赋值和结果集中的数据处理的。
一.结构设计
首先我们来看下类型转换模块的结构设计。
在这么多的Java类中,比较核心的是TypeHandler,BaseTypeHandler,以及众多的具体的类型处理器( XXXXTypeHandler ) 他们之间有如下的关系。
还有就是提供的有两个注册器,TypeAliasRegister,TypeHandlerRegister.用来实现相关数据的存储。
1.TypeHandler
MyBatis中的所有的类型转换器都继承了TypeHandler接口,在TypeHandler中定义了类型转换器的最基本的功能。
/** * @author Clinton Begin */ public interface TypeHandler<T> { /** * 负责将Java类型转换为JDBC的类型 * 本质上执行的就是JDBC操作中的 如下操作 * String sql = "SELECT id,user_name,real_name,password,age,d_id from t_user where id = ? and user_name = ?"; * ps = conn.prepareStatement(sql); * ps.setInt(1,2); * ps.setString(2,"张三"); * @param ps * @param i 对应占位符的 位置 * @param parameter 占位符对应的值 * @param jdbcType 对应的 jdbcType 类型 * @throws SQLException */ void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException; /** * 从ResultSet中获取数据时会调用此方法,会将数据由JdbcType转换为Java类型 * @param columnName Colunm name, when configuration <code>useColumnLabel</code> is <code>false</code> */ T getResult(ResultSet rs, String columnName) throws SQLException; T getResult(ResultSet rs, int columnIndex) throws SQLException; T getResult(CallableStatement cs, int columnIndex) throws SQLException; }
我们可以看到这个接口中定义的两类方法分别是
- setParameter 对占位符赋值
- getResult 根据字段获取值
2.BaseTypeHandler
为了方便用户自定义TypeHandler的实现,在MyBatis中提供了BaseTypeHandler这个抽象类,它实现了TypeHandler接口,并继承了TypeReference类,
在BaseTypeHandler中的实现方法中实现了对null的处理,非空的处理是交给各个子类去实现的。这个在代码中很清楚的体现了出来. — 代码有简化,方便查看
@Override public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException { if (parameter == null) { // 简化了代码 如果参数为空就设置 null ps.setNull(i, jdbcType.TYPE_CODE); } else { // 省略 try 语句 参数不为空就调用子类的实现 setNonNullParameter(ps, i, parameter, jdbcType); } } @Override public T getResult(ResultSet rs, String columnName) throws SQLException { return getNullableResult(rs, columnName); } @Override public T getResult(ResultSet rs, int columnIndex) throws SQLException { return getNullableResult(rs, columnIndex); } @Override public T getResult(CallableStatement cs, int columnIndex) throws SQLException { return getNullableResult(cs, columnIndex); } public abstract void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException; /** * 子类重写这些抽象方法!!! */ public abstract T getNullableResult(ResultSet rs, String columnName) throws SQLException; public abstract T getNullableResult(ResultSet rs, int columnIndex) throws SQLException; public abstract T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException;
3.TypeHandler实现类
TypeHandler的实现类比较多,而且实现也都比较简单。
以Integer为例
/** * @author Clinton Begin */ public class IntegerTypeHandler extends BaseTypeHandler<Integer> { @Override public void setNonNullParameter(PreparedStatement ps, int i, Integer parameter, JdbcType jdbcType) throws SQLException { ps.setInt(i, parameter); // 实现参数的绑定 } @Override public Integer getNullableResult(ResultSet rs, String columnName) throws SQLException { int result = rs.getInt(columnName); // 获取指定列的值 return result == 0 && rs.wasNull() ? null : result; } @Override public Integer getNullableResult(ResultSet rs, int columnIndex) throws SQLException { int result = rs.getInt(columnIndex); // 获取指定列的值 return result == 0 && rs.wasNull() ? null : result; } @Override public Integer getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { int result = cs.getInt(columnIndex); // 获取指定列的值 return result == 0 && cs.wasNull() ? null : result; } }
4.TypeHandlerRegistry
通过前面的介绍我们发现在MyBatis中给我们提供的具体的类型转换器实在是太多了,那么在实际的使用时我们是如何知道使用哪个转换器类处理的呢?实际上再MyBatis中是将所有的TypeHandler都保存注册在了TypeHandlerRegistry中的。首先注意声明的相关属性
// 记录JdbcType和TypeHandle的对应关系 private final Map<JdbcType, TypeHandler<?>> jdbcTypeHandlerMap = new EnumMap<>(JdbcType.class); // 记录Java类型向指定的JdbcType转换时需要使用到的TypeHandle private final Map<Type, Map<JdbcType, TypeHandler<?>>> typeHandlerMap = new ConcurrentHashMap<>(); private final TypeHandler<Object> unknownTypeHandler; // 记录全部的TypeHandle类型及对应的TypeHandle对象 private final Map<Class<?>, TypeHandler<?>> allTypeHandlersMap = new HashMap<>(); // 空TypeHandle的标识 private static final Map<JdbcType, TypeHandler<?>> NULL_TYPE_HANDLER_MAP = Collections.emptyMap();
然后在器构造方法中完成了系统提供的TypeHandler的注册
代码太长,请自行查阅。注意的是register()方法, 关键几个实现如下
private <T> void register(Type javaType, TypeHandler<? extends T> typeHandler) { // 获取@MappedJdbcTypes注解 MappedJdbcTypes mappedJdbcTypes = typeHandler.getClass().getAnnotation(MappedJdbcTypes.class); if (mappedJdbcTypes != null) { // 遍历获取注解中指定的 JdbcType 类型 for (JdbcType handledJdbcType : mappedJdbcTypes.value()) { // 调用下一个重载的方法 register(javaType, handledJdbcType, typeHandler); } if (mappedJdbcTypes.includeNullJdbcType()) { // JdbcType类型为空的情况 register(javaType, null, typeHandler); } } else { register(javaType, null, typeHandler); } } private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) { if (javaType != null) {// 如果不为空 // 从 TypeHandle集合中根据Java类型来获取对应的集合 Map<JdbcType, TypeHandler<?>> map = typeHandlerMap.get(javaType); if (map == null || map == NULL_TYPE_HANDLER_MAP) { // 如果没有就创建一个新的 map = new HashMap<>(); } // 把对应的jdbc类型和处理器添加到map集合中 map.put(jdbcType, handler); // 然后将 java类型和上面的map集合保存到TypeHandle的容器中 typeHandlerMap.put(javaType, map); } // 同时也把这个处理器添加到了 保存有所有处理器的容器中 allTypeHandlersMap.put(handler.getClass(), handler); }
有注册的方法,当然也有从注册器中获取TypeHandler的方法,getTypeHandler方法,这个方法也有多个重载的方法,这里重载的方法最终都会执行的方法是
/** * 根据对应的Java类型和Jdbc类型来查找对应的TypeHandle */ private <T> TypeHandler<T> getTypeHandler(Type type, JdbcType jdbcType) { if (ParamMap.class.equals(type)) { return null; } // 根据Java类型获取对应的 Jdbc类型和TypeHandle的集合容器 Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = getJdbcHandlerMap(type); TypeHandler<?> handler = null; if (jdbcHandlerMap != null) { // 根据Jdbc类型获取对应的 处理器 handler = jdbcHandlerMap.get(jdbcType); if (handler == null) { // 获取null对应的处理器 handler = jdbcHandlerMap.get(null); } if (handler == null) { // #591 handler = pickSoleHandler(jdbcHandlerMap); } } // type drives generics here return (TypeHandler<T>) handler; }
当然除了使用系统提供的TypeHandler以外,我们还可以创建我们自己的TypeHandler了,之前讲解案例的时候已经带大家写过了,如果忘记可以复习下。
5 TypeAliasRegistry
我们在MyBatis的应用的时候会经常用到别名,这能大大简化我们的代码,其实在MyBatis中是通过TypeAliasRegistry类管理的。首先在构造方法中会注入系统常见类型的别名
注册的方法逻辑也比较简单
public void registerAlias(String alias, Class<?> value) { if (alias == null) { throw new TypeException("The parameter alias cannot be null"); } // issue #748 别名统一转换为小写 String key = alias.toLowerCase(Locale.ENGLISH); // 检测别名是否存在 if (typeAliases.containsKey(key) && typeAliases.get(key) != null && !typeAliases.get(key).equals(value)) { throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + typeAliases.get(key).getName() + "'."); } // 将 别名 和 类型 添加到 Map 集合中 typeAliases.put(key, value); }
那么我们在实际使用时通过package指定别名路径和通过@Alisa注解来指定别名的操作是如何实现的呢?也在TypeAliasRegistry中有实现
/** * 根据 packagename 来指定 * @param packageName * @param superType */ public void registerAliases(String packageName, Class<?> superType) { ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>(); resolverUtil.find(new ResolverUtil.IsA(superType), packageName); Set<Class<? extends Class<?>>> typeSet = resolverUtil.getClasses(); for (Class<?> type : typeSet) { // Ignore inner classes and interfaces (including package-info.java) // Skip also inner classes. See issue #6 if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) { registerAlias(type); } } } /** * 扫描 @Alias注解 * @param type */ public void registerAlias(Class<?> type) { String alias = type.getSimpleName(); // 扫描 @Alias注解 Alias aliasAnnotation = type.getAnnotation(Alias.class); if (aliasAnnotation != null) { // 获取注解中定义的别名名称 alias = aliasAnnotation.value(); } registerAlias(alias, type); }
二.核心流程应用
1.SqlSessionFactory
在构建SqlSessionFactory时,在Configuration对象实例化的时候在成员变量中完成了TypeHandlerRegistry和TypeAliasRegistry的实例化,在全局配置文件解析的时候完成了自定义的TypeAlias和TypeHandler的注册,在配置文件的加载解析中完成了SQL语句占位符的处理。
1.1 TypeHandlerRegistry和TypeAliasRegistry初始化
进入1中查看
成员变量中完成了TypeHandlerRegistry和TypeAliasRegistry的初始化操作
同时在Configuration的构造方法中完成了系统提供的类型别名的注册工作。
以上步骤完成了TypeHandlerRegistry和TypeAliasRegistry的初始化操作
然后在解析全局配置文件时会通过解析<typeAliases>标签和<typeHandlers>标签,可以注册我们添加的别名和TypeHandler。
到这儿我们搞清楚了系统初始化时的TypeHandlerRegistry和TypeAliasRegistry的操作,但是我们在<select> 标签中写的SQL语句是什么时候处理的呢?
1.2 SQL解析
映射文件解析中怎么实现的占位符的处理的呢?
<select id="selectUserById" resultMap="BaseResultMap" statementType="PREPARED" parameterType="_int" > select id, user_name , real_name , password, age, d_id from t_user where id = #{id} </select>
怎么转换为
select id, user_name , real_name , password, age, d_id from t_user where id = ?
从开始解析映射文件的位置进入。
继续进入
继续进入
public void parse() { // 总体上做了两件事情,对于语句的注册和接口的注册 // 判断是否已经加载过了 映射文件 if (!configuration.isResourceLoaded(resource)) { // 1、具体增删改查标签的解析。 // 一个标签一个MappedStatement。 >> configurationElement(parser.evalNode("/mapper")); configuration.addLoadedResource(resource); // 2、把namespace(接口类型)和工厂类绑定起来,放到一个map。 // 一个namespace 一个 MapperProxyFactory >> bindMapperForNamespace(); // 注册 Mapper 接口 } // 处理 configurationElement 方法中解析失败的 <resultMap> 节点 parsePendingResultMaps(); // 处理 configurationElement 方法中解析失败的 <cache-ref> 节点 parsePendingCacheRefs(); // 处理 configurationElement 方法中解析失败的 SQL 语句节点 parsePendingStatements(); }
进入configurationElement方法中。
继续
继续进入
开始解析具体的 select 标签
继续往下,看到关键方法调用
进入createSQLSource 方法中
进入构造器
public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) { // 获取解析器 SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration); // 得到参数类型 Class<?> clazz = parameterType == null ? Object.class : parameterType; // 解析器解析sql sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<>()); }
进入parse方法
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) { // 为参数构建 ParameterMapping ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters); // 把 #参数 解析成 ? 占位符 select * from t_user where id = #{id} GenericTokenParser parser = new GenericTokenParser("#{", "}", handler); String sql = parser.parse(originalSql); // select * from t_user where id = ? return new StaticSqlSource(configuration, sql, handler.getParameterMappings()); }
Debug可以看到的效果
到这儿我们可以总结出在构建SqlSessionFactory的过程中完成了和类型转换模块有关的操作为:
2.SQL执行
执行SQL语句的时候和类型转换模块有关系的应该是两块
- 占位符的赋值
- 结果集的映射
2.1 占位符的赋值
要掌握这块的内容首先要搞清楚在我们获取了PreparedStatement对象后及在执行SQL之前我们需要完成占位符的赋值操作。
String sql = "SELECT * from t_user where id = ? and user_name = ?"; ps = conn.prepareStatement(sql); ps.setInt(1,2); ps.setString(2,"张三"); ps.executeQuery();
继续进入
@Override public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) { try { // MappedStatement 记录了一个 select 标签所具有的所有的信息 MappedStatement ms = configuration.getMappedStatement(statement); // 如果 cacheEnabled = true(默认),Executor会被 CachingExecutor装饰 wrapCollection(parameter) 对参数进行处理 return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); } catch (Exception e) { throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }
跳过CachingExcutor进入
@Override public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { BoundSql boundSql = ms.getBoundSql(parameter); // 一级缓存和二级缓存的CacheKey是同一个 CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); return query(ms, parameter, rowBounds, resultHandler, key, boundSql); }
继续进入,执行数据库操作。
进入核心方法
@Override public void parameterize(Statement statement) throws SQLException { parameterHandler.setParameters((PreparedStatement) statement); }
处理占位符的方法
@Override public void setParameters(PreparedStatement ps) { ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId()); // 取出 SQL 中的参数映射列表 id int IntegerHandler List<ParameterMapping> parameterMappings = boundSql.getParameterMappings(); if (parameterMappings != null) { for (int i = 0; i < parameterMappings.size(); i++) { ParameterMapping parameterMapping = parameterMappings.get(i); if (parameterMapping.getMode() != ParameterMode.OUT) { // 过滤掉存储过程中的参数 Object value; // 记录实参 String propertyName = parameterMapping.getProperty(); // 获取参数名称 // 获取对应的实参值 if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params value = boundSql.getAdditionalParameter(propertyName); } else if (parameterObject == null) { value = null; } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { value = parameterObject; } else { MetaObject metaObject = configuration.newMetaObject(parameterObject); value = metaObject.getValue(propertyName); } TypeHandler typeHandler = parameterMapping.getTypeHandler(); // IntegerTypeHandler JdbcType jdbcType = parameterMapping.getJdbcType(); if (value == null && jdbcType == null) { jdbcType = configuration.getJdbcTypeForNull(); } try { // 给每一个占位符 赋值 typeHandler.setParameter(ps, i + 1, value, jdbcType); } catch (TypeException | SQLException e) { throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e); } } } } }
标识重点:
到这儿,相信对应占位符处理这块应该是很清楚了。
2.2 结果集的映射
接下来我们看看在结果集处理中是怎么利用TypeHandler来完成相关的处理工作的。
处理多结果集方法的完成代码,参考注释
@Override public List<Object> handleResultSets(Statement stmt) throws SQLException { ErrorContext.instance().activity("handling results").object(mappedStatement.getId()); // 该集合用于保存映射结果集得到的结果对象 final List<Object> multipleResults = new ArrayList<>(); int resultSetCount = 0; // 获取第一个ResultSet对象 ResultSetWrapper rsw = getFirstResultSet(stmt); // <resultMap>会被解析为 ResultMap对象,并保存到 mappedStatement.resultMaps 集合中 // 如果SQL节点能够产生多个ResultSet,那么我们可以在SQL节点的 resultMap属性中配置多个<resultMap>节点的id // 他们之间通过','分割,实现对多个结果集的映射 List<ResultMap> resultMaps = mappedStatement.getResultMaps(); // 获取 resultMap的个数 int resultMapCount = resultMaps.size(); // 验证 如果结果集不为空 那么 resultMaps不能为空,否则抛异常 validateResultMapsCount(rsw, resultMapCount); // 遍历 resultMaps 集合 while (rsw != null && resultMapCount > resultSetCount) { // 获取循环的 ResultMap对象 ResultMap resultMap = resultMaps.get(resultSetCount); // 处理单个ResultSet对象 也就是根据resultMap中定义的映射规则对ResultSet进行映射 // 并将结果添加到 multipleResults 中 对单个结果集的映射 handleResultSet(rsw, resultMap, multipleResults, null); rsw = getNextResultSet(stmt);// 获取下一个结果集 cleanUpAfterHandlingResultSet(); // 清空 nestedResultObjects resultSetCount++; } // resultSets 该属性仅仅对多结果集的情况适用,该属性将列出语句执行后返回的结果集,并给每个结果集一个名称 // 名称是逗号分隔的 可以先不关注 String[] resultSets = mappedStatement.getResultSets(); if (resultSets != null) { while (rsw != null && resultSetCount < resultSets.length) { ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]); if (parentMapping != null) { String nestedResultMapId = parentMapping.getNestedResultMapId(); ResultMap resultMap = configuration.getResultMap(nestedResultMapId); handleResultSet(rsw, resultMap, null, parentMapping); } rsw = getNextResultSet(stmt); cleanUpAfterHandlingResultSet(); resultSetCount++; } } return collapseSingleResultList(multipleResults); }
单个结果集处理的方法入口是:
继续进入
进入到关键方法中。
applyAutomaticMappings 方法中完成的是没有使用 resultMap 标签的情况
private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException { // 获取ResultSet中存在,但在ResultMap中没有明确映射的列 List<UnMappedColumnAutoMapping> autoMapping = createAutomaticMappings(rsw, resultMap, metaObject, columnPrefix); boolean foundValues = false; if (!autoMapping.isEmpty()) { for (UnMappedColumnAutoMapping mapping : autoMapping) { // 使用TypeHandler 获取自定映射的列值 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') // 将自动映射的属性值设置到结果对象中 metaObject.setValue(mapping.property, value); } } } return foundValues; }
重点标识:
applyPropertyMappings方法完成的是有 resultMap 标签映射的情况
~ 好了,到这给大伙介绍完了 类型转换模块的相关内容,希望这部分能够帮助到大家。