SpringBoot整合Mybatis實現自動轉換列舉型別
背景
在做之前專案的時候,裡面充斥很多不明的變數,一般來說狀態,標誌等等屬性都需要使用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.