MyBatis TypeHandler学习及实战

5,688 阅读6分钟

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,如下:
<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的文件进行配置
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.

注解类:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MappedPropertiesChangeType {

    PersonDetailChangeEnum value();

    String name();
}

DTO类:

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类:

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实现:

@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文件:

<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实现:

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信息:

<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>

运行结果:

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]