一文帮你搞定MyBatis的类型转换模块,深度好文,欢迎一键三连!!!
Posted 波波烤鸭
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一文帮你搞定MyBatis的类型转换模块,深度好文,欢迎一键三连!!!相关的知识,希望对你有一定的参考价值。
本文来给大家详细的分析下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 标签
继续往下,看到关键方法调用
一文帮你搞定Yolov3
深度分析:那些Java中你一定遇到过的问题,一次性帮你搞定!深度分析:那些Java中你一定遇到过的问题,一次性帮你搞定!