mybatis优雅处理枚举类-通用TypeHandler
前言
经过前文问题的处理,现在对通用的TypeHandler也有一定的把握了。
思路
现在需要明确的内容是,我们要尽量接管TypeHandler的实例化过程,在实例化的时候,将枚举类型设置进TypeHandler中。
可知的是,mybatis提供了TypeHandlerRegistry,所以我们可以自行实例化我们想要的TypeHandler。
初次尝试
首先,编写通用的TypeHandler
通用TypeHandler-可正常使用
import com.will.cn.constants.ConstantEnumIFace;
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;
/**
* @author jeff
* 第一个T,表示这个TypeHandler只为实现ConstantEnumIFace的类做数据转换
* 第二个T,表示ConstantEnumIFace接口中的方法,返回的类型为T
* 第三个T,表示这个TypeHandler只为T类型数据做数据转换
*/
public class ConstantEnumsTypeHandler<T extends ConstantEnumIFace<T>> extends BaseTypeHandler<T> {
public ConstantEnumsTypeHandler(Class<T> tClass){
theEnums =tClass.getEnumConstants()[0];
}
private T theEnums;
@Override
public void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
ps.setInt(i, parameter.getValue());
}
@Override
public T getNullableResult(ResultSet rs, String columnName) throws SQLException {
int i =rs.getInt(columnName);
if (0==i && rs.wasNull()) return null;
return getValue(i);
}
@Override
public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
int i =rs.getInt(columnIndex);
if (0==i && rs.wasNull()) return null;
return getValue(i);
}
@Override
public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
int i =cs.getInt(columnIndex);
if (0==i && cs.wasNull()) return null;
return getValue(i);
}
T getValue(int value){
return theEnums.theEnum(value);
}
}
注册相应的TypeHandler
/**
* @author jeff
* @time 2023/4/30 22:19
*/
@Configuration
public class MybatisConfiguration implements ApplicationContextAware {
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
//从spring容器获取sqlSessionFactory
SqlSessionFactory sqlSessionFactory = applicationContext.getBean(SqlSessionFactory.class);
//获取typeHandler注册器
TypeHandlerRegistry typeHandlerRegistry = sqlSessionFactory.getConfiguration().getTypeHandlerRegistry();
//注册typeHandler
typeHandlerRegistry.register(IsDefault.class, new ConstantEnumsTypeHandler(IsDefault.class));
typeHandlerRegistry.register(UserStatus.class, new ConstantEnumsTypeHandler(UserStatus.class));
}
}
测试
可以看到数据已经正常封装了
但这还不够简化,因为每添加一个枚举常量,我们就要去register一个TypeHandler,显然不够通用化。
这时就可以考虑扫包获取枚举实体类,然后使用Class.forName进行反射,获取类对象,再进行register
注册TypeHandler最终版
可以在springboot中加入配置项,配置枚举常量的包名
再在注册时,获取包下所有的class进行反射,解放双手
import com.will.cn.constants.ConstantEnumIFace;
import com.will.cn.handler.ConstantEnumsTypeHandler;
import lombok.Setter;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternUtils;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import java.io.IOException;
/**
* @author jeff
* @time 2023/4/30 22:19
*/
@Configuration
@Setter
public class ConstantEnumConfiguration implements ApplicationContextAware, ResourceLoaderAware {
@Value("${custom.constants.enum.path}")
private String enumPath;
private ResourceLoader resourceLoader;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
//从spring容器获取sqlSessionFactory
SqlSessionFactory sqlSessionFactory = applicationContext.getBean(SqlSessionFactory.class);
//获取typeHandler注册器
TypeHandlerRegistry typeHandlerRegistry = sqlSessionFactory.getConfiguration().getTypeHandlerRegistry();
//注册typeHandler
try {
ResourcePatternResolver resolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader);
MetadataReaderFactory metaReader = new CachingMetadataReaderFactory(resourceLoader);
String s = "classpath*:" + enumPath.replace(".", "/") + "/*.class";
Resource[] resources = resolver.getResources(s);
for (Resource r : resources) {
MetadataReader reader = metaReader.getMetadataReader(r);
Class<?> aClass = Class.forName(reader.getClassMetadata().getClassName());
if (ConstantEnumIFace.class.isAssignableFrom(aClass)) {
typeHandlerRegistry.register(aClass, ConstantEnumsTypeHandler.class);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
}
一定要注意的点!
注册TypeHandler的配置类,一定要确保在mybatis完全初始化之前加载!!!
因为如果让Mybatis完成扫包的话,它会对实体类做映射,而扫描到实体中的枚举类型时,会找有无相应的TypeHandler,无,则会映射到默认的EnumTypeHandler上。
后续如果在preparedSql中使用如 #{status} 取数据时,就会去找实体类映射的TypeHandler,而不会去找我们后面注册的TypeHandler!
如果我们先它一步注册TypeHandler,Mybatis在做映射时找到了对应的TypeHandler,就不会设置其为EnumTypeHandler
解决方案
提高配置类优先级
将配置类的位置放在启动类的下一层中,如:
优先加载注册TypeHandler的配置类
在启动类中注入该配置类
@Resource ConstantEnumConfiguration constantEnumConfiguration;
设置mybatis默认的EnumTypeHandler
将Mybatis 默认的处理Enum的TypeHandler为我们自定义的TypeHandler
弊端:实体类中所有的enum,都要实现我们的枚举顶级接口,不然会出现class cast异常
值得注意的点
可以看到在扫包过程中,注册TypeHandler时,直接使用的ConstantEnumsTypeHandler.class类对象,而不是自行new对象。
但是ConstantEnumsTypeHandler未提供无参构造,那为什么还能完成注册呢?
因为Mybatis在实例化TypeHandler时,会尝试获取以该类型的class对象为参数的构造器,如果有,则使用该构造器构造TypeHandler
要是Jackson实例化Deserializer的时候,也这么人性化就好了。。
解决Deserializer的问题花了我不少时间看jackson的源码呢。。
总结
到这里,关于系统中用枚举值替换所有常量的内容就结束了。
有空的话,我会考虑把这套内容,封装成starter包,开源给大家使用~