背景
在做之前项目的时候,里面充斥很多不明的变量,一般来说状态,标志等等属性都需要使用Int
或者固定字符串来标识,比如0
代表可用,1
代表禁用,或者是可用
,不可用
,随着人员的增加,萝卜酸菜各有所爱,有些人可能会使用1
代表可用,0
代表不可用。还有的人不喜欢使用0
,直接用1
,2
来代替。使用字符串就更加坑爹了,比如你使用可用
,不可用
,他使用可用
,禁用
。虽然知道你要表达的意思,但是给前端人员的时候就十分难受了,难道要写n
种if else
,这无疑来说是一种灾难。所以我们需要制定一个好点的方案,讲其统一进行管理,比较简单是直接写个constant
接口,将所需要的规范的信息全部放入里面。另一种比较规范的做法是定义枚举
,这就是接下来我们所要展开叙述的。
如何进行扩展
最为一款优秀的ORM
框架,类型转换是不可或缺的核心组成部分,既然被称之为对象关系映射
,那就一定会有对象属性与数据库表字段进行映射的手段,所以我们需要查看源码寻找类型映射的部分。
打开源码包最终发现如下有一个叫type
的包:
这里面包含了大多数基本类型的处理类,大多数都是TypeHandler
为后缀的,这无疑就是我们需要的类了。
里面好像包含了Enum
的处理类型,一个是org.apache.ibatis.type.EnumOrdinalTypeHandler
,另外一个是org.apache.ibatis.type.EnumTypeHandler
。它们有什么作用呢?
我们看下源码:
public class EnumTypeHandler<E extends Enum<E>> extends BaseTypeHandler<E> {
private final Class<E> type;
public EnumTypeHandler(Class<E> type) {
if (type == null) {
throw new IllegalArgumentException("Type argument cannot be null");
}
this.type = type;
}
@Override
public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException {
if (jdbcType == null) {
ps.setString(i, parameter.name());
} else {
ps.setObject(i, parameter.name(), jdbcType.TYPE_CODE); // see r3589
}
}
@Override
public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
String s = rs.getString(columnName);
return s == null ? null : Enum.valueOf(type, s);
}
@Override
public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String s = rs.getString(columnIndex);
return s == null ? null : Enum.valueOf(type, s);
}
@Override
public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String s = cs.getString(columnIndex);
return s == null ? null : Enum.valueOf(type, s);
}
}
注意到:setNonNullParameter
,包装我们的PreparedStatement
进行SQL
的插值操作。看到这里,我们发现这个默认的实现,会引用enum
的name
,也就是enum
的toString
,这显然不是我们所需要的,另一个org.apache.ibatis.type.EnumOrdinalTypeHandler
也不是我们所需要的,它只能处理Int,String
这两种类型,如果是复杂的类型,比如:
DELETE(9, "删除");
这样就不支持了,所以我们需要自定义实现。
可以通过观察源码的其他类型,发现全部都是继承BaseTypeHandler
这个类。
我们可以看下这个类中有些什么:
public abstract class BaseTypeHandler<T> extends TypeReference<T> implements TypeHandler<T> {
protected Configuration configuration;
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 {
T result;
try {
result = getNullableResult(rs, columnName);
} catch (Exception e) {
throw new ResultMapException("Error attempting to get column '" + columnName + "' from result set. Cause: " + e, e);
}
if (rs.wasNull()) {
return null;
} else {
return result;
}
}
@Override
public T getResult(ResultSet rs, int columnIndex) throws SQLException {
T result;
try {
result = getNullableResult(rs, columnIndex);
} catch (Exception e) {
throw new ResultMapException("Error attempting to get column #" + columnIndex+ " from result set. Cause: " + e, e);
}
if (rs.wasNull()) {
return null;
} else {
return result;
}
}
@Override
public T getResult(CallableStatement cs, int columnIndex) throws SQLException {
T result;
try {
result = getNullableResult(cs, columnIndex);
} catch (Exception e) {
throw new ResultMapException("Error attempting to get column #" + columnIndex+ " from callable statement. Cause: " + e, e);
}
if (cs.wasNull()) {
return null;
} else {
return result;
}
}
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;
}
存在一个configuration
全局配置属性对象,进行了一些非空的校验,实际完成功能的是下面的子类。
如何实现
有了上面的分析,我们就很好办的了,我们就仿照EnumTypeHandler
来写一个,可以直接"照搬"
过来。但是我们得考虑一下,我们需要如何做到通用,也就是我们写完一个枚举就可以实现自动的映射,而不需要重复TypeHandler
。
通过封装、继承、多态
的特性。我们可以定义一套接口,让需要定义的枚举类实现它,这样我们就可以根据类型判断,是否超类是否为该接口,为什么不是直接通过接口来扫描实现类呢,因为JDK没有这样的实现,根据父类获取所有子类。
1. 定义枚举接口
public interface BaseEnum<E extends Enum<E>, T> {
//接口实现类装载容器,方便快速获取全部子类,所有实现子类必须使用静态块将其注册进来
Set<Class<?>> subClass = Sets.newConcurrentHashSet();
/**
* 真正与数据库进行映射的值
*
* @return
*/
T getValue();
/**
* 显示的信息
*
* @return
*/
String getDisplayName();
}
2. 实现一个State枚举
public enum State implements BaseEnum<State, Integer> {
/**
* 正常状态
*/
NORMAL(0, "正常"),
/**
* 删除状态
*/
DELETE(9, "删除");
private final int value;
private final String description;
static {
subClass.add(State.class);
}
State(int value, String description) {
this.value = value;
this.description = description;
}
@Override
public Integer getValue() {
return value;
}
@Override
public String getDisplayName() {
return description;
}
}
3. 实现GeneralTypeHandler
处理类
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import java.sql.*;
@Slf4j
public final class GeneralTypeHandler<E extends BaseEnum> extends BaseTypeHandler<E> {
private Class<E> type;
private E[] enums;
public GeneralTypeHandler(Class<E> type) {
if (type == null) {
throw new IllegalArgumentException("Type argument cannot be null");
}
this.type = type;
this.enums = this.type.getEnumConstants();
if (this.enums == null) {
throw new IllegalArgumentException(type.getSimpleName() + " does not represent an enum type.");
}
}
@Override
public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException {
//BaseTypeHandler 进行非空校验
log.debug("index : {}, parameter : {},jdbcType : {} ", i, parameter.getValue(), jdbcType);
if (jdbcType == null) {
ps.setObject(i, parameter.getValue());
} else {
ps.setObject(i, parameter.getValue(), jdbcType.TYPE_CODE);
}
}
@Override
public E getNullableResult(ResultSet rs, String columnName) throws SQLException {
Object code = rs.getObject(columnName);
if (rs.wasNull()) {
return null;
}
return getEnmByCode(code);
}
@Override
public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
Object code = rs.getObject(columnIndex);
if (rs.wasNull()) {
return null;
}
return getEnmByCode(code);
}
@Override
public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
Object code = cs.getObject(columnIndex);
if (cs.wasNull()) {
return null;
}
return getEnmByCode(code);
}
private E getEnmByCode(Object code) {
if (code == null) {
throw new NullPointerException("the result code is null " + code);
}
if (code instanceof Integer) {
for (E e : enums) {
if (e.getValue() == code) {
return e;
}
}
throw new IllegalArgumentException("Unknown enumeration type , please check the enumeration code : " + code);
}
if (code instanceof String) {
for (E e : enums) {
if (code.equals(e.getValue())) {
return e;
}
}
throw new IllegalArgumentException("Unknown enumeration type , please check the enumeration code : " + code);
}
throw new IllegalArgumentException("Unknown enumeration type , please check the enumeration code : " + code);
}
}
现在我们基本组件已经实现完全了,剩下就是需要将GeneralTypeHandler
与Mybatis
进行关联起来,所以接下来我们分析下,TypeHandler
的处理流程
TypeHandler的注册
注册流程图:
register
是依赖TypeHandlerRegistry
这个对象的,所以我们需要取得这个对象,万幸的是,TypeHandlerRegistry
是属于Configuration
的一位成员变量而存在,那么我们只需要获取到Configuration
就可以进行注册啦。前面我们不是讲到BaseTypeHandler
中就存在Configuration
的引用么,所以我们可以将TypeHandlerRegistry
,在构造器中注入到Configuration
中。但是我们这里结合SpringBoot
,所以我们需要优雅的处理一下,使用SpringBoot
的方式进行注册。
SpringBoot
整合Mybatis
的时候为我们提供了一个自定义的Configuration
回调,我们只需要实现一个接口,就可以获取Configuration
对象,在它的基础上进行添砖加瓦。
接口长这样:
public interface ConfigurationCustomizer {
/**
* Customize the given a {@link Configuration} object.
* @param configuration the configuration object to customize
*/
void customize(Configuration configuration);
}
实现自定义ConfigurationCustomizer
@Component
@Slf4j
public class RegisterEnumHandlerConfig implements ConfigurationCustomizer {
@Override
public void customize(Configuration configuration) {
log.debug("ConfigurationCustomizer init....");
TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
try {
final List<Class<?>> allAssignedClass = ClassUtil.getAllAssignedClass(BaseEnum.class);
allAssignedClass.forEach((clazz) -> typeHandlerRegistry.register(clazz, GeneralTypeHandler.class));
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
ClassUtil
的作用是获取BaseEnum
的全部子类,只有将所有的子类与GeneralTypeHandler
进行了映射,这样我们才能达到自动识别枚举的效果。
ClassUtil 的实现
public class ClassUtil {
/**
* 获取当前类的所有实现子类
*
* @param superClass
* @return
* @throws ClassNotFoundException
*/
public static List<Class<?>> getAllAssignedClass(Class<?> superClass) throws ClassNotFoundException {
List<Class<?>> classes = new ArrayList<>();
for (Class<?> c : getClasses(superClass)) {
if (superClass.isAssignableFrom(c) && !superClass.equals(c)) {
classes.add(c);
}
}
return classes;
}
public static List<Class<?>> getClasses(Class<?> cls) throws ClassNotFoundException {
String pk = cls.getPackage().getName();
String path = pk.replace(".", "/");
ClassLoader classloader = Thread.currentThread().getContextClassLoader();
URL url = classloader.getResource(path);
return getClasses(new File(url.getFile()), pk);
}
private static List<Class<?>> getClasses(File dir, String pk) throws ClassNotFoundException {
List<Class<?>> classes = new ArrayList<>();
if (!dir.exists()) {
return classes;
}
for (File file : dir.listFiles()) {
if (file.isDirectory()) {
classes.addAll(getClasses(file, pk + "." + file.getName()));
}
String fileName = file.getName();
if (fileName.endsWith(".class")) {
classes.add(Class.forName(pk + "." + fileName.substring(0, fileName.length() - 6)));
}
}
return classes;
}
public static void main(String[] args) throws ClassNotFoundException {
for (Class<?> c : getAllAssignedClass(BaseEnum.class)) {
System.out.println(c);
}
}
}
另一种获取子类的方式:
可以在定义的接口中设置一个集合类,没当子类实现的接口的时候,通过子类的静态代码块,将该类的Class注入进去
public interface BaseEnum<E extends Enum<E>, T> {
//接口实现类装载容器,方便快速获取全部子类,所有实现子类必须使用静态块将其注册进来
Set<Class<?>> subClass = Sets.newConcurrentHashSet();
}
public enum State implements BaseEnum<State, Integer> {
static {
subClass.add(State.class);
}
}