前言
在实现业务代码时,根据业务场景会抽象出类似于状态、阶段等枚举类。依据枚举值表示特定阶段,定义枚举值时,一般会定义code和value两个属性,用code表示枚举的标识,value表示对枚举值的描述,虽然使用code值或者value值也可以作为阶段的标识(例如0表示未提交,1表示已提交),但是直接使用code或者value容易出错且不便于统一修改,而使用枚举值可以使代码更加清晰明确,并且更加好维护。
但是使用枚举值进行入库和读库时要注意,mybatis默认的枚举类转换器EnumTypeHandler
,即按照枚举值进行处理的,不一定符合实际业务场景的需求。因此,需要实现自定义的mybatis枚举类转换器,对业务中使用的枚举类进行处理。
BaseTypeHandler
BaseTypeHandler
是mybatis提供的一个支持泛型的抽象类型处理器,这个类声明如下:
public abstract class BaseTypeHandler<T> extends TypeReference<T> implements TypeHandler<T> {
/**
* @deprecated Since 3.5.0 - See https://github.com/mybatis/mybatis-3/issues/1203. This field will remove future.
*/
@Deprecated
protected Configuration configuration;
/**
* @deprecated Since 3.5.0 - See https://github.com/mybatis/mybatis-3/issues/1203. This property will remove future.
*/
@Deprecated
public void setConfiguration(Configuration c) {
this.configuration = c;
}
@Override
public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
if (parameter == null) {
if (jdbcType == null) {
throw new TypeException("JDBC requires that the JdbcType must be specified for all nullable parameters.");
}
try {
ps.setNull(i, jdbcType.TYPE_CODE);
} catch (SQLException e) {
throw new TypeException("Error setting null for parameter #" + i + " with JdbcType " + jdbcType + " . "
+ "Try setting a different JdbcType for this parameter or a different jdbcTypeForNull configuration property. "
+ "Cause: " + e, e);
}
} else {
try {
setNonNullParameter(ps, i, parameter, jdbcType);
} catch (Exception e) {
throw new TypeException("Error setting non null for parameter #" + i + " with JdbcType " + jdbcType + " . "
+ "Try setting a different JdbcType for this parameter or a different configuration property. "
+ "Cause: " + e, e);
}
}
}
@Override
public T getResult(ResultSet rs, String columnName) throws SQLException {
try {
return getNullableResult(rs, columnName);
} catch (Exception e) {
throw new ResultMapException("Error attempting to get column '" + columnName + "' from result set. Cause: " + e, e);
}
}
@Override
public T getResult(ResultSet rs, int columnIndex) throws SQLException {
try {
return getNullableResult(rs, columnIndex);
} catch (Exception e) {
throw new ResultMapException("Error attempting to get column #" + columnIndex + " from result set. Cause: " + e, e);
}
}
@Override
public T getResult(CallableStatement cs, int columnIndex) throws SQLException {
try {
return getNullableResult(cs, columnIndex);
} catch (Exception e) {
throw new ResultMapException("Error attempting to get column #" + columnIndex + " from callable statement. Cause: " + e, e);
}
}
public abstract void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;
public abstract T getNullableResult(ResultSet rs, String columnName) throws SQLException;
public abstract T getNullableResult(ResultSet rs, int columnIndex) throws SQLException;
public abstract T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException;
}
其中声明了四个抽象方法,用于在入库时设置参数以及在读取时读取结果。我们只需要继承BaseTypeHandler
并且实现抽象方法即可。
声明枚举类父级接口
由于java的枚举类不支持继承,而我们又想要对业务枚举类提供一个父级模板,因此只能通过实现接口的方式为这些枚举类使用同一个模板。提供父级模板的优势在于通过自定义枚举类转换器对模板类型进行处理即可,只要枚举类实现了该模板即可使用此自定义枚举类转换器,无需提供多种自定义枚举类转换器。
package com.cube.share.base.templates;
import org.springframework.lang.NonNull;
import java.io.Serializable;
/**
* @author poker.li
* @date 2021/7/30 11:05
* <p>
* 枚举类父级接口
*/
public interface IEnum<C extends Serializable, V extends Serializable> {
/**
* 获取枚举的code
*
* @return code
*/
C getCode();
/**
* 获取枚举的value
*
* @return value
*/
V getValue();
/**
* 根据code从指定枚举类中获取枚举值
*
* @param clazz 枚举类
* @param code code
* @param <E> 枚举的类型
* @return 枚举值
*/
@NonNull
static <E extends Enum<?> & IEnum, C> E formCode(@NonNull Class<E> clazz, @NonNull C code) {
E[] elements = clazz.getEnumConstants();
for (E element : elements) {
if (element.getCode().equals(code)) {
return element;
}
}
throw new IllegalArgumentException("不合法的枚举code");
}
}
自定义枚举类转换器
package com.cube.share.handler.config;
import com.cube.share.base.templates.IEnum;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import java.io.Serializable;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* @author poker.li
* @date 2021/7/30 11:52
* <p>
* 自定义枚举类处理器
*/
public class IEnumTypeHandler<C extends Serializable, V extends Serializable, E extends Enum<?> & IEnum<C, V>> extends BaseTypeHandler<IEnum<C, V>> {
private final Class<E> type;
public IEnumTypeHandler(Class<E> type) {
if (type == null) {
throw new IllegalArgumentException("Enum type must be not null!");
}
this.type = type;
}
@Override
public void setNonNullParameter(PreparedStatement ps, int i, IEnum parameter, JdbcType jdbcType) throws SQLException {
ps.setObject(i, parameter.getCode());
}
@Override
public IEnum<C, V> getNullableResult(ResultSet rs, String columnName) throws SQLException {
@SuppressWarnings("unchecked") C code = (C) rs.getObject(columnName);
return rs.wasNull() ? null : IEnum.formCode(type, code);
}
@Override
public IEnum<C, V> getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return rs.wasNull() ? null : IEnum.formCode(type, columnIndex);
}
@Override
public IEnum<C, V> getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
@SuppressWarnings("unchecked") C code = (C) cs.getObject(columnIndex);
return cs.wasNull() ? null : IEnum.formCode(type, code);
}
}
代码比较简单,就不详细介绍了。
自适应枚举类转换器
在一些情况下,让mybatis根据枚举的声明顺序获取枚举值可能也是有用的,因此,可以根据枚举的类型自主选择枚举类处理器,同时兼容EnumOrdinalTypeHandler
。
package com.cube.share.handler.config;
import com.cube.share.base.templates.IEnum;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.EnumOrdinalTypeHandler;
import org.apache.ibatis.type.JdbcType;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* @author poker.li
* @date 2021/7/30 15:00
* <p>
* 自适应枚举类处理器
*/
public class AutoEnumTypeHandler<E extends Enum<E>> extends BaseTypeHandler<E> {
private final BaseTypeHandler<E> typeHandler;
public AutoEnumTypeHandler(Class<E> type) {
if (type == null) {
throw new IllegalArgumentException("Type must be not null!");
}
if (IEnum.class.isAssignableFrom(type)) {
//noinspection unchecked
typeHandler = new IEnumTypeHandler(type);
} else {
typeHandler = new EnumOrdinalTypeHandler<>(type);
}
}
@Override
public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException {
typeHandler.setNonNullParameter(ps, i, parameter, jdbcType);
}
@Override
public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
return typeHandler.getNullableResult(rs, columnName);
}
@Override
public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return typeHandler.getNullableResult(rs, columnIndex);
}
@Override
public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return typeHandler.getNullableResult(cs, columnIndex);
}
}
这里也可以看出声明业务枚举父级接口模板的重要性,可以将业务枚举归类统一处理,而其他未实现父级接口模板的枚举类则采用EnumOrdinalTypeHandler
进行处理。
使自适应枚举类转换器生效
在yml文件中增加如下配置即可
mybatis:
configuration:
default-enum-type-handler: com.cube.share.handler.config.AutoEnumTypeHandler
注意,如果是在配置类中指定,要按照以下的方式进行声明
@Bean
@DependsOn("dataSource")
public SqlSessionFactory sqlSessionFactory(@Qualifier("dataSource") DataSource dataSource, MybatisProperties mybatisProperties) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*Mapper.xml"));
org.apache.ibatis.session.Configuration configuration = mybatisProperties.getConfiguration();
configuration.getTypeHandlerRegistry().setDefaultEnumTypeHandler(AutoEnumTypeHandler.class);
sqlSessionFactoryBean.setConfiguration(configuration);
return sqlSessionFactoryBean.getObject();
}
在配置类中进行指定时,有以下常见错误写法,会导致自定义枚举类转换器无法生效
@Bean
@DependsOn("dataSource")
public SqlSessionFactory sqlSessionFactory(@Qualifier("dataSource") DataSource dataSource, MybatisProperties mybatisProperties) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*Mapper.xml"));
//获取SqlSessionFactory实例
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBean.getObject();
//SqlSessionFactory实例时依据Configuration创建,由于上一步已经生成了SqlSessionFactory,虽然下面通过Configuration获取注册器设置了默认的枚举类转换器
//实际上对已经创建了的SqlSessionFactory实例并不会生效
TypeHandlerRegistry typeHandlerRegistry = sqlSessionFactory.getConfiguration().getTypeHandlerRegistry();
typeHandlerRegistry.setDefaultEnumTypeHandler(AutoEnumTypeHandler.class);
return sqlSessionFactory;
}
这种写法是在创建了SqlSessionFactory实例后再设置默认的枚举类转换器,实际上并不会生效。
测试
数据库脚本
CREATE TABLE `sys_task` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`description` varchar(1000) DEFAULT NULL,
`submit_status` tinyint NOT NULL,
`phase_status` tinyint DEFAULT NULL,
`chinese_number` char(10) DEFAULT NULL,
`ordinal_type` int NOT NULL DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
业务枚举类
package com.cube.share.handler.config;
import com.cube.share.base.templates.IEnum;
/**
* @author poker.li
* @date 2021/7/31 9:42
* <p>
* 阶段状态枚举类
*/
public enum PhaseStatus implements IEnum<Integer, String> {
/**
* 未开始
*/
NOT_BEGUN(1, "未开始"),
/**
* 进行中
*/
ONGOING(2, "进行中"),
/**
* 已结束
*/
FINISHED(3, "已结束");
private final Integer code;
private final String desc;
PhaseStatus(Integer code, String desc) {
this.code = code;
this.desc = desc;
}
@Override
public Integer getCode() {
return this.code;
}
@Override
public String getValue() {
return this.desc;
}
}
package com.cube.share.handler.config;
import com.cube.share.base.templates.IEnum;
/**
* @author cube.li
* @date 2021/7/31 22:22
* @description 大写数字枚举类
*/
public enum ChineseNumber implements IEnum<String, Integer> {
ONE("壹", 1),
TWO("贰", 2),
THREE("弎", 3);
private final String code;
private final Integer value;
ChineseNumber(String code, Integer value) {
this.code = code;
this.value = value;
}
@Override
public String getCode() {
return this.code;
}
@Override
public Integer getValue() {
return this.value;
}
}
普通枚举类
package com.cube.share.handler.config;
/**
* @author cube.li
* @date 2021/7/31 22:59
* @description 顺序枚举类
*/
public enum OrdinalType {
THREE,
ONE,
TWO
}
实体类
package com.cube.share.handler.entity;
import com.cube.share.handler.config.ChineseNumber;
import com.cube.share.handler.config.OrdinalType;
import com.cube.share.handler.config.PhaseStatus;
import com.cube.share.handler.config.SubmitStatus;
import lombok.Data;
/**
* @author poker.li
* @date 2021/7/30 15:56
* <p>
* 任务实体
*/
@Data
public class SysTask {
private Long id;
private String name;
private String description;
private SubmitStatus submitStatus;
private PhaseStatus phaseStatus;
private ChineseNumber chineseNumber;
private OrdinalType ordinalType;
}
单元测试
package com.cube.share.handler.service;
import com.cube.share.handler.config.ChineseNumber;
import com.cube.share.handler.config.PhaseStatus;
import com.cube.share.handler.config.SubmitStatus;
import com.cube.share.handler.entity.SysTask;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
/**
* @author poker.li
* @date 2021/7/30 16:27
*/
@SpringBootTest
class SysTaskServiceTest {
@Resource
SysTaskService sysTaskService;
@Test
void save() {
SysTask sysTask = new SysTask();
sysTask.setName("任务3");
sysTask.setDescription("任务三的描述");
sysTask.setSubmitStatus(SubmitStatus.SUBMITTED);
sysTask.setPhaseStatus(PhaseStatus.FINISHED);
sysTask.setChineseNumber(ChineseNumber.THREE);
sysTaskService.save(sysTask);
}
@Test
void getOne() {
SysTask sysTask = sysTaskService.getOne(11L);
System.out.println(sysTask);
}
}
微信截图_20210731235921.png
微信截图_20210731235229.png
可以看出读库入库均能正确执行,对于业务枚举类采用自定义枚举类处理器
IEnumTypeHandler
处理,对于普通枚举类采用EnumOrdinalTypeHandler
处理。
示例代码: https://gitee.com/li-cube/share/tree/master/enum-handler
网友评论