国际化开发规范

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

总结:

  1. timestamp,当用string存储和查看时,写入的时间和读出的时间不受mysql_url设置时区影响,并且不受mysql系统设置时区影响。
    当用LocalDateTime时,展示和存储都受mysql_url设置时区影响,并且受mysql系统设置时区影响;
  2. 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 = 0String 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转成时间对象即可(推荐使用);

还需注意什么

  1. 时间戳的值,所有系统统一以UTC时间为标准,因此不会受jvm系统时区、mysql系统时区、jdbc连接配置时区影响,但这三个时区会决定在对应环境中展示的时间字符串时间;
  2. Instant的toString(),采用UTC的00:00时区为准;
  3. 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

上一篇:常用的SpringBoot时间格式化


下一篇:Oracle之内存结构(SGA、PGA)