Mybatis源码分析(三)通过实例来看typeHandlers

一、案例分析

在日常开发中,我们肯定有对日期类型的操作。比如订单时间、付款时间等,通常这一类数据在数据库以 datetime类型保存。如果需要在页面上展示此值,在Java中以什么类型接收它呢?

在不执行任何二次操作的情况下: 用 java.util.Date接收,在页面展示的就是 TueOct1616:05:13CST2018。 用 java.lang.String接收,在页面展示的就是 2018-10-1616:10:47.0

显然,我们不能显示第一种。第二种似乎可行,但大部分情况下不能出现毫秒数。当然了,不管哪种方式,在显示的时候format一下当然是可行的。有没有更好的方式呢?

二、typeHandlers

无论是 MyBatis 在预处理语句(PreparedStatement)中设置一个参数时,还是从结果集中取出一个值时, 都会用类型处理器将获取的值以合适的方式转换成 Java 类型。 在数据库中,datetime和timestamp类型含义是一样的,不过timestamp存储空间小, 所以它表示的时间范围也更小。 下面来看几个Mybatis默认的时间类型处理器。

JDBC 类型Java 类型类型处理器
DATEjava.util.DateDateOnlyTypeHandler
DATEjava.sql.DateSqlDateTypeHandler
DATEjava.time.LocalDateLocalDateTypeHandler
DATEjava.time.LocalTimeLocalTimeTypeHandler
TIMESTAMPjava.util.DateDateTypeHandler
TIMESTAMPjava.time.InstantInstantTypeHandler
TIMESTAMPjava.time.LocalDateTimeLocalDateTimeTypeHandler
TIMESTAMPjava.sql.TimestampSqlTimestampTypeHandler

它是什么意思呢?如果数据库字段类型为 JDBC类型,同时Java字段的类型为 Java类型,那么就调用类型处理器 类型处理器

三、自定义处理器

基于上面这个逻辑,我们可以增加一种处理器来处理我们开头所描述的问题。我们可以在Java中,以String类型接收数据库的DateTime类型数据。因为现在的接口以restful风格居多,用String类型方便传输。 最后的毫秒数通过自定义的处理器统一截取去除即可。

JDBC 类型Java 类型类型处理器
TIMESTAMPjava.lang.StringCustomTypeHandler
<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接收数据库的时间类型数据了。

四、源码分析

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、调用

注册完毕之后,它在什么地方生效呢?关键在于能否可以找到这个处理器。看完上面的注册过程,查找其实很简单。先从TYPEHANDLERMAP根据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的问题。

  1. 姓名: 关小羽    创建时间: 2018-10-15 17:04:11

  2. 姓名: 小露娜    创建时间: 2018-10-15 17:10:46

  3. 姓名: 亚麻瑟    创建时间: 2018-10-15 17:10:46

  4. 姓名: 小鲁班    创建时间: 2018-10-16 16:10:47

上面的代码量很小,显然问题出在 ResultSet对象上。通过跟踪源码,最后笔者发现两台机器的mysql-connector-java版本不一样。一个是5.1.31,一个是6.0.6。我们把版本换成5.1.31,执行上面的main方法再看结果。

  1. 姓名: 关小羽    创建时间: 2018-10-15 17:04:11.0

  2. 姓名: 小露娜    创建时间: 2018-10-15 17:10:46.0

  3. 姓名: 亚麻瑟    创建时间: 2018-10-15 17:10:46.0

  4. 姓名: 小鲁班    创建时间: 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版本解决。如果不能动版本,再考虑自定义的类型处理器。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值