一鬓椭、案例分析
在日常開發(fā)中,我們肯定有對日期類型的操作关划。比如訂單時間小染、付款時間等,通常這一類數(shù)據(jù)在數(shù)據(jù)庫以datetime
類型保存贮折。如果需要在頁面上展示此值裤翩,在Java中以什么類型接收它呢?
在不執(zhí)行任何二次操作的情況下:
用java.util.Date
接收调榄,在頁面展示的就是Tue Oct 16 16:05:13 CST 2018
踊赠。
用java.lang.String
接收,在頁面展示的就是2018-10-16 16:10:47.0
每庆。
顯然筐带,我們不能顯示第一種。第二種似乎可行扣孟,但大部分情況下不能出現(xiàn)毫秒數(shù)烫堤。當然了,不管哪種方式,在顯示的時候format一下當然是可行的鸽斟。有沒有更好的方式呢拔创?
二、typeHandlers
無論是 MyBatis 在預(yù)處理語句(PreparedStatement)中設(shè)置一個參數(shù)時富蓄,還是從結(jié)果集中取出一個值時剩燥, 都會用類型處理器將獲取的值以合適的方式轉(zhuǎn)換成 Java 類型。
在數(shù)據(jù)庫中立倍,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 |
它是什么意思呢?如果數(shù)據(jù)庫字段類型為JDBC 類型
寝志,同時Java字段的類型為Java 類型
娇斑,那么就調(diào)用類型處理器類型處理器
。
三材部、自定義處理器
基于上面這個邏輯毫缆,我們可以增加一種處理器來處理我們開頭所描述的問題。我們可以在Java中乐导,以String類型接收數(shù)據(jù)庫的DateTime類型數(shù)據(jù)苦丁。因為現(xiàn)在的接口以restful風格居多,用String類型方便傳輸物臂。
最后的毫秒數(shù)通過自定義的處理器統(tǒng)一截取去除即可旺拉。
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屬性的類型鹦聪。
@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接收數(shù)據(jù)庫的時間類型數(shù)據(jù)了。
四泽本、源碼分析
1淘太、注冊
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);
}
}
}
}
2挎扰、調(diào)用
注冊完畢之后翠订,它在什么地方生效呢?關(guān)鍵在于能否可以找到這個處理器遵倦【〕看完上面的注冊過程,查找其實很簡單梧躺。先從TYPE_HANDLER_MAP根據(jù)JavaType似谁,獲取String類型的全部處理器,再從中過濾出JDBC類型為TIMESTAMP的即可掠哥。
private <T> TypeHandler<T> getTypeHandler(Type type, JdbcType jdbcType) {
//根據(jù)JavaType獲取String類型的全部處理器
Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = getJdbcHandlerMap(type);
TypeHandler<?> handler = null;
if (jdbcHandlerMap != null) {
//再根據(jù)jdbcType獲取到TIMESTAMP的處理器
handler = jdbcHandlerMap.get(jdbcType);
}
return (TypeHandler<T>) handler;
}
拿到自定義的處理器巩踏,我們自己就隨便搞嘍~
不過,在Mybatis-3.2.7版本中续搀,比較坑塞琼。在調(diào)用getTypeHandler方法時,它并沒有傳jdbcType這個參數(shù)目代,所以這個參數(shù)默認為NULL了屈梁。
那么,在執(zhí)行jdbcHandlerMap.get(jdbcType)
的時候榛了,會找不到自定義的處理器,而是找到了NULL的處理器煞抬,即StringHandler霜大。案發(fā)現(xiàn)場在下面:
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);
}
}
五、總結(jié)
自定義處理器的應(yīng)用場景很廣泛革答,比如對某些敏感字段加密战坤、狀態(tài)值的轉(zhuǎn)換(正常、注銷残拐、 已付款途茫、未發(fā)貨)等∠常回顧一下你的項目中有哪些地方實現(xiàn)的不太理想囊卜,可以考慮用它來做。
六错沃、后續(xù)
在筆者寫完這篇文章后栅组,在另外一臺電腦做測試的時候,發(fā)現(xiàn)盡管沒有對時間類型做處理枢析,但也不會出現(xiàn).0的問題玉掸。這使我睡覺都沒安穩(wěn)。醒叁。司浪。難道自己認知有誤泊业,文章寫錯了?筆者決定先拋開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(" 創(chuàng)建時間: " + createtime);
System.out.print("\n");
}
}
結(jié)果讓我很意外,用原始的JDBC查詢數(shù)據(jù)认罩,并沒有任何其他操作箱蝠,也沒有.0的問題。
姓名: 關(guān)小羽 創(chuàng)建時間: 2018-10-15 17:04:11
姓名: 小露娜 創(chuàng)建時間: 2018-10-15 17:10:46
姓名: 亞麻瑟 創(chuàng)建時間: 2018-10-15 17:10:46
姓名: 小魯班 創(chuàng)建時間: 2018-10-16 16:10:47
上面的代碼量很小垦垂,顯然問題出在ResultSet
對象上宦搬。通過跟蹤源碼,最后筆者發(fā)現(xiàn)兩臺機器的mysql-connector-java版本不一樣劫拗。一個是5.1.31间校,一個是6.0.6。我們把版本換成5.1.31页慷,執(zhí)行上面的main方法再看結(jié)果憔足。
姓名: 關(guān)小羽 創(chuàng)建時間: 2018-10-15 17:04:11.0
姓名: 小露娜 創(chuàng)建時間: 2018-10-15 17:10:46.0
姓名: 亞麻瑟 創(chuàng)建時間: 2018-10-15 17:10:46.0
姓名: 小魯班 創(chuàng)建時間: 2018-10-16 16:10:47.0
好了,讓我們看看它們的差別在哪里吧酒繁。其實就是因為5.1.31多做了一步操作滓彰,它針對時間類型的數(shù)據(jù)又處理了一次,導致問題產(chǎn)生州袒。
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為已經(jīng)從數(shù)據(jù)庫取到的值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:
//數(shù)據(jù)庫的DateTime類型會走到這里
//MySQL把它又轉(zhuǎn)成了Timestamp類型, .0的問題從這里產(chǎn)生
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問題產(chǎn)生揭绑,可以通過升級mysql-java版本解決。如果不能動版本郎哭,再考慮自定義的類型處理器他匪。