1. 程式人生 > >SpringBoot整合Mybatis實現自動轉換列舉型別

SpringBoot整合Mybatis實現自動轉換列舉型別

背景

  在做之前專案的時候,裡面充斥很多不明的變數,一般來說狀態,標誌等等屬性都需要使用Int或者固定字串來標識,比如0代表可用,1代表禁用,或者是可用不可用,隨著人員的增加,蘿蔔酸菜各有所愛,有些人可能會使用1代表可用,0代表不可用。還有的人不喜歡使用0,直接用12來代替。使用字串就更加坑爹了,比如你使用可用不可用,他使用可用禁用。雖然知道你要表達的意思,但是給前端人員的時候就十分難受了,難道要寫nif 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的插值操作。看到這裡,我們發現這個預設的實現,會引用enumname,也就是enumtoString,這顯然不是我們所需要的,另一個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);
    }
}

現在我們基本元件已經實現完全了,剩下就是需要將GeneralTypeHandlerMybatis進行關聯起來,所以接下來我們分析下,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.