1、需求规范:
1.1、所有需求点都要求解释标注国际化怎么处理。如果没有标注如何处理,则默认为国际通行处理(不处理),即和区域、语言、人文文化、时区、宗教等没有关系。
1.2、
2、时间规范:
2.1、时间点、时间戳、时刻的表示和存储规范:
2.1.1、默认优先使用 timestamp 类型存储:
timestamp 类型存储和数据库服务器时区、应用服务器时区没有任何关系,展示和解析不同而已。如:created_at, updated_at, deleted_at, paid_at, refound_at 等等时间点。
隐藏问题:timestamp 能表示的时间在 1970-01-01 00:00:00 UTC 到 2038-01-19 03:14:07 UTC 之间。
2.1.2、如果 timestamp 范围不够存储:
使用:【date/datetime/time + 时区】两列共同表示一个时间点。时区优先使用 UTC。使用 MySQL 的 datetime 等时间数据类型,存储的是一段字符串,跟写入时的状态有关,读出来的时候不会再改变字面量(写入后就保持静态),所以如果要让这个字面量在不同的时区表现出相关性,则需要在配合时区列来做转换。
比如:生日在 1970 年以前。预测未来的某个时间点大于 2038 年,等等。
3、技术调研:
mysql存储的值类型 | jvm系统时区 | mysql链接指定时区 | 原始值 | java对应字段类型 | 映射值 | 存入字符串数据,mysql加时区-8:00 |
---|---|---|---|---|---|---|
datetime | 系统时区 | serverTimezone=Asia/Shanghai |
2020-04-16 16:45:44 | String | 2020-04-16 16:45:44 |
数据库中的值:2020-04-16 16:45:44 展示值: 2020-04-16 16:45:44 |
datetime | 系统时区 | serverTimezone=Asia/Shanghai |
2020-04-16 16:45:44 | LocalDateTime |
2020-04-16T16:45:44 |
数据库中的值:2020-04-16 16:45:44 展示值: 2020-04-16T16:45:44 |
datetime | 系统时区 | serverTimezone=Asia/Shanghai |
2020-04-16 16:45:44 | ZonedDateTime |
不支持 | 不支持 |
datetime | 系统时区 | serverTimezone=Australia/Darwin | 2020-04-16 16:45:44 | String | 2020-04-16 16:45:44 | 数据库中的值:2020-04-16 16:45:44``展示值: 2020-04-16 16:45:44 |
datetime | 系统时区 | serverTimezone=Australia/Darwin | 2020-04-16 16:45:44 | LocalDateTime |
2020-04-16 16:45:44 | 数据库中的值:2020-04-16 18:15:44``展示值: 2020-04-16T18:15:44 |
timestamp | 系统时区 | serverTimezone=Asia/Shanghai | 2020-04-16 16:45:44 | String | 2020-04-16 16:45:44 | 数据库中的值:2020-04-16 16:45:44``展示值: 2020-04-16 16:45:44 |
timestamp | 系统时区 | serverTimezone=Asia/Shanghai |
2020-04-16 16:45:44 | LocalDateTime |
2020-04-16T16:45:44 |
数据库中的值:2020-04-16 00:45:44``展示值: 2020-04-16T16:45:44 |
timestamp | 系统时区 | serverTimezone=Asia/Shanghai |
2020-04-16 16:45:44 | ZonedDateTime |
不支持 | 不支持 |
timestamp | 系统时区 | serverTimezone=Australia/Darwin | 2020-04-16 16:45:44 | String | 2020-04-16 16:45:44 | 数据库中的值:2020-04-16 16:45:44``展示值: 2020-04-16 16:45:44 |
timestamp | 系统时区 | serverTimezone=Australia/Darwin | 2020-04-16 16:45:44 | LocalDateTime |
2020-04-16 16:45:44 | 数据库中的值:2020-04-16 18:15:44``展示值: 2020-04-16T18:15:44 |
总结:
- timestamp,当用string存储和查看时,写入的时间和读出的时间不受mysql_url设置时区影响,并且不受mysql系统设置时区影响。
当用LocalDateTime时,展示和存储都受mysql_url设置时区影响,并且受mysql系统设置时区影响; - datetime,当用string存储时,写入的时间和读出的时间不受mysql_url设置时区影响,并且不受mysql系统设置时区影响。
当用LocalDateTime时写入的时间和存储时间受mysql_url设置时区影响,但是不受mysql系统设置时区影响;
JDK8所有时间对象
对象名称 | 作用 | 实现方式 | 备注 |
---|---|---|---|
Date | 时间戳UTC+时区偏移量 | long fastTime = 1594367598993 Gregorian.Date cdate = "2020-07-10T15:53:18.993+0800" | 记录时间和时区 |
Instant | 时间戳UTC | long seconds = 1594367634int nanos = 998000000 | 对应jdk7之前的Date,可通过Epoch Time 纪元时相互转换 |
LocalDateTime | 获取当前系统的日期时间(内部不记录时区) | LocalDate date = "2020-07-10" LocalTime time = "15:54:07.858" | 可以认为由LocalDate和LocalTime组成 |
LocalDate | 获取当前系统的日期 | int year = 2020short month = 7short day = 10 | |
LocalTime | 获取当前系统的时间 | byte hour = 15 byte minute = 54int nano = 906000000byte second = 33 | |
ZoneId | 时区,"+08:00"和"Asia/Shanghai" | id = "Asia/Shanghai" ZoneRules rules = "ZoneRules[currentStandardOffset=+08:00]" | ZoneId除了处理与标准时间的时间差还处理地区时(夏令时,冬令时等) |
ZoneOffset | 时区,只处理 "+01:00" |
int totalSeconds = 0 String id = "Z" |
ZoneOffset是ZoneId的子类,只处理与格林尼治的时间差 |
ZoneDateTime | 时间字符串+时区+时间偏移量 | LocalDateTime dateTime = "2020-07-10T15:55:45.859" ZoneOffset offset = "+08:00" ZoneRegion zone = "Asia/Shanghai" | LocalDateTime内部不记录时区,ZoneDateTime记录 |
OffsetDateTime |
时间字符串+时区偏移量 | LocalDateTime dateTime = "2020-07-10T15:56:16.174" ZoneOffset offset = "+08:00" |
**
**
概念
1.相对时间和绝对时间
-
相对时间: "yyyy-MM-dd HH:mm:ss" 或 "HH:mm"。
比如:门店营业时间点 09:00 - 22:00,在各个国家和地区都是相对当地而言,因此称为相对时间;
又比如:权益有效时间 2020-07-08 00:00:00 - 2021-07-09 00:00:00 指的是用户在某一个国家、某一个地区、某一个门店的权益有效时间,因此一般情况下是相对时间(如果需求上认为这些权益不是跟随国家、地区、门店维度,那将会产生一些业务漏洞)。在 0 时区有效时间用户看到的是 2020-07-08 00:00:00 - 2021-07-09 00:00:00,在中国,用户看到的是 2020-07-07 16:00:00 - 2021-07-08 16:00:00,如果说需求上要求全球通用权益,那时间就需要做转换,业务会变复杂起来。
-
绝对时间: "yyyy-MM-dd HH:mm:ss" + timezone 或 timestamp。
比如:创建订单时间是一个时间点/时刻,一刹那,所以是绝对时间,从 0 时区算起开始到创建订单时间是不会相对不同时区产生歧义,因此,成为绝对时间。
怎么做
\1. 数据库字段类型
- 相对时间:datetime,date,time;
- 绝对时间:timestamp;
\2. java【实体类】时间字段类型
- 相对时间:java.lang.String;
- 绝对时间:java.time.Instant;
\3. instant使用规则
- 时间戳转换统一指定UTC;
- 当请求通过url拼接参数的格式传输时,后端用Instant对象接收时,只能用世界标准时间格式:yyyy-MM-ddTHH:mm:ssZ,2020-04-16T16:45:44Z;
- 当请求通过body参数的格式传输时,后端用Instant对象接收时,jackson有3种方式将前端传的值解析为Instant:
1.(默认)世界标准时间2020-04-16T16:45:44Z
2.时间戳1587055544(以UTC标准)
3.后端指定注解指定日期格式和时区=UTC,前端传2020-04-16 16:45:44; - 其它方式:
1.后端用Long来接收,前端传的时间戳以UTC为标准
2.后端用String和时区接收,前端传日期字符串格式和时区;
为什么
1.相对时间,【实体对象】从mysql或者前端接受为什么用String而不是LocalDate、LocalTime?
- LocalDate、LocalTime存储读取时,会转成jvm的系统时区时间,导致读取值不准确,因为mysql的datetime字段是不分时区的(不推荐使用)
- String 存的就是字符串,不管什么时区都不会变,使用时只需指定ZoneId转成时间对象即可(推荐使用)
2.绝对时间,【实体对象】从mysql或者前端接受为什么用Instant而不是Date、LocalDateTime、ZonedDateTime、OffSetDateTime?
- Date已经比较旧了,不推荐使用;
- ZonedDateTime、OffSetDateTimezoneDateTime之类的不是高版本的mybatis不支持,是mybatis对数据库提出了更高的要求,目前mysql不支持,原因:https://github.com/mybatis/mybatis-3/issues/1750 (不推荐使用);
- LocalDateTime存绝对时间的时候,我们希望无论是在数据库中存储还是在jvm取值的时候,都是UTC时间标准的时间类型,而 LocalDateTime在取值的时候会默认将时间戳按照服务器时区转换成【int类型的年/月/日/时/分/秒 + zoneId】的形式存储,不利于:
\1. 不同时区的时间比较
\2. 服务部署在不同时区
而Instant的实现是基于UCT的时间戳,本身不具备时区信息,可以确保在业务使用时按照预期给出转换结果,规避时区不同引起的bug(不建议使用); - Instant 时间戳对象,存的就是时间戳,与时区无关,只需根据ZoneId转成时间对象即可(推荐使用);
还需注意什么
- 时间戳的值,所有系统统一以UTC时间为标准,因此不会受jvm系统时区、mysql系统时区、jdbc连接配置时区影响,但这三个时区会决定在对应环境中展示的时间字符串时间;
- Instant的toString(),采用UTC的00:00时区为准;
- Instant的compareTo(Instant otherInstant)、isBefore(Instant otherInstant)、isAfter(Instant otherInstant)比较的是时间戳;
使用Instant示例
表
CREATE TABLE `zone_time` (
`id` INT NOT NULL AUTO_INCREMENT,
`created_at` datetime NULL COMMENT '创建时间',
`updated_at` TIMESTAMP NULL COMMENT '更新时间',
PRIMARY KEY ( `id` ) USING BTREE );
实体对象
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class ZoneTime implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
private String createdAt;
private Instant updatedAt;
}
使用instant接收参数
-
eg1: 传值:世界标准时间格式,@PathVariable
{url}/{updatedAt},举例:url/2020-04-16T16:45:44Z
后端参数:
@PathVariable("updatedAt") Instant updatedAt
java取出的值:
-
eg2:传值:世界标准时间格式(body) yyyy-MM-ddTHH:mm:ssZ
RequestBody对象
@Data @EqualsAndHashCode(callSuper = false) @Accessors(chain = true) public class ZoneTime implements Serializable { private static final long serialVersionUID = 1L; @TableId(value = "id", type = IdType.AUTO) private Integer id; private String createdAt; private Instant updatedAt; }
前端请求的body
{ "createdAt":"2020-04-16 16:45:44", "updatedAt":"2020-04-16T16:45:44Z" }
-
eg3: 传值:时间戳(body)
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class ZoneTime implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
private String createdAt;
private Instant updatedAt;
}
前端请求的body
{
"createdAt":"2020-04-16 16:45:44",
"updatedAt":"1587055544"
}
-
eg4: 传日期时间格式(body)
RequestBody对象
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class ZoneTime implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
private String createdAt;
private Instant updatedAt;
}
前端请求的body
{
"createdAt":"2020-04-16 16:45:44",
"updatedAt":"2020-04-16 16:45:44"
}
mysql存入的值
java8的时间互转示例
Instant->其他时间类
- Instant→ZonedDateTime
Instant instant = Instant.now();
ZonedDateTime zonedDateTime = instant.atZone(ZoneId.systemDefault());
- Instant→OffsetDateTime
Instant instant = Instant.now();
OffsetDateTime offsetDateTime = instant.atOffset(ZoneOffset.UTC);
- Instant→LocalDateTime
Instant instant = Instant.now();
LocalDateTime localDateTime = instant.atZone(ZoneId.systemDefault()).toLocalDateTime();
LocalDateTime localDateTime = instant.atOffset(ZoneOffset.UTC).toLocalDateTime();
- Instant→LocalDate
LocalDate localDate = instant.atZone(ZoneId.systemDefault()).toLocalDate();
LocalDate localDate = instant.atOffset(ZoneOffset.UTC).toLocalDate();
- Instant→LocalTime
LocalTime localTime = instant.atOffset(ZoneOffset.UTC).toLocalTime();
LocalTime localTime = instant.atZone(ZoneId.systemDefault()).toLocalTime();
- 汇总instant转为其他时间对象:
public static void main(String[] args) {
Instant instant = Instant.now();
ZonedDateTime zonedDateTime = instant.atZone(ZoneId.systemDefault());
System.out.println("Instant→ZonedDateTime:" + zonedDateTime);
OffsetDateTime offsetDateTime = instant.atOffset(ZoneOffset.UTC);
System.out.println(" Instant→OffsetDateTime:" + offsetDateTime);
LocalDateTime zoneToLocalDateTime = instant.atZone(ZoneId.systemDefault()).toLocalDateTime();
LocalDateTime offSetLocalDateTime = instant.atOffset(ZoneOffset.UTC).toLocalDateTime();
System.out.println(" Instant→LocalDateTime:" + zoneToLocalDateTime);
System.out.println(" Instant→LocalDateTime:" + offSetLocalDateTime);
LocalDate zoneToLocalDate = instant.atZone(ZoneId.systemDefault()).toLocalDate();
LocalDate offSetLocalDate = instant.atOffset(ZoneOffset.UTC).toLocalDate();
System.out.println(" Instant→LocalDate:" + zoneToLocalDate);
System.out.println(" Instant→LocalDate:" + offSetLocalDate);
LocalTime zoneToLocalTime = instant.atOffset(ZoneOffset.UTC).toLocalTime();
LocalTime offSetLocalTime = instant.atZone(ZoneId.systemDefault()).toLocalTime();
System.out.println(" Instant→LocalTime:" + zoneToLocalTime);
System.out.println(" Instant→LocalTime:" + offSetLocalTime);
}
String->...->Instant
String→instant(String格式必须为如下格式)默认时区UTC
String date = "2007-12-03T10:15:30.00Z";
Instant instant=Instant.parse(date);
String->LocalDateTime→instant
String date = "2020-04-16 16:45:44";
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
Instant instant = LocalDateTime.parse(date, formatter).toInstant(ZoneOffset.UTC);
String->ZonedDateTime→instant
String date = "2020-04-16 16:45:44";
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(ZoneId.systemDefault());
ZonedDateTime zonedDateTime=ZonedDateTime.parse(date,formatter);
Instant instant=zonedDateTime.toInstant();
使用UTC时间转换对应时区
对象实现
@Slf4j
public class Test {
public static void main(String[] args) {
String date = "2020-04-16 16:45:44";
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
Instant instant = LocalDateTime.parse(date, formatter).toInstant(ZoneOffset.UTC);
log.info("UTC时间:{}", instant);
log.info("时间戳:{}", instant.getEpochSecond());
ZonedDateTime mskZonedDateTime = ZonedDateTime.ofInstant(instant, ZoneId.of("Europe/Moscow"));
log.info("莫斯科时间:{}", mskZonedDateTime);
log.info("莫斯科时间偏移量:{}", mskZonedDateTime.getOffset().getId());
log.info("莫斯科时间偏移量总秒:{}", mskZonedDateTime.getOffset().getTotalSeconds());
ZonedDateTime shanghaiZonedDateTime = ZonedDateTime.ofInstant(instant, ZoneId.of("Asia/Shanghai"));
log.info("上海时间:{}", shanghaiZonedDateTime);
log.info("上海时间偏移量:{}", shanghaiZonedDateTime.getOffset().getId());
log.info("上海时间偏移量总秒:{}", shanghaiZonedDateTime.getOffset().getTotalSeconds());
}
}
输出结果
时间对象相关的链接
https://blog.****.net/u012107143/article/details/78790378
https://lw900925.github.io/java/java8-newtime-api.html