一、案例分析
在日常开发中,有不少对日期类型的操作。比如订单时间、付款时间等,通常这一类数据在
数据库以datetime类型保存。如果需要在
页面上展示此值,在Java中以什么类型接收它呢?在不执行任何二次操作的情况下: 用java.util.Date接收,在
页面展示的就是Tue Oct 16 16:05:13 CST 2018。 用
java.lang.String接收,在
页面展示的就是2018-10-16 16:10:47.0。显然,我们不能
显示第一种。第二种似乎可行,但大部分情况下不能出现毫秒数。当然了,不管哪种方式,在
显示的时候format一下当然是可行的。有没有更好的方式呢?
二、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 类型,那么就
调用类型处理器类型处理器。
@H_
404_85@三、
自定义处理器
基于上面这个逻辑,我们可以
增加一种处理器来处理我们开头所描述的问题。我们可以在Java中,以String类型接收
数据库的DateTime类型数据。因为现在的接口以restful风格居多,用String类型方便传输。 最后的毫秒数通过
自定义的处理器统一
截取去除即可。
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接收
数据库的时间类型数据了。
四、源码分析
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);
}
}
五、总结
自定义处理器的应用场景很广泛,比如对某些敏感字段加密、状态值的转换(正常、注销、 已付款、未发货)等,可以考虑用它来做。
六、后续
在写完这篇
文章后,在另外一台电脑做测试的时候,发现尽管没有对时间类型做处理,但也不会出现.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
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版本
解决。如果不能动版本,可以考虑
自定义的类型处理器。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 [email protected] 举报,一经查实,本站将立刻删除。