背景
在做之前项目的时候,里面充斥很多不明的变量,一般来说状态,标志等等属性都需要使用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);
}
}