Mybatis源碼分析(三)經過實例來看typeHandlers

1、案例分析

在平常開發中,咱們確定有對日期類型的操做。好比訂單時間、付款時間等,一般這一類數據在數據庫以datetime類型保存。若是須要在頁面上展現此值,在Java中以什麼類型接收它呢?java

在不執行任何二次操做的狀況下: 用java.util.Date接收,在頁面展現的就是Tue Oct 16 16:05:13 CST 2018。 用java.lang.String接收,在頁面展現的就是2018-10-16 16:10:47.0mysql

顯然,咱們不能顯示第一種。第二種彷佛可行,但大部分狀況下不能出現毫秒數。固然了,無論哪一種方式,在顯示的時候format一下固然是可行的。有沒有更好的方式呢?sql

2、typeHandlers

不管是 MyBatis 在預處理語句(PreparedStatement)中設置一個參數時,仍是從結果集中取出一個值時, 都會用類型處理器將獲取的值以合適的方式轉換成 Java 類型。 在數據庫中,datetime和timestamp類型含義是同樣的,不過timestamp存儲空間小, 因此它表示的時間範圍也更小。 下面來看幾個Mybatis默認的時間類型處理器。數據庫

JDBC 類型 Java 類型 類型處理器
DATE java.util.Date DateOnlyTypeHandler
DATE java.sql.Date SqlDateTypeHandler
DATE java.time.LocalDate LocalDateTypeHandler
DATE java.time.LocalTime LocalTimeTypeHandler
TIMESTAMP java.util.Date DateTypeHandler
TIMESTAMP java.time.Instant InstantTypeHandler
TIMESTAMP java.time.LocalDateTime LocalDateTimeTypeHandler
TIMESTAMP java.sql.Timestamp SqlTimestampTypeHandler

它是什麼意思呢?若是數據庫字段類型爲JDBC 類型,同時Java字段的類型爲Java 類型,那麼就調用類型處理器類型處理器bash

3、自定義處理器

基於上面這個邏輯,咱們能夠增長一種處理器來處理咱們開頭所描述的問題。咱們能夠在Java中,以String類型接收數據庫的DateTime類型數據。由於如今的接口以restful風格居多,用String類型方便傳輸。 最後的毫秒數經過自定義的處理器統一截取去除便可。restful

JDBC 類型 Java 類型 類型處理器
TIMESTAMP java.lang.String CustomTypeHandler
<property name="typeHandlers">
	<array>
		<bean class="com.viewscenes.netsupervisor.util.CustomTypeHandler"></bean>
	</array>
</property>
複製代碼

@MappedJdbcTypes註解表示JDBC的類型,@MappedTypes表示Java屬性的類型。app

@MappedJdbcTypes({ JdbcType.TIMESTAMP })
@MappedTypes({ String.class })
public class CustomTypeHandler extends BaseTypeHandler<String>{	
	@Override
	public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType)
			throws SQLException {
		ps.setString(i, parameter);
	}
	@Override
	public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
		return substring(rs.getString(columnName));
	}
	@Override
	public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
		return rs.getString(columnIndex);
	}
	@Override
	public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
		return cs.getString(columnIndex);
	}
	private String substring(String value) {
		if (!"".endsWith(value) && value != null) {
			return value.substring(0, value.length() - 2);
		}
		return value;
	}
}
複製代碼

經過以上方式,咱們就能夠放心的在Java中以String接收數據庫的時間類型數據了。ide

4、源碼分析

一、註冊

public final class TypeHandlerRegistry {
	//typeHandler爲當前自定義類型處理器
	public <T> void register(TypeHandler<T> typeHandler) {
		boolean mappedTypeFound = false;
		//mappedTypes即String
		MappedTypes mappedTypes = typeHandler.getClass().getAnnotation(MappedTypes.class);
		if (mappedTypes != null) {
			for (Class<?> handledType : mappedTypes.value()) {
				register(handledType, typeHandler);
			}
		}
	}
}
複製代碼
public final class TypeHandlerRegistry {
	private <T> void register(Type javaType, TypeHandler<? extends T> typeHandler) {
		//JDBC的類型,即TIMESTAMP
		MappedJdbcTypes mappedJdbcTypes = typeHandler.getClass().
				getAnnotation(MappedJdbcTypes.class);
		if (mappedJdbcTypes != null) {
			for (JdbcType handledJdbcType : mappedJdbcTypes.value()) {
				//TYPE_HANDLER_MAP是Java類型中的默認處理器。
				//以String爲例,它默承認以處理VARCHAR、CHAR、NVARCHAR、CLOB、NCLOB、NULL
				Map<JdbcType, TypeHandler<?>> map = TYPE_HANDLER_MAP.get(javaType);
				//給String添加一種處理器爲typeHandler
				map.put(jdbcType, typeHandler);
				//註冊處理器實例
				ALL_TYPE_HANDLERS_MAP.put(typeHandler.getClass(), typeHandler);
			}
		}
	}
}
複製代碼

二、調用

註冊完畢以後,它在什麼地方生效呢?關鍵在於可否能夠找到這個處理器。看完上面的註冊過程,查找其實很簡單。先從TYPE_HANDLER_MAP根據JavaType,獲取String類型的所有處理器,再從中過濾出JDBC類型爲TIMESTAMP的便可。源碼分析

private <T> TypeHandler<T> getTypeHandler(Type type, JdbcType jdbcType) {
	//根據JavaType獲取String類型的所有處理器
	Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = getJdbcHandlerMap(type);
	TypeHandler<?> handler = null;
	if (jdbcHandlerMap != null) {
		//再根據jdbcType獲取到TIMESTAMP的處理器
		handler = jdbcHandlerMap.get(jdbcType);
	}
	return (TypeHandler<T>) handler;
}
複製代碼

拿到自定義的處理器,咱們本身就隨便搞嘍~測試

不過,在Mybatis-3.2.7版本中,比較坑。在調用getTypeHandler方法時,它並無傳jdbcType這個參數,因此這個參數默認爲NULL了。 那麼,在執行jdbcHandlerMap.get(jdbcType)的時候,會找不到自定義的處理器,而是找到了NULL的處理器,即StringHandler。案發現場在下面:

public class ResultSetWrapper {
	public TypeHandler<?> getTypeHandler(Class<?> propertyType, String columnName) {
		//3.4.6
		JdbcType jdbcType = getJdbcType(columnName);
		handler = typeHandlerRegistry.getTypeHandler(propertyType, jdbcType);
		//3.2.7
		handler = typeHandlerRegistry.getTypeHandler(propertyType);
	}
}
複製代碼

5、總結

自定義處理器的應用場景很普遍,好比對某些敏感字段加密、狀態值的轉換(正常、註銷、 已付款、未發貨)等。回顧一下你的項目中有哪些地方實現的不太理想,能夠考慮用它來作。

6、後續

在筆者寫完這篇文章後,在另一臺電腦作測試的時候,發現儘管沒有對時間類型作處理,但也不會出現.0的問題。這使我睡覺都沒安穩。。。難道本身認知有誤,文章寫錯了?筆者決定先拋開Mybatis,用最原始的JDBC作測試。

public static void main(String[] args) throws Exception {
	Connection conn = getConnection();
	Statement stat = conn.createStatement();
	String sql = "select * from user";
	ResultSet rs = stat.executeQuery(sql);
	while(rs.next()){
		String username = rs.getString("username");
		String createtime = rs.getString("createtime");
		System.out.print("姓名: " + username);
		System.out.print(" 建立時間: " + createtime);
		System.out.print("\n");
	}
}
複製代碼

結果讓我很意外,用原始的JDBC查詢數據,並無任何其餘操做,也沒有.0的問題。

姓名: 關小羽	建立時間: 2018-10-15 17:04:11
姓名: 小露娜	建立時間: 2018-10-15 17:10:46
姓名: 亞麻瑟	建立時間: 2018-10-15 17:10:46
姓名: 小魯班	建立時間: 2018-10-16 16:10:47
複製代碼

上面的代碼量很小,顯然問題出在ResultSet對象上。經過跟蹤源碼,最後筆者發現兩臺機器的mysql-connector-java版本不同。一個是5.1.31,一個是6.0.6。咱們把版本換成5.1.31,執行上面的main方法再看結果。

姓名: 關小羽	建立時間: 2018-10-15 17:04:11.0
姓名: 小露娜	建立時間: 2018-10-15 17:10:46.0
姓名: 亞麻瑟	建立時間: 2018-10-15 17:10:46.0
姓名: 小魯班	建立時間: 2018-10-16 16:10:47.0
複製代碼

好了,讓咱們看看它們的差異在哪裏吧。其實就是由於5.1.31多作了一步操做,它針對時間類型的數據又處理了一次,致使問題產生。

5.1.31

package com.mysql.jdbc;
public class ResultSetImpl implements ResultSetInternalMethods {
	protected String getStringInternal(int columnIndex, boolean checkDateTypes)
		// JDBC is 1-based, Java is not !?
		int internalColumnIndex = columnIndex - 1;
		Field metadata = this.fields[internalColumnIndex];		
		String stringVal = null;	
		String encoding = metadata.getCharacterSet();
		//stringVal爲已經從數據庫取到的值2018-10-16 16:10:47
		stringVal = this.thisRow.getString(internalColumnIndex, encoding, this.connection);
		
		// Handles timezone conversion and zero-date behavior
		//Mysql針對時間類型又作了一次處理
		if (checkDateTypes && !this.connection.getNoDatetimeStringSync()) {
			switch (metadata.getSQLType()) {
			case Types.TIME:
				......略
			case Types.DATE:
				......略
			case Types.TIMESTAMP:
				//數據庫的DateTime類型會走到這裏
				//MySQL把它又轉成了Timestamp類型,  .0的問題從這裏產生
				Timestamp ts = getTimestampFromString(columnIndex,
						null, stringVal, this.getDefaultTimeZone(), false);
				return ts.toString();
			default:
				break;
			}
		}
		return stringVal;
	}
}
複製代碼

6.0.6

package com.mysql.cj.jdbc.result;

public class ResultSetImpl extends MysqlaResultset 
				implements ResultSetInternalMethods, WarningListener {
	
	public String getString(int columnIndex) throws SQLException {
        
        Field f = this.columnDefinition.getFields()[columnIndex - 1];
        ValueFactory<String> vf = new StringValueFactory(f.getEncoding());
        // return YEAR values as Dates if necessary
        if (f.getMysqlTypeId() == MysqlaConstants.FIELD_TYPE_YEAR && this.yearIsDateType) {
            vf = new YearToDateValueFactory<>(vf);
        }
        String stringVal = this.thisRow.getValue(columnIndex - 1, vf);

        return stringVal;
    }
}
複製代碼

若是你們項目裏面有.0問題產生,能夠經過升級mysql-java版本解決。若是不能動版本,再考慮自定義的類型處理器。

相關文章
相關標籤/搜索