MyBatis 學習筆記(五)---MyBatis通用類型處理器的實現(xiàn)與自動注冊

概述及背景

實際項目中,我們經常要處理一些枚舉類型的數據。例如:訂單的狀態(tài)就分為已下單,已付款,已發(fā)貨,訂單完成等等很多狀態(tài),數據庫中我們一般只存儲的一個數字表示各種狀態(tài)。但是,前臺顯示的話就需要顯示名稱給用戶看,所以這中間就涉及到一個轉化。我們見過太多了在前端通過 if, else 寫死判斷的。這樣初期沒啥,后期難以擴展及維護。針對這種情況,我們思考下能不能直接將枚舉直接返回給前端,讓前端顯示時取value, 保存時傳入key?這樣的前端就不用寫一堆判斷了。

項目結構

在這里插入圖片描述
項目的結構如上圖所示,主要有7個部分,其中 1,4,7 是通過MyBatis操作數據庫所必須的。我們只做簡要分析,其余如通用的類型處理GeneralEnumHandler 和重寫TypeHandlerRegistry類將是我們重點分析的對象。

枚舉類基類


public interface BaseEnum<E extends Enum<?>, T> {
    /**
     * 真正與數據庫進行映射的值
     * @return
     */
    T getKey();

    /**
     * 顯示的信息
     * @return
     */
    String getValue();
}

枚舉類基類作為萬能的模板,主要定義了獲取key和value的方法,供子類實現(xiàn)。然后其傳入的泛型<E extends Enum<?>, T>一個是枚舉類自身,一個是枚舉的key的類型。

通用類型處理器的設計

// 所有的自定義類型處理器都需要實現(xiàn)TypeHandler或者繼承BaseTypeHandler類。
public class GeneralEnumHandler<E extends BaseEnum> extends BaseTypeHandler<E> {
    private Class<E> type;
    private E[] enums;
	   //設置參數到preparedStatement
    @Override
    public void setNonNullParameter(PreparedStatement preparedStatement, int i, E e, JdbcType jdbcType) throws SQLException {
        if (jdbcType == null) {
            preparedStatement.setObject(i, e.getKey());
        } else {
            preparedStatement.setObject(i, e.getKey(), jdbcType.TYPE_CODE);
        }
    }
   //設置參數到preparedStatement
    @Override
    public void setNonNullParameter(PreparedStatement preparedStatement, int i, E e, JdbcType jdbcType) throws SQLException {
        if (jdbcType == null) {
            preparedStatement.setObject(i, e.getKey());
        } else {
            preparedStatement.setObject(i, e.getKey(), jdbcType.TYPE_CODE);
        }
    }

    //根據ResultSet返回的key,找到其對應的enum
    @Override
    public E getNullableResult(ResultSet resultSet, String s) throws SQLException {
        if (resultSet.wasNull()) {
            return null;
        }
        Object key = resultSet.getObject(s);
        return locateEnumsStatus(key);
    }
   // 省略部分方法。
    /**
     * @param key
     * @return
     */
    private E locateEnumsStatus(Object key) {
        if (key instanceof Integer) {
            for (E anEnum : enums) {
                if (anEnum.getKey() == key) {
                    return anEnum;
                }
            }
            throw new IllegalArgumentException("未知的枚舉類型:" + key + ",請核對" + type.getSimpleName());
        }
        if (key instanceof String) {
            for (E anEnum : enums) {
                if (anEnum.getKey().equals(key)) {
                    return anEnum;
                }
            }
            throw new IllegalArgumentException("未知的枚舉類型:" + key + ",請核對" + type.getSimpleName());
        }
        throw new IllegalArgumentException("未知的枚舉類型:" + key + ",請核對" + type.getSimpleName());
    }

}

如上處理后,我們就可以在配置文件或者映射文件中配置使用通用的處理器了。
我們只需要在配置文件中配置(1-1配置)

    <typeHandlers>
        <typeHandler handler="com.jay.chapter3.handler.GeneralEnumHandler" javaType="com.jay.chapter3.enums.SexEnum"/>
    </typeHandlers>

或者在映射文件中配置

  <result property="sexEnum" column="sex"
                typeHandler="com.jay.chapter3.Handler.GeneralEnumHandler"/>

需要注意的是要在用到枚舉類的實體中引入枚舉類對象。如下,

public class Student {
	    /**
     * 性別
     */
    private SexEnum sexEnum;
	//省略getter,setter方法
}

如上,定義一個通用的類型處理器來處理枚舉還是比較方便的。只是美中不足的是,如果我們有很多枚舉類,那么我們就需要在配置文件中定義很多(1-1配置),著實有點繁瑣。那么我思考下如何減少配置呢?哈哈,答案就是對枚舉類實現(xiàn)自動掃描。但是,MyBatis 的框架默認是不支持的。下面我們就來看看源碼。

TypeHandlerRegistry源碼

   public void register(Class<?> typeHandlerClass) {
    boolean mappedTypeFound = false;
    MappedTypes mappedTypes = typeHandlerClass.getAnnotation(MappedTypes.class);
    if (mappedTypes != null) {
      for (Class<?> javaTypeClass : mappedTypes.value()) {
        register(javaTypeClass, typeHandlerClass);
        mappedTypeFound = true;
      }
    }
    if (!mappedTypeFound) {
      register(getInstance(null, typeHandlerClass));
    }
  }

如上,我們MyBatis框架是通過register(Class<?> typeHandlerClass)將類型處理器注冊到注冊機中。但是,我們也知道,MappedTypes 元數據只支持配置一個個具體的Class對象,這顯然不符合我們的需求。那么我們的重寫點就從MappedTypes元數據開始,使其可以支持包名配置,然后,在通過包掃描其下面的所有枚舉類,循環(huán)注入即可。

重寫MappedTypes元數據


@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MappedTypes {

    Class<?>[] value() default {};
	
	// 增加包名配置
    String[] basePackage() default {};
}

需要注意的是重寫框架的類,包名路徑要與框架保持完全一致。 然后在通用類型處理器GeneralEnumHandler中類名上添加如下注解即可

// 該包名是枚舉類下的包名路徑
@MappedTypes(basePackage = {"com.jay.chapter3.enums"})

接著我們來重寫下TypeHandlerRegistry的register方法。

重寫TypeHandlerRegistry

  public void register(Class<?> typeHandlerClass) {
    boolean mappedTypeFound = false;
    MappedTypes mappedTypes = typeHandlerClass.getAnnotation(MappedTypes.class);
    if (mappedTypes != null) {
      if (mappedTypes.value().length > 0) {
        for (Class<?> javaTypeClass : mappedTypes.value()) {
          register(javaTypeClass, typeHandlerClass);
          mappedTypeFound = true;
        }
      }
      if (mappedTypes.basePackage().length > 0) {
        for (String packageName : mappedTypes.basePackage()) {
//           掃描并注冊包下所有繼承于superType的類型
          ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
          resolverUtil.find(new ResolverUtil.IsA(Object.class), packageName);
   	    //獲取該包下所有滿足條件的class對象
        Set<Class<? extends Class<?>>> mTypes = resolverUtil.getClasses();
          for (Class<? extends Class<?>> javaTypeClass : mTypes) {
           	// 注冊枚舉類以及其所使用的類型處理器
		   register(javaTypeClass,typeHandlerClass);
            mappedTypeFound = true;
          }
        }
      }
    }
    if (!mappedTypeFound) {
      register(getInstance(null, typeHandlerClass));
    }
  }

如上,包掃描那個分支的主要流程有三個:

  1. 掃描出包下所有繼承于superType(一般是Object.class)類型的class 對象
  2. 獲取該包下所有滿足條件的class對象
  3. 循環(huán)注冊枚舉類以及其所使用的類型處理器。
    重寫完成之后,接著我們來看看文件中的配置:
   <typeHandlers>
        <package name="com.jay.chapter3.handler"/>
    </typeHandlers>

下面我們就來測試一下:測試代碼如下:

  @Test
    public void testSelectId() {
        String resource = "chapter3/mybatis-cfg.xml";
        try {
            InputStream inputStream = Resources.getResourceAsStream(resource);
            SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
            SqlSession session = factory.openSession();
            Student3Mapper mapper = session.getMapper(Student3Mapper.class);
            Student student = mapper.selectStudentById(1);
            System.out.println("------->student={}"+student);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

測試結果如下:
在這里插入圖片描述

總結

本文通過一個小小的demo 示范了如果如何自定義類型處理器,然后,通過重寫MyBatis中的MappedTypes和TypeHandlerRegistry兩個類,實現(xiàn)了對枚舉類所在包的掃描已經注冊。希望對讀者朋友們有所幫助。

源代碼

https://github.com/XWxiaowei/MyBatisLearn/tree/master/mybatisDemo



作者:碼農飛哥
微信公眾號:碼農飛哥