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版本解决。若是不能动版本,再考虑自定义的类型处理器。

相关文章
相关标签/搜索