“夏令时”的版本间的差异
| Jihongchang(讨论 | 贡献)  (→具体的实现是) | Jihongchang(讨论 | 贡献)  | ||
| (未显示同一用户的8个中间版本) | |||
| 第56行: | 第56行: | ||
| </syntaxhighlight>如果把驱动包换成 8.0.21就不会抛异常 | </syntaxhighlight>如果把驱动包换成 8.0.21就不会抛异常 | ||
| − | === 1941-03-15 00:00 不存在 === | + | |
| + | 8.0.21 后的版本实现都会因为 java.util.Date birthDate 的属性声明,把 birthDate 当成一个 Timestamp 解析:<syntaxhighlight lang="java"> | ||
| + | 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); | ||
| + |             } | ||
| + |         } | ||
| + |     } | ||
| + | |||
| + | } | ||
| + | |||
| + | </syntaxhighlight> | ||
| + | |||
| + | === 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 类型 === | ||
| − | MySQL 中的 date  | + | MySQL 中的 date 类型只存年月日,实际应该对应 java.sql.Date 类型 | 
| + | |||
| + | 其他Java类型和JDBC类型的对照 | ||
| + | |||
| + | https://docs.oracle.com/cd/E19900-01/819-4734/beajw/index.html | ||
| + | |||
| + | === Java 中的 java.util.Date 和 java.sql.Date === | ||
| + | java.util.Date 精确到时分秒毫秒 | ||
| + | |||
| + | 但 java.sql.Date 对应数据库中的 Date 类型,只存储年月日 | ||
| + | |||
| + | === 应该把 MySQL 中的 date 类型 和 Java 中的 java.sql.Date 做映射(推荐) === | ||
| + | <syntaxhighlight lang="java"> | ||
| + | import java.sql.Date; | ||
| + | |||
| + | public class User { | ||
| + | |||
| + |   private Date birthDate; | ||
| + | |||
| + | } | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | === 或者在 mapper.xml 配置文件中声明 === | ||
| + | <syntaxhighlight lang="xml"> | ||
| + | …… | ||
| + | <resultMap type="User" id="UserResult"> | ||
| + |   …… | ||
| + |   <result property="birthDate" column="birth_date" jdbcType="DATE"> | ||
| + |   …… | ||
| + | </resultMap> | ||
| + | …… | ||
| + | <select id="selectUserList" resultMap="UserResult"> | ||
| + |   select * from user | ||
| + | </select> | ||
| + | …… | ||
| + | </syntaxhighlight> | ||
| + | |||
| + | === 其他 === | ||
| + | |||
| + | ==== System.currentTimeMillis() 获取到的毫秒偏移量也是相对于地区时间的 ==== | ||
| + | <syntaxhighlight lang="java"> | ||
| + |     @Test | ||
| + |     public void test() { | ||
| + |         SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); | ||
| + |         Date d = new Date(0); | ||
| + |         String str = simpleDateFormat.format(d); | ||
| + |         System.out.println(str); | ||
| + | |||
| + |         long currentTimeMillis = System.currentTimeMillis(); | ||
| + |         long quotient = currentTimeMillis / Integer.MAX_VALUE; | ||
| + |         long remainder = currentTimeMillis % Integer.MAX_VALUE; | ||
| + |         int remainderInt = (int) remainder; | ||
| + | |||
| + |         Calendar calendar = Calendar.getInstance(); | ||
| + |         calendar.setTimeInMillis(currentTimeMillis); | ||
| + | |||
| + |         TimeZone timeZone = calendar.getTimeZone(); | ||
| + |         System.out.println(timeZone); | ||
| + | |||
| + |         for (int i = 0; i < quotient; i++) { | ||
| + |             calendar.add(Calendar.MILLISECOND, -Integer.MAX_VALUE); | ||
| + |         } | ||
| + |         calendar.add(Calendar.MILLISECOND, -remainderInt); | ||
| + | |||
| + |         Date date = calendar.getTime(); | ||
| + | |||
| + | |||
| + |         String s = simpleDateFormat.format(date); | ||
| + | |||
| + |         System.out.println(s); | ||
| + | |||
| + |     } | ||
| + | </syntaxhighlight>输出:<syntaxhighlight lang="console"> | ||
| + | 1970-01-01 08:00:00.000 | ||
| + | sun.util.calendar.ZoneInfo[id="Asia/Shanghai",offset=28800000,dstSavings=0,useDaylight=false,transitions=31,lastRule=null] | ||
| + | 1970-01-01 08:00:00.000 | ||
| + | </syntaxhighlight>对于中国来说是相较于 1970-01-01 08:00:00.000 的毫秒偏移量,不是 1970-01-01 00:00:00.000 | ||
2025年8月19日 (二) 03:29的最新版本
一次生产环境遇到的问题
现象是程序运行过程中查询用户的数据抛异常:
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类型和JDBC类型的对照
https://docs.oracle.com/cd/E19900-01/819-4734/beajw/index.html
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>
……
其他
System.currentTimeMillis() 获取到的毫秒偏移量也是相对于地区时间的
    @Test
    public void test() {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
        Date d = new Date(0);
        String str = simpleDateFormat.format(d);
        System.out.println(str);
        long currentTimeMillis = System.currentTimeMillis();
        long quotient = currentTimeMillis / Integer.MAX_VALUE;
        long remainder = currentTimeMillis % Integer.MAX_VALUE;
        int remainderInt = (int) remainder;
        Calendar calendar = Calendar.getInstance();
        calendar.setTimeInMillis(currentTimeMillis);
        TimeZone timeZone = calendar.getTimeZone();
        System.out.println(timeZone);
        for (int i = 0; i < quotient; i++) {
            calendar.add(Calendar.MILLISECOND, -Integer.MAX_VALUE);
        }
        calendar.add(Calendar.MILLISECOND, -remainderInt);
        Date date = calendar.getTime();
        String s = simpleDateFormat.format(date);
        System.out.println(s);
    }
输出:
1970-01-01 08:00:00.000
sun.util.calendar.ZoneInfo[id="Asia/Shanghai",offset=28800000,dstSavings=0,useDaylight=false,transitions=31,lastRule=null]
1970-01-01 08:00:00.000
对于中国来说是相较于 1970-01-01 08:00:00.000 的毫秒偏移量,不是 1970-01-01 00:00:00.000