MyBatis自定义TypeHandler MyBatis自

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

MyBatis自定义TypeHandler

1 什么是TypeHandler

TypeHandler根据字面意思即为类型处理器

引用官方文档的描述: MyBatis在设置预处理语句(PreparedStatement)中的参数或从结果集中取出一个值时, 都会用类型处理器将获取到的值以合适的方式转换成 Java 类型

MyBatis存在一些默认的类型处理器, 可参考官方文档

2 为什么要使用TypeHandler

在开发过程中, 当默认的TypeHandler无法满足需求时, 例如遇到MyBatis不支持的数据类型需要特殊处理的类型转换, 便需要自己定制对应的TypeHandler

笔者会在下面的代码实现中完成如下几种情况的TypeHandler:

  1. 逗号分隔保存在数据库中的数据, 在对应的Java类中为数组
  2. 自定义枚举

3 如何自定义TypeHandler

MyBatis提供了接口org.apache.ibatis.type.TypeHandler和类org.apache.ibatis.type.BaseTypeHandler

官方文档给出的示例为继承BaseTypeHandler, 笔者在这里也使用这种方式

先来观察一下官方的StringTypeHandler:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
java复制代码public class StringTypeHandler extends BaseTypeHandler<String> {

 @Override
 public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType)
     throws SQLException {
   ps.setString(i, parameter);
}

 @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);
}
   
}

方法名称和代码都简洁明了, 观察可知, 只需要完成四个方法的覆盖, 即可实现自定义TypeHandler

3.1 逗号分隔字符串转数组

假设用户表t_user设计如下:

id username tags
1 admin admin, user

对应的Java类为:

1
2
3
4
5
6
7
8
java复制代码@Data
@NoArgsConstructor
@SuperBuilder(toBuilder = true)
public class User {
   private String id;
   private String username;
   private String[] tags;
}

tags属性在数据库中用逗号分隔的字符串保存, 但User类对应的属性为String数组

可以创建StringArrayTypeHandler来解决类型转换的问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
java复制代码public class StringArrayTypeHandler extends BaseTypeHandler<String[]> {

 @Override
 public void setNonNullParameter(PreparedStatement preparedStatement, int i, String[] strings, JdbcType jdbcType)
   throws SQLException {
   preparedStatement.setString(i, StringUtils.join(strings, ","));
}

 @Override
 public String[] getNullableResult(ResultSet resultSet, String s) throws SQLException {
   return convert(resultSet.getString(s));
}

 @Override
 public String[] getNullableResult(ResultSet resultSet, int i) throws SQLException {
   return convert(resultSet.getString(i));
}

 @Override
 public String[] getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
   return convert(callableStatement.getString(i));
}

 /**
  * 将查询值转换为数组
  *
  * @param value 查询值, String
  * @return 转换结果, String[]
  */
 private String[] convert(String value) {
   return StringUtils.isEmpty(value) ? new String[0] : value.split(",");
}

}

3.2 自定义枚举

如何创建包含中文名称的枚举, 可以参考MyBatis中使用Java类与枚举

先创建工具类用于根据code获取枚举实体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public class ValueNameEnumUtils {

 private ValueNameEnumUtils() {
}

 public static <E extends ValueNameEnum> E valueOf(Class<E> enumClass, int value) {
     E[] enumConstants = enumClass.getEnumConstants();
     for (E e : enumConstants) {
       if (e.getValue() == value) {
         return e;
      }
    }
     return null;
}
 
}

和3.1中的情况不同, 枚举的具体类型是不确定, 所以我们要使用泛型的方式处理TypeHandler

创建ValueNameEnumTypeHandler:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public class ValueNameEnumTypeHandler<E extends ValueNameEnum> extends BaseTypeHandler<ValueNameEnum> {

 private final Class<E> type;

 public ValueNameEnumTypeHandler(Class<E> type) {
   if (type == null) {
     throw new IllegalArgumentException("Type argument cannot be null");
  }
   this.type = type;
}
 
}

泛型虽然名之为, 但在编译过程中实际会发生类型擦除

关于类型擦除可以阅读面试官:说说什么是泛型的类型擦除?

总之, 对于泛型TypeHandler, 我们需要声明一个用来标识具体类型的属性private final Class<E> type和创建对应的构造函数public ValueNameEnumTypeHandler(Class<E> type)

接下来和3.1中的一致, 重写四个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
java复制代码public class ValueNameEnumTypeHandler<E extends ValueNameEnum> extends BaseTypeHandler<ValueNameEnum> {

 private final Class<E> type;

 public ValueNameEnumTypeHandler(Class<E> type) {
   if (type == null) {
     throw new IllegalArgumentException("Type argument cannot be null");
  }
   this.type = type;
}

 @Override
 public void setNonNullParameter(PreparedStatement ps, int i, ValueNameEnum parameter, JdbcType jdbcType)
   throws SQLException {
   ps.setInt(i, parameter.getValue());
}

 @Override
 public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
   int code = rs.getInt(columnName);
   return rs.wasNull() ? null : valueOf(code);
}

 @Override
 public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
   int code = rs.getInt(columnIndex);
   return rs.wasNull() ? null : valueOf(code);
}

 @Override
 public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
   int code = cs.getInt(columnIndex);
   return cs.wasNull() ? null : valueOf(code);
}

 /**
  * 根据枚举值返回枚举示例
  *
  * @param code 枚举值
  * @return 枚举实例
  */
 private E valueOf(int code) {
   try {
     return ValueNameEnumUtils.valueOf(type, code);
  } catch (Exception ex) {
     throw new IllegalArgumentException(
       "Cannot convert " + code + " to " + type.getSimpleName() + " by code value.", ex);
  }
}
   
}

完成上述代码直接启动, 会抛出异常: Unable to find a usable constructor for class cn.houtaroy.springboot.common.mybatis.handler.ValueNameEnumTypeHandler

产生异常的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码public <T> TypeHandler<T> getInstance(Class<?> javaTypeClass, Class<?> typeHandlerClass) {
   // 未指定JavaType, 此处为false
   if (javaTypeClass != null) {
     try {
       Constructor<?> c = typeHandlerClass.getConstructor(Class.class);
       return (TypeHandler<T>) c.newInstance(javaTypeClass);
    } catch (NoSuchMethodException ignored) {
       // ignored
    } catch (Exception e) {
       throw new TypeException("Failed invoking constructor for handler " + typeHandlerClass, e);
    }
  }
   try {
     // 此处抛出异常
     Constructor<?> c = typeHandlerClass.getConstructor();
     return (TypeHandler<T>) c.newInstance();
  } catch (Exception e) {
     throw new TypeException("Unable to find a usable constructor for " + typeHandlerClass, e);
  }
}

报错的原因直白, 没有找到ValueNameEnumTypeHandler的构造函数

首先我们要了解下Java类构造函数的机制: 如果定义了构造函数, 则使用定义, 否则默认生成空构造函数

在3.1中的StringArrayTypeHandler, 我们没有定义构造函数, 自动生成空构造函数, typeHandlerClass.getConstructor()不会抛出异常

ValueNameEnumTypeHandler定义了一个构造函数ValueNameEnumTypeHandler(Class<E> type), 且没有指定JavaType, typeHandlerClass.getConstructor()自然抛出异常

解决方法有两种:

  1. 创造空的构造函数
  2. 指定JavaType

笔者推荐第二种, 因为第一种方式枚举类属性type会产生NPE(空指针异常), MyBatis官方也我们提供了注解@MappedTypes用于指定JavaType:

1
2
3
4
java复制代码@MappedTypes(ValueNameEnum.class)
public class ValueNameEnumTypeHandler<E extends ValueNameEnum> extends BaseTypeHandler<ValueNameEnum> {
   //...
}

4 如何使用TypeHandler

在上一章节中, 我们完成了编码实现自定义TypeHandler, 但完成的TypeHandler还没办法进行使用, 需要手动进行配置

有两种配置方式: 局部使用和全局使用

StringArrayTypeHandler举例:

4.1 局部使用

在ResultMap中使用:

1
2
3
4
xml复制代码<resultMap id="UserResultMap" type="cn.houtaroy.springboot.common.system.model.User">
   <id column="id" property="id"/>
<result column="tags" property="tags" typeHandler="cn.houtaroy.springboot.extension.mybatis.handler.StringArrayTypeHandler"/>
</resultMap>

在语句中使用:

1
sql复制代码update t_user set tags = #{tags, typeHandler=cn.houtaroy.springboot.extension.mybatis.handler.StringArrayTypeHandler}

4.2 全局使用

使用配置文件指定handler包名:

1
2
yaml复制代码mybatis:
type-handlers-package: cn.houtaroy.springboot.extension.mybatis.handler

注意, 此配置类型为String, 只能配置一个包, 推荐使用下面的方式

手写配置类:

1
2
3
4
5
6
7
8
9
java复制代码@Configuration
public class MybatisTypeHandlerConfiguration {
 
 @Bean
 ConfigurationCustomizer typeHandlerRegistry() {
   return configuration -> configuration.getTypeHandlerRegistry().register(ValueNameEnumTypeHandler.class);
}
 
}

StringArrayTypeHandler不适合全局配置, 它会在全部JavaType为String[]的属性上使用

5 拓展阅读

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%