大家好,我是Dog Lee 😁😁 一名金融科技领域的JAVA系统研发😊😊
我希望将自己工作和学习中的经验以最朴实,最严谨的方式分享给大家,共同进步👉💓👈
《MyBatis源码与实战》专栏,会陆续更新关于MyBatis的源码讲解,实战使用等内容
👉👉👉👉👉👉👉👉💓写作不易,期待大家的关注和点赞💓👈👈👈👈👈👈👈👈
目录
- 前言
- 正文
- 一. 示例工程搭建
- 二. 问题演示与解决
- 三. 如何自定义TypeHandler
- 四. TypeHandler如何生效
- 五. 小补充
- 总结
前言
在数据库的使用中,一个JAVA对象的字段有其JAVA类型,比如String
类型,当这个JAVA对象存储到数据库中后,其字段在数据库中也需要以相应的JDBC类型来存储,比如VARCHAR
。同理,当从数据库中查询出一条数据时也需要将JDBC类型转换成JAVA类型才能将这条数据映射成一个JAVA对象。JAVA类型到JDBC类型和JDBC类型到JAVA类型的转换,MyBatis框架提供了TypeHandler
作为解决方案。
MyBatis提供了大量内置的TypeHandler
用于常规数据类型的转换,例如BooleanTypeHandler
完成JAVA类型java.lang.Boolean
和boolean
到JDBC类型BOOLEAN
的转换,LocalDateTimeTypeHandler
完成JAVA类型java.time.LocalDateTime
到JDBC类型TIMESTAMP
的转换,StringTypeHandler
完成JAVA类型java.lang.String
到JDBC类型CHAR
和VARCHAR
的转换,全量的内置TypeHandler
可以参考官方文档, 这里不再列举。正是由于MyBatis提供了大量的内置TypeHandler
,所以通常情况下使用MyBatis时不用关心JAVA类型与JDBC类型的转换,但是也会有一些场景,MyBatis内置的TypeHandler
是无法满足需求的,例如JAVA对象有一个List
字段,而这个字段在数据库中对应的JDBC类型是VARCHAR
,那么此时传统的做法就是需要在程序中做大量的List
到String
以及String
到List
的转换,且代码不易复用,此时就可以通过自定义TypeHandler
来解决这种问题。
本篇文章将先从0到1搭建示例工程,并演示如何自定义TypeHandler
来解决List
到String
的转换问题,然后会介绍TypeHandler
的相关概念和配置,以及生效条件。
MyBatis
版本:3.5.6
正文
一. 示例工程搭建
首先创建示例表,DDL语句如下。
CREATE TABLE student(
id INT(11) PRIMARY KEY AUTO_INCREMENT,
stu_name VARCHAR(255) NOT NULL,
stu_age INT(11) NOT NULL,
stu_num VARCHAR(255) NOT NULL,
stu_intention VARCHAR(255) NOT NULL
)
student表主要存储一名学生的基本信息,包括:姓名,年龄,学号和意向学校。意向学校在数据库中是以VARCHAR
类型存储,例如清华,北大,复旦这样的形式。
然后创建MAVEN工程,pom文件如下所示。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.lee.learn.mybatis</groupId>
<artifactId>learn-mybatis-typehandler</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!-- MyBatis依赖 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.16</version>
</dependency>
<!-- 单元测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
<!-- Lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8.1</version>
</dependency>
</dependencies>
<!-- 映射接口和映射文件放在一起时需要如下配置 -->
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
<filtering>false</filtering>
</resource>
</resources>
</build>
</project>
MyBatis配置文件mybatis-config.xml如下所示。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 配置日志打印 -->
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
<!-- 配置环境 -->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://192.168.101.7:3306/test?characterEncoding=utf-8&serverTimezone=UTC&useSSL=false"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<!-- 配置映射接口 -->
<mappers>
<package name="com.lee.learn.mybatis.dao"/>
</mappers>
</configuration>
映射接口(Mapper
接口)和映射文件(XML文件)放在com.lee.learn.mybatis.dao
路径下,映射接口StudentMapper
如下所示。
public interface StudentMapper {
/**
* 添加一个学生。
*
* @param studentName 学生名。
* @param studentAge 学生年龄。
* @param studentNum 学生唯一标识串。
* @param studentIntention 学生意向学校。
*/
void addStudent(@Param("studentName") String studentName,
@Param("studentAge") int studentAge,
@Param("studentNum") String studentNum,
@Param("studentIntention") List<String> studentIntention);
/**
* 查询所有学生。
*
* @return {@link Student}的集合。
*/
List<Student> queryAllStudents();
}
映射文件StudentMapper.xml如下所示。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.lee.learn.mybatis.dao.StudentMapper">
<resultMap id="studentResultMap" type="com.lee.learn.mybatis.entity.Student">
<id property="id" column="id"/>
<result property="studentName" column="stu_name"/>
<result property="studentAge" column="stu_age"/>
<result property="studentNum" column="stu_num"/>
<result property="intentions" column="stu_intention"/>
</resultMap>
<insert id="addStudent">
INSERT INTO student (stu_name, stu_age, stu_num, stu_intention)
VALUES (#{studentName}, #{studentAge}, #{studentNum}, #{studentIntention})
</insert>
<select id="queryAllStudents" resultMap="studentResultMap">
SELECT
id,
stu_name,
stu_num,
stu_intention
FROM student
</select>
</mapper>
与数据库记录做映射的实体对象Student
放在com.lee.learn.mybatis.entity.Student
路径下,且其结构如下。
@Getter
@Setter
@ToString
public class Student {
private int id;
private String studentName;
private int studentAge;
private String studentNum;
private List<String> intentions;
}
学生实体对象Student
的intentions字段是一个List
类型,这里就存在一个List
到VARCHAR
的相互转换问题。
最后是单元测试程序,MybatisTest
如下所示。
public class MybatisTest {
private SqlSession sqlSession;
@Before
public void setUp() throws Exception {
String resource = "mybatis-config.xml";
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder()
.build(Resources.getResourceAsStream(resource));
sqlSession = sqlSessionFactory.openSession(false);
}
@After
public void tearDown() {
sqlSession.close();
}
@Test
public void 添加一个学生() {
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
studentMapper.addStudent("Lee", 20,
"0001", Arrays.asList("清华", "北大", "复旦"));
sqlSession.commit();
}
@Test
public void 查询所有学生() {
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
List<Student> students = studentMapper.queryAllStudents();
students.forEach(System.out::println);
}
}
示例工程的目录结构如下所示。
二. 问题演示与解决
执行上述示例工程的添加一个学生测试案例,会执行失败,且打印的错误日志中会有如下的提示。
java.lang.IllegalStateException: No typehandler found for property intentions
因为Student#intentions
字段是List<String>
类型,而Student#intentions
字段对应数据库中的类型是VARCHAR
,同时MyBatis没有内置的List
转换String
的TypeHandler
以及示例工程的业务代码中也没有去做转换,最终导致报错。
下面演示如何自定义一个TypeHandler
来解决上述问题。
自定义用于完成List
到String
相互转换的类型处理器ListStringTypeHandler
放在com.lee.learn.mybatis.typehandler
路径下,且其实现如下。
@MappedJdbcTypes({JdbcType.VARCHAR, JdbcType.CHAR})
public class ListStringTypeHandler extends BaseTypeHandler<List<String>> {
private final String seq = ",";
@Override
public void setNonNullParameter(PreparedStatement ps, int i, List<String> parameter,
JdbcType jdbcType) throws SQLException {
ps.setString(i, listToString(parameter));
}
@Override
public List<String> getNullableResult(ResultSet rs, String columnName) throws SQLException {
return StringToList(rs.getString(columnName));
}
@Override
public List<String> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return StringToList(rs.getString(columnIndex));
}
@Override
public List<String> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return StringToList(cs.getString(columnIndex));
}
private String listToString(List<String> parameter) throws SQLException {
if (parameter == null || parameter.size() == 0) {
throw new SQLException();
}
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < parameter.size(); i++) {
stringBuilder.append(parameter.get(i));
if (i != parameter.size() - 1) {
stringBuilder.append(seq);
}
}
return stringBuilder.toString();
}
private List<String> StringToList(String paramStr) {
if (StringUtils.isEmpty(paramStr)) {
return new ArrayList<>();
}
String[] params = paramStr.split(seq);
return new ArrayList<>(Arrays.asList(params));
}
}
在MyBatis配置文件中将自定义的TypeHandler
进行注册,mybatis-config.xml修改如下所示。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 配置日志打印 -->
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
<!-- 配置自定义TypeHandler -->
<typeHandlers>
<typeHandler handler="com.lee.learn.mybatis.typehandler.ListStringTypeHandler"/>
</typeHandlers>
<!-- 配置环境 -->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://192.168.101.7:3306/test?characterEncoding=utf-8&serverTimezone=UTC&useSSL=false"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<!-- 配置映射接口 -->
<mappers>
<package name="com.lee.learn.mybatis.dao"/>
</mappers>
</configuration>
光完成注册还无法让ListStringTypeHandler
生效,还需要在映射文件中的<resultMap>
和<insert>
标签中引用注册好的ListStringTypeHandler
。StudentMapper.xml文件修改如下。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.lee.learn.mybatis.dao.StudentMapper">
<resultMap id="studentResultMap" type="com.lee.learn.mybatis.entity.Student">
<id property="id" column="id"/>
<result property="studentName" column="stu_name"/>
<result property="studentAge" column="stu_age"/>
<result property="studentNum" column="stu_num"/>
<result property="intentions" column="stu_intention"
typeHandler="com.lee.learn.mybatis.typehandler.ListStringTypeHandler"/>
</resultMap>
<insert id="addStudent">
INSERT INTO student (stu_name, stu_age, stu_num, stu_intention)
VALUES (#{studentName}, #{studentAge}, #{studentNum}, #{studentIntention, typeHandler=com.lee.learn.mybatis.typehandler.ListStringTypeHandler})
</insert>
<select id="queryAllStudents" resultMap="studentResultMap">
SELECT
id,
stu_name,
stu_num,
stu_intention
FROM student
</select>
</mapper>
修改点如下。
-
<resultMap>
标签下的<result>
标签中使用了typeHandler属性来指定使用ListStringTypeHandler
; -
<insert>
标签里的第四个#{}
占位符中使用了typeHandler属性来指定使用ListStringTypeHandler
。
上述都是显示的使用TypeHandler
(还有隐式的使用,后面再讲)。运行添加一个学生测试用例,可以执行成功。此时运行查询所有学生测试用例,日志打印如下。
可见自定义的ListStringTypeHandler
在设置预处理语句(PreparedStatement
)中的参数或将查询记录与JAVA实体对象做映射时,完成了类型转换功能。
三. 如何自定义TypeHandler
类型处理器(TypeHandler
)通常用于MyBatis在设置预处理语句(PreparedStatement
)中的参数时,完成JAVA类型到JDBC类型的转换(通常用在#{}
参数占位符中),以及在将查询到的结果记录映射到JAVA实体对象时,完成JDBC类型到JAVA类型的转换(通常用在<result>
标签中)。
MyBatis提供了大量内置的TypeHandler
作为默认类型处理器,用于基本数据类型的转换,而对于非标准的数据类型的转换,可以通过实现org.apache.ibatis.type.TypeHandler
接口,或继承org.apache.ibatis.type.BaseTypeHandler
。先看一下TypeHandler
接口。
public interface TypeHandler<T> {
// 用于将JAVA类型参数设置到预处理语句PreparedStatement中(JavaType -> JdbcType)
void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;
// 用于将查询结果集ResultSet的columnName列的数据转换成JAVA类型(JdbcType -> JavaType)
T getResult(ResultSet rs, String columnName) throws SQLException;
// 用于将查询结果集ResultSet的第columnIndex列的数据转换成JAVA类型(JdbcType -> JavaType)
T getResult(ResultSet rs, int columnIndex) throws SQLException;
// 用于将存储过程CallableStatement的第columnIndex列的数据转换成JAVA类型(JdbcType -> JavaType)
T getResult(CallableStatement cs, int columnIndex) throws SQLException;
}
即TypeHandler
接口定义了setParameter()
方法用于在将JAVA类型参数设置到预处理语句PreparedStatement
中时完成JavaType到JdbcType的转换,也定义了getResult()
方法用于在将查询结果映射到JAVA实体对象时完成JdbcType到JavaType的转换。而BaseTypeHandler
是相比于TypeHandler
更易用的实现类型处理器的抽象类,BaseTypeHandler
实现了TypeHandler
接口并会在实现的四个方法中分别调用BaseTypeHandler
定义的四个抽象方法,并对调用做了异常捕获与处理,如下所示。
@Override
public T getResult(ResultSet rs, String columnName) throws SQLException {
try {
return getNullableResult(rs, columnName);
} catch (Exception e) {
throw new ResultMapException("Error attempting to get column '" + columnName + "' from result set. Cause: " + e, e);
}
}
public abstract T getNullableResult(ResultSet rs, String columnName) throws SQLException;
更推荐通过继承BaseTypeHandler
的方式来自定义类型处理器。
下面是示例工程中的自定义类型处理器,如下所示。
@MappedJdbcTypes({JdbcType.VARCHAR, JdbcType.CHAR})
public class ListStringTypeHandler extends BaseTypeHandler<List<String>> {
private final String seq = ",";
@Override
public void setNonNullParameter(PreparedStatement ps, int i, List<String> parameter,
JdbcType jdbcType) throws SQLException {
ps.setString(i, listToString(parameter));
}
@Override
public List<String> getNullableResult(ResultSet rs, String columnName) throws SQLException {
return StringToList(rs.getString(columnName));
}
@Override
public List<String> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return StringToList(rs.getString(columnIndex));
}
@Override
public List<String> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return StringToList(cs.getString(columnIndex));
}
private String listToString(List<String> parameter) throws SQLException {
if (parameter == null || parameter.size() == 0) {
throw new SQLException();
}
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < parameter.size(); i++) {
stringBuilder.append(parameter.get(i));
if (i != parameter.size() - 1) {
stringBuilder.append(seq);
}
}
return stringBuilder.toString();
}
private List<String> StringToList(String paramStr) {
if (StringUtils.isEmpty(paramStr)) {
return new ArrayList<>();
}
String[] params = paramStr.split(seq);
return new ArrayList<>(Arrays.asList(params));
}
}
上述自定义类型处理器,支持的JAVA类型是List
,支持的JDBC类型是VARCHAR
和CHAR
。如果要决定一个类型处理器支持哪些JAVA类型,有如下途径。
- 类型处理器的泛型可以决定类型处理器支持的JavaType;
- 在类型处理器上使用注解
@MappedTypes
来指定,例如@MappedTypes({List.class})
; - 在配置文件中注册类型处理器时,通过
<typeHandler>
标签的javaType属性来指定,例如<typeHandler handler="com.lee.learn.mybatis.typehandler.ListStringTypeHandler" javaType="List"/>
。
优先级是1到3依次增加。
如果要决定一个类型处理器支持哪些JDBC类型,有如下途径。
- 在类型处理器上使用注解
@MappedJdbcTypes
来指定,例如@MappedJdbcTypes({JdbcType.VARCHAR, JdbcType.CHAR})
; - 在配置文件中注册类型处理器时,通过
<typeHandler>
标签的jdbcType属性来指定(注意:同时也需要设置了javaType属性,否则jdbcType属性不生效),
例如<typeHandler handler="com.lee.learn.mybatis.typehandler.ListStringTypeHandler" javaType="List" jdbcType="VARCHAR"/>
。
优先级是1到2依次增加。
四. TypeHandler如何生效
通常,TypeHandler
的使用场景有两个。
- 在设置预处理语句(
PreparedStatement
)中的参数时,完成JAVA类型到JDBC类型的转换,通常就是INSERT和UPDATE的场景; - 在将查询到的结果记录映射到JAVA实体对象时,完成JDBC类型到JAVA类型的转换,通常就是会使用到
<resultMap>
的场景。
在使用场景下,如何让我们自定义的TypeHandler
生效,如下直接给出结论,再做验证。
- 显式使用。示例中就是显示使用,即在
<result>
标签中和#{}
占位符中使用typeHandler属性来指定使用的类型处理器,这种方式是最简单粗暴的,就算不在配置文件中注册类型处理器,就算没有为类型处理器配置任何支持的JDBC类型,只要在<result>
标签中和#{}
占位符中使用了typeHandler属性来指定要使用的类型处理器,那么MyBatis就会使用这个类型处理器; - 隐式使用。通常,我们是不会关注到
TypeHandler
的,然而大部分时候JAVA类型到JDBC类型的相互转换都能成功完成,是因为MyBatis会隐式使用其内置的TypeHandler
,而隐式使用哪个内置TypeHandler
,是通过<result>
标签和#{}
占位符的JavaType和JdbcType进行推断的。
显式使用没什么好说的,最为简单明了。下面重点说一下隐式使用。
首先能够被隐式使用的TypeHandler
,都需要完成注册,自定义的TypeHandler
可以在配置文件中通过<typeHandler>
标签注册,而内置的TypeHandler
是在org.apache.ibatis.type.TypeHandlerRegistry#TypeHandlerRegistry(org.apache.ibatis.session.Configuration)
方法完成的注册,这个方法有点长,这里不再展示。
然后每一个TypeHandler
都有其支持的JAVA类型,以及可能支持的JDBC类型(也可能没有),TypeHandler
注册到MyBatis中后,是按照如下形式存储的。
// Map[JavaType, Map[JdbcType, TypeHandler]]
private final Map<Type, Map<JdbcType, TypeHandler<?>>> typeHandlerMap = new ConcurrentHashMap<>();
下面得跟一下org.apache.ibatis.type.TypeHandlerRegistry#register(java.lang.Class<T>, org.apache.ibatis.type.TypeHandler<? extends T>)
方法,才能更好的理解TypeHandler
是如何在MyBatis中被存放以及如果TypeHandler
支持的JDBC类型为空时这个TypeHandler
如何被隐式使用。方法源码如下所示。
public <T> void register(Class<T> javaType, TypeHandler<? extends T> typeHandler) {
register((Type) javaType, typeHandler);
}
会继续调用到org.apache.ibatis.type.TypeHandlerRegistry#register(java.lang.reflect.Type, org.apache.ibatis.type.TypeHandler<? extends T>)
方法,如下所示。
private <T> void register(Type javaType, TypeHandler<? extends T> typeHandler) {
MappedJdbcTypes mappedJdbcTypes = typeHandler.getClass().getAnnotation(MappedJdbcTypes.class);
if (mappedJdbcTypes != null) {
// 如果配置了@MappedJdbcTypes注解且通过注解配置了支持的JDBC类型
// 则按照Map[JavaType, Map[JdbcType, TypeHandler]]的结构来存放TypeHandler
// 同一个TypeHandler可能会在Map中存多份,因为@MappedJdbcTypes注解可以配置多个JDBC类型
for (JdbcType handledJdbcType : mappedJdbcTypes.value()) {
register(javaType, handledJdbcType, typeHandler);
}
// 如果配置了@MappedJdbcTypes注解且将includeNullJdbcType属性设置为了true
// 则再将TypeHandler与一个null的JDBC类型做关联
if (mappedJdbcTypes.includeNullJdbcType()) {
register(javaType, null, typeHandler);
}
} else {
// 因为没有支持的JDBC类型
// 所以将TypeHandler与一个null的JDBC类型做关联
register(javaType, null, typeHandler);
}
}
所以就算没有为一个TypeHandler
设置支持的JDBC类型,但是这个TypeHandler
一样会被存放在MyBatis中。不过请注意,如果有两个及以上的TypeHandler
支持的JAVA类型一样,支持的JDBC类型也一样(包括null),那么后注册的TypeHandler
会覆盖先注册的TypeHandler
(自定义的TypeHandler
注册顺序是按照配置文件里的先后顺序来依次加载的,先配置的先加载,后配置的后加载)。
现在已经知道了注册到MyBatis中的TypeHandler
是按照Map[JavaType, Map[JdbcType, TypeHandler]]
这样的形式存放在MyBatis的org.apache.ibatis.type.TypeHandlerRegistry#typeHandlerMap
中。同时,MyBatis在解析映射文件时,每一个<result>
标签会被解析为一个ResultMapping
对象,每一个#{}
参数占位符会被解析为一个ParameterMapping
对象,在这个过程中也会将需要使用的TypeHandler
绑定到ResultMapping
或ParameterMapping
上。那么再看示例工程中的如下一个<result>
标签。
<result column="stu_intention" property="intentions"/>
MyBatis在解析这个标签时,会将这个标签解析并构造为一个ResultMapping
,具体可看org.apache.ibatis.mapping.ResultMapping.Builder#build
方法,如下所示。
public ResultMapping build() {
resultMapping.flags = Collections.unmodifiableList(resultMapping.flags);
resultMapping.composites = Collections.unmodifiableList(resultMapping.composites);
// 为ResultMapping推断出应该使用的TypeHandler
resolveTypeHandler();
validate();
return resultMapping;
}
继续看org.apache.ibatis.mapping.ResultMapping.Builder#resolveTypeHandler
方法,如下所示。
private void resolveTypeHandler() {
// <result>标签就算没有显式的配置JavaType,JavaType也是已知的,不会为null
// <result>标签如果没有显式的配置JdbcType,则JdbcType是未知的,为null
if (resultMapping.typeHandler == null && resultMapping.javaType != null) {
Configuration configuration = resultMapping.configuration;
TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
// 根据从JavaType和JdbcType从TypeHandlerRegistry中拿TypeHandler
resultMapping.typeHandler = typeHandlerRegistry.getTypeHandler(resultMapping.javaType, resultMapping.jdbcType);
}
}
上述方法表明ResultMapping
使用的TypeHandler
是根据<result>
标签对应的JavaType和JdbcType去TypeHandlerRegistry
中匹配。通常,如果不为<result>
标签指定JavaType和JdbcType,则JavaType是已知的,因为可以通过映射对象的字段类型做推断(示例工程中可以根据映射对象Student
的intentions字段类型做推断),但是JdbcType是未知的(因为MyBatis是不知道数据库的元信息的也不会去检测数据库元信息),那么对于上述示例工程中的<result>
标签,会以JavaType=List, JdbcType=null去TypeHandlerRegistry
中匹配TypeHandler
。继续看org.apache.ibatis.type.TypeHandlerRegistry#getTypeHandler(java.lang.Class<T>, org.apache.ibatis.type.JdbcType)
的实现,如下所示。
public <T> TypeHandler<T> getTypeHandler(Class<T> type, JdbcType jdbcType) {
return getTypeHandler((Type) type, jdbcType);
}
private <T> TypeHandler<T> getTypeHandler(Type type, JdbcType jdbcType) {
if (ParamMap.class.equals(type)) {
return null;
}
// 先根据JavaType得到这个JavaType对应的Map[JdbcType, TypeHandler]
Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = getJdbcHandlerMap(type);
TypeHandler<?> handler = null;
if (jdbcHandlerMap != null) {
// 根据JdbcType拿到TypeHandler
handler = jdbcHandlerMap.get(jdbcType);
if (handler == null) {
// 根据JdbcType拿不到TypeHandler
// 则再以JdbcType为null去拿TypeHandler
handler = jdbcHandlerMap.get(null);
}
if (handler == null) {
// 如果根据JdbcType和null都拿不到TypeHandler
// 则调用pickSoleHandler()方法拿TypeHandler
handler = pickSoleHandler(jdbcHandlerMap);
}
}
return (TypeHandler<T>) handler;
}
private TypeHandler<?> pickSoleHandler(Map<JdbcType, TypeHandler<?>> jdbcHandlerMap) {
TypeHandler<?> soleHandler = null;
// 遍历JavaType对应的所有TypeHandler
// 所有TypeHandler都是同一个TypeHandler时,就返回这个TypeHandler
// 只要出现两种以上的TypeHandler,则返回null
for (TypeHandler<?> handler : jdbcHandlerMap.values()) {
if (soleHandler == null) {
soleHandler = handler;
} else if (!handler.getClass().equals(soleHandler.getClass())) {
return null;
}
}
return soleHandler;
}
也就是会先以JavaType来拿到这个JavaType对应的Map[JdbcType, TypeHandler]
,然后再以JdbcType去Map[JdbcType, TypeHandler]
中匹配TypeHandler
,如果没有匹配的TypeHandler
,则再以null去Map[JdbcType, TypeHandler]
中匹配TypeHandler
(这里主要是为了使配置了@MappedJdbcTypes
注解且将includeNullJdbcType属性设置为true的TypeHandler
能够被匹配到),如果还是没匹配到TypeHandler
,则调用pickSoleHandler()
方法判断当前JavaType是不是只有一个对应的TypeHandler
,如果是,则返回这个TypeHandler
,如果不是,则返回null(这是MyBatis在3.4.0版本做的优化,以支持某个JAVA类型只有一个注册的类型处理器时,即使没有设置@MappedJdbcTypes
注解的includeNullJdbcType属性为true,这个类型处理器也能被选择用于处理这个JAVA类型)。
这里直接对隐式使用TypeHandler
做一个小结,不关心源码的可以直接看这里。
- 每一个
TypeHandler
都有其支持的JAVA类型,以及可能支持的JDBC类型(也可能没有),并且在MyBatis中以Map[JavaType, Map[JdbcType, TypeHandler]]
的形式存放; - 如果有多个
TypeHandler
的支持的JAVA类型和JDBC类型都一样,则后注册的TypeHandler
会覆盖先注册的TypeHandler
; - 如果在MyBatis的参数占位符
#{}
或者结果映射标签<result>
中通过javaType属性指定了JavaType,则MyBatis在推断使用哪种TypeHandler
时依据的JavaType会使用javaType属性的值,否则,如果是<result>
的话则MyBatis能根据映射对象推断出JavaType,如果是#{}
的话则JavaType为Object
; - 如果在MyBatis的参数占位符
#{}
或者结果映射标签<result>
中通过jdbcType属性指定了JdbcType,则MyBatis在推断使用哪种TypeHandler
时依据的JdbcType会使用jdbcType属性的值,否则依据的JdbcType会为null; - MyBatis在推断使用哪个
TypeHandler
时,会先使用JavaType拿到JavaType对应的Map[JdbcType, TypeHandler]
,然后使用JdbcType去匹配TypeHandler
,匹配不到则再使用JdbcType=null去匹配TypeHandler
,如果还匹配不到,则判断JavaType对应的TypeHandler
是否有多个,如果是多个则返回null表示匹配失败,如果只有一个则使用这个TypeHandler
。
下面是几个例子,加深理解。
例子1
自定义的TypeHandler
,<result>
标签和参数占位符#{}
如下所示。
@MappedJdbcTypes({JdbcType.VARCHAR, JdbcType.CHAR})
public class ListStringTypeHandler extends BaseTypeHandler<List<String>>
<result column="stu_intention" property="intentions"/>
#{studentIntention}
<result>
能使用到ListStringTypeHandler
,#{}
使用不到ListStringTypeHandler
。
例子2
自定义的TypeHandler
,<result>
标签和参数占位符#{}
如下所示。
@MappedJdbcTypes({JdbcType.VARCHAR, JdbcType.CHAR})
public class ListStringTypeHandler extends BaseTypeHandler<List<String>>
<result column="stu_intention" property="intentions" jdbcType="DATE"/>
#{studentIntention, javaType=List}
<result>
能使用到ListStringTypeHandler
,#{}
也能使用到ListStringTypeHandler
。
例子3
自定义的TypeHandler
,<result>
标签和参数占位符#{}
如下所示。
@MappedJdbcTypes({JdbcType.VARCHAR, JdbcType.CHAR})
public class ListStringTypeHandler extends BaseTypeHandler<List<String>>
@MappedJdbcTypes({JdbcType.DATE})
public class AnotherListStringTypeHandler extends BaseTypeHandler<List<String>>
<result column="stu_intention" property="intentions"/>
#{studentIntention, javaType=List}
<result>
和#{}
都使用不到ListStringTypeHandler
和AnotherListStringTypeHandler
。
例子4
自定义的TypeHandler
,<result>
标签和参数占位符#{}
如下所示。
@MappedJdbcTypes({JdbcType.VARCHAR, JdbcType.CHAR})
public class ListStringTypeHandler extends BaseTypeHandler<List<String>>
@MappedJdbcTypes({JdbcType.DATE})
public class AnotherListStringTypeHandler extends BaseTypeHandler<List<String>>
<result column="stu_intention" property="intentions" jdbcType="VARCHAR"/>
#{studentIntention, javaType=List, jdbcType=CHAR}
<result>
和#{}
都能使用到ListStringTypeHandler
。
最后建议自定义的TypeHandler
都要为其指定支持的JavaType和JdbcType,以及在<result>
标签和#{}
占位符中都把javaType和jdbcType属性配置上,这样MyBatis能够快速无误的帮我们推断出应该使用哪个类型处理器。
五. 小补充
这里再对为参数占位符#{}
推断类型处理器时的一些逻辑进行补充说明,不看也不影响对本篇文章的理解。
为参数占位符#{}
推断类型处理器时,如果没有通过javaType来指定JAVA类型,那么MyBatis是无法知道JAVA类型是什么的(而<result>
标签是可以的,这是不同点),此时MyBatis会默认JAVA类型是Object
,然后通过Object
这个JavaType拿到一个org.apache.ibatis.type.UnknownTypeHandler
内置类型处理器,下面看一下UnknownTypeHandler
的setNonNullParameter()
方法。
@Override
public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType)
throws SQLException {
TypeHandler handler = resolveTypeHandler(parameter, jdbcType);
handler.setParameter(ps, i, parameter, jdbcType);
}
private TypeHandler<?> resolveTypeHandler(Object parameter, JdbcType jdbcType) {
TypeHandler<?> handler;
if (parameter == null) {
handler = OBJECT_TYPE_HANDLER;
} else {
// 根据需要设置到PreparedStatement中的参数判断出JAVA类型
// 然后再调用到org.apache.ibatis.type.TypeHandlerRegistry#getTypeHandler(java.lang.Class<T>, JdbcType)拿TypeHandler
handler = typeHandlerRegistrySupplier.get().getTypeHandler(parameter.getClass(), jdbcType);
if (handler == null || handler instanceof UnknownTypeHandler) {
handler = OBJECT_TYPE_HANDLER;
}
}
return handler;
}
已知setNonNullParameter()
方法是会在实际执行SQL语句前被调用到,此时会完成PreparedStatement
的参数设置,因此这时能够拿到实际设置到PreparedStatement
中的参数值从而得到参数的JavaType,所以这时会再尝试基于JavaType和JdbcType去匹配TypeHandler
。
所以本质上就算没有通过javaType指定JavaType,<result>
标签和#{}
参数占位符都是能够拿到JavaType,只不过<result>
标签在构建ResultMapping
时就能够拿到JavaType,而#{}
参数占位符需要在SQL语句实际执行前为PreparedStatement
设置参数时才能够拿到JavaType。
那么按照本节的结论,为什么第四节最后的例子1中的#{}
使用不到ListStringTypeHandler
呢,这是因为在为PreparedStatement
设置参数时,studentIntention这个参数的实际类型是ArrayList
,而不是List
,但在MyBatis中,认为ListStringTypeHandler
是支持List
而不是ArrayList
的。
总结
本篇文章第三节和第四节的结论加起来就是本篇文章的总结,这里不再重复归纳。
大家好,我是Dog Lee 😁😁 一名金融科技领域的JAVA系统研发😊😊
我希望将自己工作和学习中的经验以最朴实,最严谨的方式分享给大家,共同进步👉💓👈
《MyBatis源码与实战》专栏,会陆续更新关于MyBatis的源码讲解,实战使用等内容
👉👉👉👉👉👉👉👉💓写作不易,期待大家的关注和点赞💓👈👈👈👈👈👈👈👈