1

The original simpler way to customize TypeHandler

http://followtry.cn/2016-08-17/mybatis-handler-enum.html

background

Because Mybatis default Enum org.apache.ibatis.type.EnumTypeHandler only supports 06113758091082 or org.apache.ibatis.type.EnumOrdinalTypeHandler . But because many businesses define types using enumerations, the fields stored in the database are of int or varchar type. Generally, the default name or ordinal of the enumeration is not used as the value storage in the database. Therefore, when using mybatis to store enumerations, it is necessary to manually fetch the int value of the enumeration (taking out the custom code attribute of the int type as an example), which is not easy to maintain in the future. Therefore, I want to implement a two-way conversion between int and mapped enumeration through a custom enumeration type.

Custom EnumTypeHandler practice plan

If you use a custom enumeration processor, you need to implement a fixed interface for all enumerations, and get the int value through the interface method

Custom enumeration needs to implement the interface

The interface name is BaseBizEnum

/**
 * @author followtry
 * @since 2021/8/9 3:30 下午
 */
public interface BaseBizEnum {

    Integer getCode();
}

Implement a custom enumeration of the interface

The custom enumeration is AgreementType, which implements BaseBizEnum, and its getCode method is marked with the @Override annotation

import com.google.common.collect.Maps;
import lombok.Getter;
import java.util.Map;
import java.util.Optional;

public enum AgreementType implements BaseBizEnum{
    /***/
    QUICK_PAY(1,"免密支付"),
    ;

    private final Integer code;
    @Getter
    private final String desc;
    private static Map<Integer,AgreementType> itemMap = Maps.newHashMap();
    static {
        for (AgreementType typeEnum : AgreementType.values()) {
            itemMap.put(typeEnum.getCode(),typeEnum);
        }
    }
    AgreementType(Integer code, String desc) {
        this.code = code;
        this.desc = desc;
    }
    //重写了接口BaseBizEnum的方法
    @Override
    public Integer getCode() {
        return code;
    }
    public static AgreementType ofNullable(Integer code) {
        return itemMap.get(code);
    }
}

With a custom enumeration, you need to have a custom enumeration type to parse the enumeration

Define enumeration class handler

For the same type of enumeration, you can define the processing class of the base class to implement general logic.

import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;

/**
 * @author followtry
 * @since 2021/8/9 3:38 下午
 */
public class BizEnumTypeHandler<E extends BaseBizEnum> extends BaseTypeHandler<E> {

    private Class<E> type;

    //初始化时定义枚举和code的映射关系
    private final Map<Integer,E> enumsMap = new HashMap<>();

    public BizEnumTypeHandler(Class<E> type) {
        if (type == null) {
            throw new IllegalArgumentException("Type argument cannot be null");
        }
        this.type = type;
        for (E enumConstant : type.getEnumConstants()) {
            enumsMap.put(enumConstant.getCode(),enumConstant);
        }
        if (this.enumsMap.size() == 0) {
            throw new IllegalArgumentException(type.getSimpleName() + " does not represent an enum type.");
        }
    }

    //在请求Sql执行时转换参数
    @Override
    public void setNonNullParameter(PreparedStatement preparedStatement, int i, E e, JdbcType jdbcType) throws SQLException {
        preparedStatement.setInt(i,e.getCode());
    }

    //处理返回结果
    @Override
    public E getNullableResult(ResultSet resultSet, String columnName) throws SQLException {
        if (resultSet.wasNull()) {
            return null;
        }
        int code = resultSet.getInt(columnName);
        return getEnum(code);
    }

    private E getEnum(Integer code) {
        try {
            return getEnumByValue(code);
        } catch (Exception ex) {
            throw new IllegalArgumentException(
                    "Cannot convert " + code + " to " + type.getSimpleName() + " by ordinal value.", ex);
        }
    }

    protected E getEnumByValue(Integer code) {
        return enumsMap.get(code);
    }

    @Override
    public E getNullableResult(ResultSet resultSet, int columnIndex) throws SQLException {
        if (resultSet.wasNull()) {
            return null;
        }
        int code = resultSet.getInt(columnIndex);
        return getEnum(code);
    }

    @Override
    public E getNullableResult(CallableStatement callableStatement, int columnIndex) throws SQLException {
        if (callableStatement.wasNull()) {
            return null;
        }
        int code = callableStatement.getInt(columnIndex);
        return getEnum(code);
    }
}

Now that the base class for enumeration processing is available, it is necessary to implement a custom enumeration processing class by inheriting the base class

Create AgreementTypeEnumTypeHandler class

import com.autonavi.aos.tmp.api.enums.AgreementType;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
import org.apache.ibatis.type.MappedTypes;

 //指定处理的映射枚举类的class
@MappedTypes(value = {AgreementType.class})
//指定返回结果时哪些jdbc类型的值需要转换
@MappedJdbcTypes(value = {JdbcType.INTEGER,JdbcType.TINYINT,JdbcType.SMALLINT})
public class AgreementTypeEnumTypeHandler extends BizEnumTypeHandler<AgreementType>{
    //在当前类实例化时即给父类的构造方法指定枚举类
    public AgreementTypeEnumTypeHandler() {
        super(AgreementType.class);
    }
}

After defining the above code, it is necessary for Mybatis to scan it and register it in Mybatis's TypeHandler registrar when it is initialized to achieve parsing.

Scan the custom TypeHandler class

Because the integration of SpringBoot and Mybatis is used, the scanned directory needs to be specified in the application.properties file so that AgreementTypeEnumTypeHandler can be identified.

mybatis.type-handlers-package=cn.followtry.typehandler
mybatis.type-aliases-package=cn.followtry.typehandler
mybatis.configuration.map-underscore-to-camel-case=true

After the above steps are completed, the enumeration field can be automatically converted to the int type before being stored in the DB. The corresponding fields queried from db are automatically converted to enumerated types.

Sample Sql configuration

Table building statement

CREATE TABLE `test_agreement_info` (
    `id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
    `agreement_type` tinyint NULL COMMENT '协议类型',
    `name` varchar(100) NULL COMMENT '协议名称',
    PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET=utf8mb4 COMMENT='协议信息';

Mapper interface of Mybatis

import cn.followtry.AgreementType;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface TestAgreementInfoMapper {

    @Insert("insert into test_agreement_info(name,agreement_type) value(#{name},#{agreementType})")
    boolean insert(@Param("name") String name, @Param("agreementType") AgreementType agreementType);

    @Select("select * from test_agreement_info where name = #{name}")
    List<TestAgreementModel> selectByName(@Param("name") String name);
}

Test Controller code

@RestController
@RequestMapping(value = "/ws/tc/test", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public class TestController2 {
    @Autowired
    private TestAgreementInfoMapper testAgreementInfoMapper;
    @GetMapping(value = "insertTest")
    public Object insertTest(String name, AgreementType agreementType) {
        System.out.println("name="+name+",agreementType="+agreementType.name());
        return testAgreementInfoMapper.insert(name,agreementType);
    }
    @GetMapping(value = "getTest")
    public Object getTest(String name) {
        return testAgreementInfoMapper.selectByName(name);
    }
}

After the service is started, insert and query data respectively through interface calls

http://localhost:8080/ws/tc/test/insertTest?name=zhangsan&agreementType=QUICK_PAY
http://localhost:8080/ws/tc/test/getTest?name=zhangsan

The above code will convert the QUICK_PAY when inserting to 1 and store it in the db, and when querying, it will convert 1 to the instance QUICK_PAY of the enumeration AgreementType

that readers have understood the AutoConfiguration mechanism of SpringBoot, the loading mechanism of Mybatis, and the initialization process of the Configuration and SQLSessionFactory of Mybatis.

Principle analysis

When the application starts, SpringBoot will drive Mybatis to initialize and load the extended enumeration class into the core mechanism of Mybatis.

When Sql is executed, Mybatis will dynamically replace the enumeration parameters with the int type, and convert the corresponding int type in the returned result to the corresponding enumeration

First, the initialization phase

Load MybatisAutoConfiguration

SpringBoot will automatically load the MybatisAutoConfiguration class, which will be Bean conversion and registration according to Spring rules.

Spring initializes SqlSessionFactoryBean through the MybatisAutoConfiguration#sqlSessionFactory method, and at this time, the configured parameter analysis is set to its properties.

The parameters are as follows

mybatis.type-handlers-package=cn.followtry.typehandler
mybatis.type-aliases-package=cn.followtry.typehandler
mybatis.configuration.map-underscore-to-camel-case=true

At the end of the method, SqlSessionFactoryBean will call the getObject method (Spring's FactoryBean mechanism) to perform SessionFactory At this time, the default configuration will be initialized and the configured cn.followtry.typehandler

    if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
      factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
    }

In the SqlSessionFactoryBean#buildSqlSessionFactory method, there is the following code snippet to parse and instantiate the customized AgreementTypeEnumTypeHandler

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

scanClasses method is to collect all the subcategories of the specified type under the specified package into the candidate set.

targetConfiguration.getTypeHandlerRegistry()::register will TypeHandler subclass, AgreementTypeEnumTypeHandler into the container.

The registration code is as follows


  /**
  *  先检查`AgreementTypeEnumTypeHandler`上的MappedTypes注解。有注解的以注解注入。
  *  仅仅支持当前类上的注解,不支持父类上的。
     如果没有指定MappedTypes注解,则无法判断该处理器处理哪个枚举,只能在xml配置Sql时指定TypeHandler
  */
  public void register(Class<?> typeHandlerClass) {
    boolean mappedTypeFound = false;
    MappedTypes mappedTypes = typeHandlerClass.getAnnotation(MappedTypes.class);
    if (mappedTypes != null) {
      for (Class<?> javaTypeClass : mappedTypes.value()) {
        register(javaTypeClass, typeHandlerClass);
        mappedTypeFound = true;
      }
    }
    if (!mappedTypeFound) {
      register(getInstance(null, typeHandlerClass));
    }
  }
  public void register(Class<?> javaTypeClass, Class<?> typeHandlerClass) {
    register(javaTypeClass, getInstance(javaTypeClass, typeHandlerClass));
  }
  
  //register方法参数上调用了本方法,在本方法内通过构造方法反射生成TypeHandler的实例
  public <T> TypeHandler<T> getInstance(Class<?> javaTypeClass, Class<?> typeHandlerClass) {
    if (javaTypeClass != null) {
      try {
          //如果存在带有class参数的构造方法,则使用其生成实例,否则使用无参构造方法生成实例
        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 {
        //因为本示例中的AgreementTypeEnumTypeHandler只有无参构造方法,因此只能通过此处代码生成实例
      Constructor<?> c = typeHandlerClass.getConstructor();
      return (TypeHandler<T>) c.newInstance();
    } catch (Exception e) {
      throw new TypeException("Unable to find a usable constructor for " + typeHandlerClass, e);
    }
  }


  
  public <T> void register(Class<T> type, JdbcType jdbcType, TypeHandler<? extends T> handler) {
    register((Type) type, jdbcType, handler);
  }

  //注册最终会调用该方法将AgreementTypeEnumTypeHandler注册进typeHandlerMap中,typeHandlerMap本质是个Map,用来作为typeHandler的容器
  //而对于没有指定类型的TypeHandler,则注册进allTypeHandlersMap中,在sql配置中指定后才能使用。
  //typeHandlerMap容器的key为MappedTypes注解指定的枚举类,value为MappedJdbcTypes指定的jdbc类型和AgreementTypeEnumTypeHandler实例的映射
  private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
    if (javaType != null) {
      Map<JdbcType, TypeHandler<?>> map = typeHandlerMap.get(javaType);
      if (map == null || map == NULL_TYPE_HANDLER_MAP) {
        map = new HashMap<>();
        typeHandlerMap.put(javaType, map);
      }
      map.put(jdbcType, handler);
    }
    allTypeHandlersMap.put(handler.getClass(), handler);
  }

Take a look at the processors loaded by default in Mybatis (not all)

public TypeHandlerRegistry() {
    register(Boolean.class, new BooleanTypeHandler());
    register(boolean.class, new BooleanTypeHandler());
    register(JdbcType.BOOLEAN, new BooleanTypeHandler());
    register(JdbcType.BIT, new BooleanTypeHandler());

    register(Byte.class, new ByteTypeHandler());
    register(byte.class, new ByteTypeHandler());
    register(JdbcType.TINYINT, new ByteTypeHandler());

    register(Short.class, new ShortTypeHandler());
    register(short.class, new ShortTypeHandler());
    register(JdbcType.SMALLINT, new ShortTypeHandler());

    register(Integer.class, new IntegerTypeHandler());
    register(int.class, new IntegerTypeHandler());
    register(JdbcType.INTEGER, new IntegerTypeHandler());

    register(Long.class, new LongTypeHandler());
    register(long.class, new LongTypeHandler());

    register(Float.class, new FloatTypeHandler());
    register(float.class, new FloatTypeHandler());
    register(JdbcType.FLOAT, new FloatTypeHandler());

    register(Double.class, new DoubleTypeHandler());
    register(double.class, new DoubleTypeHandler());
    register(JdbcType.DOUBLE, new DoubleTypeHandler());
    ....
}

After the initialization and loading, when do you set the parameters to convert the enumeration to int, and then look down.

Execute Sql and convert the enumeration parameters to int

Re our Sql Code to move over, according to Mybatis mechanism if the parameters used #{} instead ${} will be used PreparedStatement , if used ${} , then Mybatis the type of processor is not in force here are not careful might Step on the pit.

@Mapper
public interface TestAgreementInfoMapper {

    @Insert("insert into test_agreement_info(name,agreement_type) value(#{name},#{agreementType})")
    boolean insert(@Param("name") String name, @Param("agreementType") AgreementType agreementType);

    @Select("select * from test_agreement_info where name = #{name}")
    List<TestAgreementModel> selectByName(@Param("name") String name);
}

@Data
public class TestAgreementModel {
    private String name;

    private AgreementType agreementType;
}

For Mybatis's parameter processing interface ParameterHandler , it has a default implementation of DefaultParameterHandler , and the parameter conversion is completed DefaultParameterHandler#setParameters

code show as below

  @Override
  public void setParameters(PreparedStatement ps) {
    ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
    //Mybatis通过Param注解解析到的参数映射,因为我们没在xml配置中指定jdbc类型和TypeHandler类型,因此在此方法内部获取TypeHandler是UnknownTypeHandler
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
      for (int i = 0; i < parameterMappings.size(); i++) {
        ParameterMapping parameterMapping = parameterMappings.get(i);
        if (parameterMapping.getMode() != ParameterMode.OUT) {
          Object value;
          String propertyName = parameterMapping.getProperty();
          if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
            value = boundSql.getAdditionalParameter(propertyName);
          } else if (parameterObject == null) {
            value = null;
          } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
            value = parameterObject;
          } else {
            MetaObject metaObject = configuration.newMetaObject(parameterObject);
            value = metaObject.getValue(propertyName);
          }
          //此处parameterMapping没有解析到设置的TypeHandler和JdbcType,因为压根就没设置,但是不妨碍Mybatis推断出使用的TypeHanler,对于没有配置TypeHandler的,Mybatis有默认实现UnknownTypeHandler
          TypeHandler typeHandler = parameterMapping.getTypeHandler();
          JdbcType jdbcType = parameterMapping.getJdbcType();
          if (value == null && jdbcType == null) {
            jdbcType = configuration.getJdbcTypeForNull();
          }
          try {
              //使用的UnknownTypeHandler来设置未获取到相关配置的参数
            typeHandler.setParameter(ps, i + 1, value, jdbcType);
          } catch (TypeException | SQLException e) {
            throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
          }
        }
      }
    }
  }

In the use of UnknownTypeHandler set the parameters, the processor of the current enumeration type will be found again according to the java type and jdbc type. code show as below

public class UnknownTypeHandler extends BaseTypeHandler<Object> {
  @Override
  public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType)
      throws SQLException {
    TypeHandler handler = resolveTypeHandler(parameter, jdbcType);
    //AgreementTypeEnumTypeHandler实例调用setParameter方法,就进入了我们自定义的AgreementTypeEnumTypeHandler中,按照我们的逻辑将枚举通过getCode方法转为了int类型
    handler.setParameter(ps, i, parameter, jdbcType);
  }

  //获取的具体枚举的类型处理器
  private TypeHandler<?> resolveTypeHandler(Object parameter, JdbcType jdbcType) {
    TypeHandler<?> handler;
    if (parameter == null) {
      handler = OBJECT_TYPE_HANDLER;
    } else {
        //如果jdbcType还是为null,则取其唯一的一个类型处理器实例。其实在Mybatis加载时多个jdbcType对应的TypeHandler实例是相同的
        //因此就能获取到AgreementTypeEnumTypeHandler实例
      handler = typeHandlerRegistry.getTypeHandler(parameter.getClass(), jdbcType);
      // check if handler is null (issue #270)
      if (handler == null || handler instanceof UnknownTypeHandler) {
        handler = OBJECT_TYPE_HANDLER;
      }
    }
    //此处返回的是AgreementTypeEnumTypeHandler实例
    return handler;
  }
}

For the enumeration parameters of Insert, through the execution of the above series of codes, the conversion of enumeration and int types has been realized. Next, use the query method to see how the returned result converts int to enumeration.

Execute query Sql, convert int to enumeration

When the query is executed, it will first call SimpleExecutor#prepareStatement method is called internally ParameterHandler#setParameters conversion achieved parameters.
After the parameter conversion, execute sql and get the result ResultSet returned by jdbc, and then convert the ResultSet to the specified type.

There is a ResultSetHandler interface for processing ResultSet results, and the ResultSet is converted into an actual type instance in the default implementation of the DefaultResultSetHandler#handleResultSets

DefaultResultSetHandler#getRowValue method actually maps a row of Sql data to the entry of the result type.

In DefaultResultSetHandler#getRowValue internal method, calls DefaultResultSetHandler#createResultObject method to get the type of value map (the TypeHandler types of results if the TypeHandler, acquired using defined) or object instance is generated using the default constructor (for custom objects). TestAgreementInfoMapper.selectByName method in this example, the return type is List<TestAgreementModel> , so each row of data will create a new TestAgreementModel object instance, and the attributes of the instance are still null.

//创建行数据对象的核心方法如下
  private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, List<Class<?>> constructorArgTypes, List<Object> constructorArgs, String columnPrefix)
      throws SQLException {
    final Class<?> resultType = resultMap.getType();
    final MetaClass metaType = MetaClass.forClass(resultType, reflectorFactory);
    final List<ResultMapping> constructorMappings = resultMap.getConstructorResultMappings();
    if (hasTypeHandlerForResultObject(rsw, resultType)) {
        //如果返回一列数据,如直接返回AgreementType,则会通过createPrimitiveResultObject方法直接获取到值
      return createPrimitiveResultObject(rsw, resultMap, columnPrefix);
    } else if (!constructorMappings.isEmpty()) {
      return createParameterizedResultObject(rsw, resultType, constructorMappings, constructorArgTypes, constructorArgs, columnPrefix);
    } else if (resultType.isInterface() || metaType.hasDefaultConstructor()) {
        //创建自定义的对象
      return objectFactory.create(resultType);
    } else if (shouldApplyAutomaticMappings(resultMap, false)) {
      return createByConstructorSignature(rsw, resultType, constructorArgTypes, constructorArgs);
    }
    throw new ExecutorException("Do not know how to create an instance of " + resultType);
  }

For the newly generated TestAgreementModel object instance, mybatis will generate a new MetaObject instance. MetaObject contains the original object and the analysis information of the original object class's properties and setters, getter methods and construction methods. The analysis information is stored in the class org.apache.ibatis.reflection.Reflector , and the access path is relatively deep ( MetaObject.BeanWrapper.MetaClass.Reflector )

Automatic mapping is realized through DefaultResultSetHandler#applyAutomaticMappings . The automatic mapping mechanism uses a cache. The attribute names with the key in the camel case format are converted to uppercase, and the value is the attribute name. By removing the underscores of the column names and converting them to uppercase letters, you can find the corresponding java object attribute names from the cache. For mapping relationship is not explicitly specified properties, mybatis mapping relationship which will be encapsulated in UnMappedColumnAutoMapping in, UnMappedColumnAutoMapping attributes include column , property , typeHandler , primitive . For the field mapping relationship, mybatis has made a first level cache to avoid parsing again in the next call and improve performance.

To get more UnMappedColumnAutoMapping After collection, the collection will be executed cyclically, and call each mapping UnMappedColumnAutoMapping of TypeHandler#getResult get to the actual value. For example, call the BizEnumTypeHandler#getNullableResult method in this example to execute our custom logic and get the converted value. Through metaObject (which contains the current java instance), the value is assigned to the original java instance through the reflection mechanism of the method. Repeat this way until the UnMappedColumnAutoMapping collection cycle is completed, and the assignment of a row of data is completed. Then continue the above operation for the next line until the data assignment is complete.

After the assignment data line ( TestAgreementModel(name=zhangsan1, agreementType=QUICK_PAY) after) temporarily stores the result required, by calling ResultHandler#handleResult method, the object is stored in the result of ResultHandler. The default implementation class DefaultResultHandler uses List<Object> to store all row data. Each ResultSet has an DefaultResultHandler , which can guarantee concurrency safety.

By DefaultResultHandler data may be assigned to the reference layer to the recipient returns a result List<Object> multipleResults , by multipleResults the data brought SimpleExecutor rather SimpleExecutor results returned to the DefaultSqlSession . SqlSession has selectOne and selectList , which can determine whether to return an object result to the application's mapper interface method or a List result or report an error if the conditions are not met.

As described above, the process of converting the ResultSet result of the Sql query into a JavaBean instance is completed.


疯狂小兵
193 声望9 粉丝

专注做后端,用java和go做工具,编写世界