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.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。