夏令时

来自姬鸿昌的知识库
跳到导航 跳到搜索

一次生产环境遇到的问题

现象是程序运行过程中查询用户的数据抛异常:

java.lang.IllegalArgumentException: HOUR_OF_DAY: 0 -> 1

	at java.util.GregorianCalendar.computeTime(GregorianCalendar.java:2829)
	at java.util.Calendar.updateTime(Calendar.java:3395)
	at java.util.Calendar.getTimeInMillis(Calendar.java:1782)

进一步定位发现只要查询一条出生日期是 1941-03-15 的数据,程序就会抛这个异常,但查别的数据都没有问题

具体的实现是

表 user 中有一个 date 类型的字段 birth_date,用来存储用户的出生年月日,

对应 Java 代码中的 User 类中声明的 java.util.Date 类型的 birthDate,然后应用了 Mybatis 执行查询,

对应的 mapper 映射文件是:

……
<resultMap type="User" id="UserResult">
  ……
  <result property="birthDate" column="birth_date">
  ……
</resultMap>
……
<select id="selectUserList" resultMap="UserResult">
  select * from user
</select>
……

大概就是上面这样


pom.xml 文件中

<dependency>
  <groupId>com.mysql</groupId>
  <artifactId>mysql-connector-j</artifactId>
  <version>8.2.0</version>
</dependency>

<!--
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <version>8.0.28</version>
</dependency>

<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <version>8.0.23</version>
</dependency>

<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <version>8.0.21</version>
</dependency>
-->

如果把驱动包换成 8.0.21就不会抛异常


8.0.21 后的版本实现都会因为 java.util.Date birthDate 的属性声明,把 birthDate 当成一个 Timestamp 解析:

package com.mysql.cj.result;

public class SqlTimestampValueFactory extends AbstractDateTimeValueFactory<Timestamp> {

    @Override
    public Timestamp localCreateFromDate(InternalDate idate) {
        if (idate.getYear() == 0 && idate.getMonth() == 0 && idate.getDay() == 0) {
            throw new DataReadException(Messages.getString("ResultSet.InvalidZeroDate"));
        }

        synchronized (this.defaultTimeZone) {
            Calendar c;

            if (this.cal != null) {
                c = this.cal;
            } else {
                // c.f. Bug#11540 for details on locale
                c = Calendar.getInstance(this.defaultTimeZone, Locale.US);
                c.setLenient(false);
            }

            try {
                c.clear();
                c.set(idate.getYear(), idate.getMonth() - 1, idate.getDay(), 0, 0, 0);
                return new Timestamp(c.getTimeInMillis());
            } catch (IllegalArgumentException e) {
                throw ExceptionFactory.createException(WrongArgumentException.class, e.getMessage(), e);
            }
        }
    }

}

1941-03-15 00:00:00 不存在

因为 Calendar 的实现,时间的计算是基于时区(java.util.TimeZone)的,一些地区实行过夏令时制度,一些时间段是不存在的,比如:1941-03-15 00:00:00,1941-03-14 23:59:59 之后就是 1941-03-15 01:00:00

https://www.iana.org/time-zones 有记录

MySQL 中的 date 类型

MySQL 中的 date 类型只存年月日,实际应该对应 java.sql.Date 类型

Java 中的 java.util.Date 和 java.sql.Date

java.util.Date 精确到时分秒毫秒

但 java.sql.Date 对应数据库中的 Date 类型,只存储年月日

应该把 MySQL 中的 date 类型 和 Java 中的 java.sql.Date 做映射(推荐)

import java.sql.Date;

public class User {

  private Date birthDate;

}

或者在 mapper.xml 配置文件中声明

……
<resultMap type="User" id="UserResult">
  ……
  <result property="birthDate" column="birth_date" jdbcType="DATE">
  ……
</resultMap>
……
<select id="selectUserList" resultMap="UserResult">
  select * from user
</select>
……