JDBC源码系列——不同版本mysql-connector保存Date类型数据时执行结果差异分析

519 阅读3分钟

一、背景

不同版本的mysql-connector执行同一条sql时,得到的结果有差别。

时间戳:1641439248850(2022-01-06 11:20:48.85)

  • 5.1.8版本执行后结果为11:20:48
  • 5.1.47版本执行后结果为11:20:49

即5.1.8版本是向下取整,5.1.47是四舍五入。

二、原因

1.测试代码

  • 连接数据库
  • 构建PreparedStatement对象
  • 调用PreparedStatement.executeUpdate()执行sql语句
  • 最后关闭连接
    DruidDataSource ds = new DruidDataSource();
    ds.setName("localhoset_3306_mytest");
    ds.setUrl("jdbc:mysql://localhost:3306/mytest?useUnicode=true&characterEncoding=UTF-8");
    ds.setUsername("root");
    ds.setPassword("");
    ds.setFilters("slf4j,stat,wall");
    String replaceSql = "insert into Haige(name,create_time) value(?,?)";
    Connection conn = ds.getConnection();// 获取Connection对象
    PreparedStatement pstmt = conn.prepareStatement(replaceSql);// 构建PreparedStatement对象
    Object[] paramValues = new Object[] { "test1", new Date() };
    for (int j = 0; j < paramValues.length; j++)
    {
        pstmt.setObject(j + 1, paramValues[j]);// 注入参数
    }
    pstmt.executeUpdate();// 执行sql
    pstmt.close();
    conn.close();

2.setObject方法

  • 所在类:com.mysql.jdbc.PreparedStatement
  • 入参的parameterObj判断类型
  • 类型如果是java.util.Date,调用setTimestamp方法
    public void setObject(int parameterIndex, Object parameterObj) throws SQLException {
        if (parameterObj == null) {
            this.setNull(parameterIndex, 1111);
        } else if (parameterObj instanceof Byte) {
            this.setInt(parameterIndex, ((Byte)parameterObj).intValue());
        .................
        } else if (parameterObj instanceof Clob) {
            this.setClob(parameterIndex, (Clob)parameterObj);
        } else if (this.connection.getTreatUtilDateAsTimestamp() && parameterObj instanceof java.util.Date) { // 类型如果是Date类型,进入setTimestamp方法
            this.setTimestamp(parameterIndex, new Timestamp(((java.util.Date)parameterObj).getTime()));// 核心方法
        } else if (parameterObj instanceof BigInteger) {
            this.setString(parameterIndex, parameterObj.toString());
        } else {
            this.setSerializableObject(parameterIndex, parameterObj);
        }

    }

3.setTimestamp方法

  • 所在类:com.mysql.jdbc.PreparedStatement
  • 进入setTimestampInternal方法
    public void setTimestamp(int parameterIndex, Timestamp x) throws SQLException {
        this.setTimestampInternal(parameterIndex, x, (Calendar)null, Util.getDefaultTimeZone(), false);// 核心方法
    }

4.setTimestampInternal方法

  • 所在类:com.mysql.jdbc.PreparedStatement
  • 处理时间戳后三位: formatNanos方法

4.1 5.1.8版本

  • 处理时间戳后三位的方法:this.formatNanos(x.getNanos())
  • 拼接结果:2022-01-06 11:20:48.0
    private void setTimestampInternal(int parameterIndex, Timestamp x, Calendar targetCalendar, TimeZone tz, boolean rollForward) throws SQLException {
        if (x == null) {
            this.setNull(parameterIndex, 93);
        } else {
            this.checkClosed();//检查数据库连接是否关闭
            if (!this.useLegacyDatetimeCode) { //useLegacyDatetimeCode 默认为true,开启服务端和客户端的时区转换
                this.newSetTimestampInternal(parameterIndex, x, targetCalendar);
            } else {
                String timestampString = null;
                //useJDBCCompliantTimezoneShift 使用JDBC是否符合时区转换,为true获取UTC时间,false获取jdbc连接中session时间
                Calendar sessionCalendar = this.connection.getUseJDBCCompliantTimezoneShift() ? this.connection.getUtcCalendar() : this.getCalendarInstanceForSessionOrNew();
                synchronized(sessionCalendar) {
                    x = TimeUtil.changeTimezone(this.connection, sessionCalendar, targetCalendar, x, tz, this.connection.getServerTimezoneTZ(), rollForward);
                }

                if (this.connection.getUseSSPSCompatibleTimezoneShift()) {
                    this.doSSPSCompatibleTimezoneShift(parameterIndex, x, sessionCalendar);
                } else {
                    synchronized(this) {
                        if (this.tsdf == null) {
                            this.tsdf = new SimpleDateFormat("''yyyy-MM-dd HH:mm:ss", Locale.US);
                        }

                        timestampString = this.tsdf.format(x);
                        StringBuffer buf = new StringBuffer();
                        buf.append(timestampString);
                        buf.append('.');
                        buf.append(this.formatNanos(x.getNanos()));// 核心方法
                        buf.append(''');
                        this.setInternal(parameterIndex, buf.toString());//拼接结果:2022-01-06 11:20:48.0
                    }
                }
            }

            this.parameterTypes[parameterIndex - 1 + this.getParameterIndexOffset()] = 93;
        }

    }

4.2 5.1.47版本

  • 处理时间戳后三位核心方法:TimeUtil.formatNanos
  • 拼接结果: 2022-01-06 11:20:48.85
    protected void setTimestampInternal(int parameterIndex, Timestamp x, Calendar targetCalendar, TimeZone tz, boolean rollForward, int fractionalLength, boolean useSSPSCompatibleTimezoneShift) throws SQLException {
        if (x == null) {
            this.setNull(parameterIndex, 93);
        } else {
            this.checkClosed();
            x = (Timestamp)x.clone();
            if (!this.serverSupportsFracSecs || !this.sendFractionalSeconds && fractionalLength == 0) {
                x = TimeUtil.truncateFractionalSeconds(x);
            }

            if (fractionalLength < 0) {
                fractionalLength = 6;
            }

            x = TimeUtil.adjustTimestampNanosPrecision(x, fractionalLength, !this.connection.isServerTruncatesFracSecs());
            if (!this.useLegacyDatetimeCode) {
                this.newSetTimestampInternal(parameterIndex, x, targetCalendar);
            } else {
                Calendar sessionCalendar = this.connection.getUseJDBCCompliantTimezoneShift() ? this.connection.getUtcCalendar() : this.getCalendarInstanceForSessionOrNew();
                sessionCalendar = TimeUtil.setProlepticIfNeeded(sessionCalendar, targetCalendar);
                x = TimeUtil.changeTimezone(this.connection, sessionCalendar, targetCalendar, x, tz, this.connection.getServerTimezoneTZ(), rollForward);
                if (useSSPSCompatibleTimezoneShift) {
                    this.doSSPSCompatibleTimezoneShift(parameterIndex, x, fractionalLength, targetCalendar);
                } else {
                    synchronized(this) {
                        this.tsdf = TimeUtil.getSimpleDateFormat(this.tsdf, "''yyyy-MM-dd HH:mm:ss", (Calendar)null, (TimeZone)null);
                        Calendar adjCal = TimeUtil.setProlepticIfNeeded(this.tsdf.getCalendar(), targetCalendar);
                        if (this.tsdf.getCalendar() != adjCal) {
                            this.tsdf.setCalendar(adjCal);
                        }

                        StringBuffer buf = new StringBuffer();
                        buf.append(this.tsdf.format(x));
                        if (fractionalLength > 0) {
                            int nanos = x.getNanos();
                            if (nanos != 0) {
                                buf.append('.');
                                buf.append(TimeUtil.formatNanos(nanos, this.serverSupportsFracSecs, fractionalLength));// 核心方法
                            }
                        }

                        buf.append(''');
                        this.setInternal(parameterIndex, buf.toString());// 拼接结果:2022-01-06 11:20:48.85
                    }
                }
            }

            this.parameterTypes[parameterIndex - 1 + this.getParameterIndexOffset()] = 93;
        }

5.formatNanos

  • 所在类:com.mysql.jdbc.PreparedStatement
  • 可以看到5.1.47是取了具体的值,而5.1.8版本是强制填写了0

5.1.47

   public static String formatNanos(int nanos, boolean serverSupportsFracSecs, int fsp) throws SQLException {
        if (nanos >= 0 && nanos <= 999999999) {
            if (fsp >= 0 && fsp <= 6) {
                if (serverSupportsFracSecs && fsp != 0 && nanos != 0) {
                    nanos = (int)((double)nanos / Math.pow(10.0D, (double)(9 - fsp)));
                    if (nanos == 0) {
                        return "0";
                    } else {
                        String nanosString = Integer.toString(nanos);
                        String zeroPadding = "000000000";
                        nanosString = "000000000".substring(0, fsp - nanosString.length()) + nanosString;

                        int pos;
                        for(pos = fsp - 1; nanosString.charAt(pos) == '0'; --pos) {
                        }

                        nanosString = nanosString.substring(0, pos + 1);
                        return nanosString;
                    }
                } else {
                    return "0";
                }
            } else {
                throw SQLError.createSQLException("fsp value must be in 0 to 6 range but was " + fsp, "S1009", (ExceptionInterceptor)null);
            }
        } else {
            throw SQLError.createSQLException("nanos value must be in 0 to 999999999 range but was " + nanos, "S1009", (ExceptionInterceptor)null);
        }
    }

5.1.8

    private String formatNanos(int nanos) {
        return "0";
    }

三、总结

  • 虽然我们System.out.println(new Date());打印出来的Date类型只精确到秒级别,但这只是toString()方法的实现,真正Date的精度还是毫秒级别的
  • mysql-connector的5.1.8版本 会强制让时间精确到秒级别,向下取整;而5.1.47版本,会保留时间的原有毫秒级别精度
  • 之后mysql在执行时,会对日期做四舍五入处理,因此导致最后结果有1秒的差别

\