MyBatis TypeHandler学习及实战

MyBatis在金融领域的使用非常广泛,作为一款优秀的ORM框架,其独特的优势在于:

①对于sql的分离,使业务开发同事更专注于逻辑实现;

②MyBatis对存储过程的有很好的支持(特别是对一些时间延时要求高,业务较为稳定的场景)。

作为一款ORM框架,不同的系统之间数据类型的转换就是首先要考虑的;MyBatis一项基本的功能,就是帮助我们,将JDBC类型与Java类型之间转换变得透明,能解放大家更多得精力去与产品吵架(狗头)。开个玩笑,正是由于JDBC类型与Java类型,并不是一对一得关系,所以我们更加需要MyBatis去帮我们解决这类繁琐而且重复的工作。

TypeHandler,就是MyBatis给出的解决方案。

TypeHandler简述

MyBatis在设置预处理语句(PreparedStatement)中的参数,或从结果集中取出一个值时, 会选择使用对应的TypeHandler类型处理器,将获取到的值以合适的方式转换成 Java 类型;MyBatis的在内部设定了很多基础的处理器,下图是从MyBatis代码中截取的,框架内部封装好的类型处理器。
image-20211028123441030.png

TypeHandler使用

在MyBatis框架中,为了方便开发人员,提供了三种方式使用TypeHandler:

  1. 从config文件中,通过typeHandlers以及子标签typeHandler,对自定义的TypeHandler进行注册;
  2. 或者通过package子标签,对TypeHandler所在包进行扫描注册;
  3. 在mapper.xml文件中,在resultMap或者在参数使用中,可以显示的声明TypeHandler,如下:
1
2
3
4
5
6
7
8
bash复制代码<result column="phone" property="phone" 			 
typeHandler="com.example.typeHandle.ClientPhoneTypeHandler"/>

<update id="updatePhone">
update users
set phone = #{phone, typeHandler=com.example.typeHandle.ClientPhoneTypeHandler}
where id = #{id}
</update>
  1. spring boot集成了MyBatis的starter,可以通过*.properties的文件进行配置
1
ini复制代码mybatis.type-handlers-package=${packagePath}

PS:笔者并未找到可以通过单个注册的处理器的方法

TypeHandler原理

那么TypeHandler在整个生命周期中是如何加载及使用的呢?

首先是注册阶段,MyBatis会通过XMLConfigBuilder对配置的xml文件中的标签进行解析,其中就包含上文中提到的TypeHandler相关标签;在解析时,会调用TypeHandlerRegistry的注册方法,将自定义的handler加载到JVM中;如果大家有兴趣,点开这个注册类,就能发现它的构建方法中,将上文列举的一些MyBatis框架内部定义的基础handler,进行了统一的初始化,并用Map将其与JDBC的类型建立的关联;

接下来,在解析mapper文件时,XMLMapperBuilder会选择适当的处理器;在解析ResultMap和ParameterMap时,从上阶段注册的TypeHandler中,找到最合适的TypeHandler,或者从mapper文件中,读取显示声明的TypehHandler,存入ParameterMapping实例中;

最终语句执行阶段,会调用处理器,对参数或查询结果进行转换;对应的Sql语句在输入参数时,调用TypeHander接口的setParameter方法,进行入参转换操作;获取到查询结果后,会调用getResult的方法,对结果转换从而返回。

自己定义一个TypeHandler

用两个实际的例子,来分享下我的使用场景。

场景1,用户更新标识;
要求:公司内部有单独的人力系统维护人员信息,笔者所在系统的人员信息修改有三种方式:1、员工手动修改;2、管理员手动修改;3、来自行内人力资源系统信息。业务规定员工手动修改信息后,不再同步来自人力资源系统的修改信息;同时需要根据修改的信息,设置同步标志,每一种不同的信息,必须通过不同的标识位进行控制。

设计与实现:人员主表增加更新标识位字段,通过二进制位,标识每个不同种类的信息的是否同步;代码中增加专用的DTO类,表明该数据为特殊更新属性标识;新增注解和二进制位枚举,通过在DTO类属性添加注解,指定该属性对应的枚举信息;增加专用TypeHandler用于数据库中的number类型与DTO类型的转换;在mapper文件中,显示的指定具体的TypeHandler。

Talk is cheap, show me your code.

注解类:

1
2
3
4
5
6
7
8
less复制代码@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MappedPropertiesChangeType {

PersonDetailChangeEnum value();

String name();
}

DTO类:

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
ini复制代码public class PropertiesUpdateMarkDTO implements Serializable {

@MappedPropertiesChangeType(
value = PersonDetailChangeEnum.EMAIL,
name = "email")
private int email = 0;

@MappedPropertiesChangeType(
value = PersonDetailChangeEnum.PHONE,
name = "phone")
private int phone = 0;

@MappedPropertiesChangeType(
value = PersonDetailChangeEnum.ICON,
name = "icon")
private int icon = 0;

@MappedPropertiesChangeType(
value = PersonDetailChangeEnum.FIXTEL,
name = "fixTel")
private int fixTel = 0;

@MappedPropertiesChangeType(
value = PersonDetailChangeEnum.DUTY,
name = "duty")
private int duty = 0;

private static class Constant {
private static final Field[] _FIELDS = PropertiesUpdateMarkDTO.class.getDeclaredFields();
}

/** 代码总条数过长,故 getter 和 setter 方法省略 **/

public static int transformDtoToInt(PropertiesUpdateMarkDTO dto) {

return Arrays.stream(Constant._FIELDS).map(
field -> PropertiesUpdateMarkDTO.bitToInt(field, dto)
).reduce(0, Integer::sum);
}

public static PropertiesUpdateMarkDTO transformIntToDTO(int input) {

PropertiesUpdateMarkDTO dto = new PropertiesUpdateMarkDTO();
Arrays.stream(Constant._FIELDS).forEach(field -> setField(field, dto, input));
return dto;
}

private static int bitToInt(Field field, PropertiesUpdateMarkDTO dto) {

MappedPropertiesChangeType[] annotations
= field.getAnnotationsByType(MappedPropertiesChangeType.class);

if (annotations.length > 1) {
throw new EbdcException(EbdcErrorCode.Biz.B0001,
"Do not support multiple MappedPropertiesChangeType annotation");
}

Method method = BeanUtil.getPropertyDescriptor(
PropertiesUpdateMarkDTO.class,
annotations[0].name()
).getReadMethod();
return ((Integer) ReflectUtil.invoke(dto, method))
<< annotations[0].value().getDisposition();
}

private static void setField(Field field, PropertiesUpdateMarkDTO dto, int input) {

MappedPropertiesChangeType[] annotations
= field.getAnnotationsByType(MappedPropertiesChangeType.class);

if (annotations.length > 1) {
throw new EbdcException(EbdcErrorCode.Biz.B0001,
"Do not support multiple MappedPropertiesChangeType annotation");
}

Method method = BeanUtil.getPropertyDescriptor(
PropertiesUpdateMarkDTO.class,
annotations[0].name()
).getWriteMethod();
ReflectUtil.invoke(dto, method,
(input & annotations[0].value().getBinary())
>> annotations[0].value().getDisposition());
}

}

Enum类:

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
csharp复制代码public enum PersonDetailChangeEnum {

EMAIL(0),//邮箱
PHONE(1),//移动电话
ICON(2),//头像
FIXTEL(3),//固定电话
DUTY(4);//职务

private final int binary;

private final int disposition;

public Integer getBinary() {
return binary;
}

public int getDisposition() {
return disposition;
}

PersonDetailChangeEnum(int disposition) {
this.binary = 1 << disposition;
this.disposition = disposition;
}

public static Integer changeBinaryState(List<Integer> integers, Integer result) {
if (result == null) {
result = 0;
}
for (Integer value : integers) {
result = value | result;
}
return result;
}
}

TypeHandler实现:

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
java复制代码@MappedTypes(PropertiesUpdateMarkDTO.class)
@MappedJdbcTypes(JdbcType.NUMERIC)
public class PropertiesMarkHandler implements TypeHandler<PropertiesUpdateMarkDTO> {

@Override
public void setParameter(PreparedStatement preparedStatement, int i,
PropertiesUpdateMarkDTO propertiesUpdateMarkDTO, JdbcType jdbcType) throws SQLException {
preparedStatement.setInt(i,
PropertiesUpdateMarkDTO.transformDtoToInt(propertiesUpdateMarkDTO));
}

@Override
public PropertiesUpdateMarkDTO getResult(ResultSet resultSet, String s) throws SQLException {
return PropertiesUpdateMarkDTO.transformIntToDTO(resultSet.getInt(s));
}

@Override
public PropertiesUpdateMarkDTO getResult(ResultSet resultSet, int i) throws SQLException {
return PropertiesUpdateMarkDTO.transformIntToDTO(resultSet.getInt(i));
}

@Override
public PropertiesUpdateMarkDTO getResult(CallableStatement callableStatement, int i) throws SQLException {
return PropertiesUpdateMarkDTO.transformIntToDTO(callableStatement.getInt(i));
}
}

mapper.xml文件:

1
2
3
4
5
ini复制代码<resultMap id="HrsPersonVOResultMap" type="*.person.bo.PersonVO" extends="PersonVOResultMap">
<result column="JOB_NAME" property="jobName"/>
<result column="PROPERTIES_UPDATE_MARK" property="properties"
typeHandler="*.PropertiesMarkHandler"/>
</resultMap>

场景2,银行卡号脱敏展示;

要求:用于展示用户银行卡账号的页面,为了保证客户信息不被泄露,都必须经过脱敏处理的,显示带有*的银行卡号;

设计与实现:增加特殊的String字段的TypeHandler类;在mapper中,找到需要脱敏的DO中字段,同时在对应的ResultMap中,显示的指定该TypeHandler;数据库读取到的银行卡账号信息,通过getNullableResult方法,进行转换,从而达到脱敏处理。

TypeHandler实现:

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
java复制代码public class DesCardNoTypeHandler extends BaseTypeHandler<String> {

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

@Override
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
return desCardNo(rs.getString(columnName));
}

@Override
public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return desCardNo(rs.getString(columnIndex));
}

@Override
public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return desCardNo(cs.getString(columnIndex));
}

private static String desCardNo(String cardNo) {
return DesensitizedUtil.bankCard(cardNo);
}
}

ResultMap信息:

1
2
3
4
5
6
7
8
9
ini复制代码<resultMap id="BaseUserCardInfo" type="com.example.entity.card.UserCard">
<id property="userId" column="user_id"/>
<result property="cardNo" column="card_no"/>
</resultMap>

<resultMap id="DesUserCardInfo" type="com.example.entity.card.UserCard">
<id property="userId" column="user_id"/>
<result property="cardNo" column="card_no" typeHandler="com.example.typeHandle.DesCardNoTypeHandler"/>
</resultMap>

运行结果:

1
2
3
4
5
6
7
8
ini复制代码Created connection 2006112337.
==> Preparing: select user_id, card_no from user_card where user_id = ?
==> Parameters: 1(String)
<== Columns: user_id, card_no
<== Row: 1, 6666666666666666666
<== Total: 1
UserCard{userId='1', cardNo='6666 **** **** *** 6666'}
Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@7792d851]

本文转载自: 掘金

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

0%