当前位置:   article > 正文

SpringBoot中使用Mybatis的TypeHandler简单案例及源码简析_springboot typehandler

springboot typehandler

1、TypeHandler是什么?

TypeHandler是Mybatis中Java对象和数据库JDBC之间进行类型转换的桥梁

是Mybatis内部的一个接口,实现它就可以完成Java对象到数据库之间的转换

内部结构如下:

public interface TypeHandler<T> {

  void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;

  /**
   * @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;

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

第一个方法是从Java对象到数据库的转换,后面三个的是从数据库到Java对象的转换

2、如何使用TypeHandler

  • 直接实现TypeHandler接口

  • Mybatis中有一个抽象类BaseTypeHandler,实现了TypeHandler并进行了扩展

采用直接继承BaseTypeHandler

有一张数据库表,其中有一个details字段为json类型,其DDL为

CREATE TABLE `test` (
 `id` int NOT NULL AUTO_INCREMENT,
 `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
 `details` json NOT NULL,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

将Java对象转换成数据库中json字段, 通过实现自定义的TypeHandler,完成对test表的查询和插入

首先定义JavaBean

@Data
public class TestDO {

    private int id;
    private LocalDateTime createTime;
    private PersonDO personDO;

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
@Data
public class PersonDO {

    private String name;
    private int age;

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

TestDO对应test数据库表,PersonDO对应着数据库中的details字段

接下来继承BaseTypeHandler

@MappedJdbcTypes(JdbcType.VARCHAR)
@MappedTypes({PersonDO.class})
public class ObjectJSONTypeHandler extends BaseTypeHandler<PersonDO> {

    private Gson gson = new Gson();

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

    @Override
    public PersonDO getNullableResult(ResultSet rs, String columnName) throws SQLException {
        return gson.fromJson(rs.getString(columnName), PersonDO.class);
    }

    @Override
    public PersonDO getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        return gson.fromJson(rs.getString(columnIndex), PersonDO.class);
    }

    @Override
    public PersonDO getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        return gson.fromJson(cs.getString(columnIndex), PersonDO.class);
    }
}
  • 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

注意:

  • @MappedJdbcTypes代表对应的数据库中字段的类型,json类型本质上也是字符串
  • @MappedTypes代表要转换的JavaBean对象
  • Java对象与JSON格式的互换借助Google的Gson完成

剩下的,就是在mapper文件中查询和插入时,指定要使用的typeHandler

<?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.demoxxx.TestMapper" >

    <resultMap id="resultMap" type="com.demo.xxx.TestDO">
        <id column="id" property="id"/>
        <result column="create_time" property="createTime"/>
        <result column="details" property="personDO" typeHandler="com.demo.xxx.ObjectJSONTypeHandler"/>
    </resultMap>

    <select id="selectById" parameterType="int" resultMap="resultMap">
        select id,create_time,details
        from test
        where id=#{param1};
    </select>

    <insert id="insert">
        insert into test(create_time,details) values (#{createTime},#{personDO, typeHandler=com.demo.xxx.ObjectJSONTypeHandler})
    </insert>
</mapper>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

或者,不在<resultMap>中指定的话,可以在application.yml文件中指定扫描的类(SpringBoot),这样<resultMap>和insert中就可以不写typeHandler

mybatis:
  type-handlers-package: com.xxx.typehandler
  • 1
  • 2

区别在于如果写在配置文件中,任何使用PersonDO的地方都会进行转换,写在mapper中,只有对应的SQL会进行转换

3、源码简析

当在yml中指定TypeHandler时,它的在于SqlSessionFactoryBean中进行,具体为其中的buildSqlSessionFactory方法

protected SqlSessionFactory buildSqlSessionFactory() throws Exception {

    final Configuration targetConfiguration;

    XMLConfigBuilder xmlConfigBuilder = null;
    
    ...省略

    if (!isEmpty(this.plugins)) {
      Stream.of(this.plugins).forEach(plugin -> {
        targetConfiguration.addInterceptor(plugin);
        LOGGER.debug(() -> "Registered plugin: '" + plugin + "'");
      });
    }

    if (hasLength(this.typeHandlersPackage)) {
      scanClasses(this.typeHandlersPackage, TypeHandler.class).stream().filter(clazz -> !clazz.isAnonymousClass())
          .filter(clazz -> !clazz.isInterface()).filter(clazz -> !Modifier.isAbstract(clazz.getModifiers()))
          .filter(clazz -> ClassUtils.getConstructorIfAvailable(clazz) != null)
          .forEach(targetConfiguration.getTypeHandlerRegistry()::register);
    }

    if (!isEmpty(this.typeHandlers)) {
      Stream.of(this.typeHandlers).forEach(typeHandler -> {
        targetConfiguration.getTypeHandlerRegistry().register(typeHandler);
        LOGGER.debug(() -> "Registered type handler: '" + typeHandler + "'");
      });
    }
    ...省略
    return this.sqlSessionFactoryBuilder.build(targetConfiguration);
  }
  • 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

scanClasses方法进行加载自定义的typeHandler

在Mapper中自定义时,依然也是在SqlSessionFactoryBean的buildSqlSessionFactory方法,不过是在scanClasses下面,毕竟没在配置文件中配置TypeHandler

protected SqlSessionFactory buildSqlSessionFactory() throws Exception {

    final Configuration targetConfiguration;

    XMLConfigBuilder xmlConfigBuilder = null;
    
    ...省略
	if (this.mapperLocations != null) {
      if (this.mapperLocations.length == 0) {
        LOGGER.warn(() -> "Property 'mapperLocations' was specified but matching resources are not 		found.");
      } else {
        for (Resource mapperLocation : this.mapperLocations) {
          if (mapperLocation == null) {
            continue;
          }
          try {
            XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
                targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments());
            xmlMapperBuilder.parse();
          } catch (Exception e) {
            throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
          } finally {
            ErrorContext.instance().reset();
          }
          LOGGER.debug(() -> "Parsed mapper file: '" + mapperLocation + "'");
        }
      }
    } else {
      LOGGER.debug(() -> "Property 'mapperLocations' was not specified.");
    }
    ... 省略
    return this.sqlSessionFactoryBuilder.build(targetConfiguration);
  • 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

这里会扫描mapper文件,扫描完后就会进入xmlMapperBuilder.parse()中解析

//XmlMapperBuilder
public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();
    }

    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

具体的解析"/mapper"元素

private void configurationElement(XNode context) {
    try {
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      builderAssistant.setCurrentNamespace(namespace);
      cacheRefElement(context.evalNode("cache-ref"));
      cacheElement(context.evalNode("cache"));
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      sqlElement(context.evalNodes("/mapper/sql"));
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
    }
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

可以看到有resultMap的解析,有insert的解析,这里只看resultMap的解析

private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) throws Exception {
    ...省略
    for (XNode resultChild : resultChildren) {
      if ("constructor".equals(resultChild.getName())) {
        processConstructorElement(resultChild, typeClass, resultMappings);
      } else if ("discriminator".equals(resultChild.getName())) {
        discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
      } else {
        List<ResultFlag> flags = new ArrayList<>();
        if ("id".equals(resultChild.getName())) {
          flags.add(ResultFlag.ID);
        }
        resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
      }
    }
    省略...
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

重点在于buildResultMappingFromContext方法中

private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags) throws Exception {
    String property;
    if (flags.contains(ResultFlag.CONSTRUCTOR)) {
      property = context.getStringAttribute("name");
    } else {
      property = context.getStringAttribute("property");
    }
    String column = context.getStringAttribute("column");
    String javaType = context.getStringAttribute("javaType");
    String jdbcType = context.getStringAttribute("jdbcType");
    String nestedSelect = context.getStringAttribute("select");
    String nestedResultMap = context.getStringAttribute("resultMap",
        processNestedResultMappings(context, Collections.emptyList(), resultType));
    String notNullColumn = context.getStringAttribute("notNullColumn");
    String columnPrefix = context.getStringAttribute("columnPrefix");
    String typeHandler = context.getStringAttribute("typeHandler");
    String resultSet = context.getStringAttribute("resultSet");
    String foreignColumn = context.getStringAttribute("foreignColumn");
    boolean lazy = "lazy".equals(context.getStringAttribute("fetchType", configuration.isLazyLoadingEnabled() ? "lazy" : "eager"));
    Class<?> javaTypeClass = resolveClass(javaType);
    Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);
    JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
    return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy);
  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

可以看到其中String typeHandler = context.getStringAttribute(“typeHandler”);

在这里完成了Typehandler的加载

总结一下的话

  • 在配置文件中配置,会直接通过文件的配置路径读取
  • 在mapper中配置,会解析mapper文件,从中得到具体的配置
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/不正经/article/detail/173068
推荐阅读
相关标签
  

闽ICP备14008679号