获取一周、一个月、一年、一小时、一分钟后的日期等
LocalDate是用来表示无时间的日期,他又一个plus()方法可以用来增加日,星期,月,ChronoUnit则用来表示时间单位
表示和处理固定的日期,比如信用卡过期时间
YearMonth是另外一个组合,可以很好处理信用卡有效期只有年、月的问题。LengthOfMonth()这个方法返回的是这个YearMonth实例有多少天,这对于检查2月是否润2月很有用
两个日期之间包含多少天,多少月(这个非常实用)
计算两个日期之间包含多少天、周、月、年。可以用java.time.Period类完成该功能。下面例子中将计算日期与将来的日期之间一共有几个月
带时区的日期与时间(以后处理时区问题,还是用ZoneDateTime吧)
在java8中,可以使用ZoneOffset来代表某个时区,可以使用它的静态方法ZoneOffset.of()方法来获取对应的时区,只要获得了这个偏移量,就可以用这个偏移量和LocalDateTime创建一个新的OffsetDateTime
说明:OffsetDateTime主要是用来给机器理解的,平时使用就用前面结束的ZoneDateTime类就可以了
如何在两个日期之间获得所有日期
这个需求其实是比较常见的需求,所有很有必要在这里实现一把。因为其实实现起来并不见得那么简单,还有不少误区:所以我这里展开说一下
LocalDate start = LocalDate.of(2018, Month.DECEMBER, 1); System.out.println(start.lengthOfMonth()); //31 System.out.println(start.lengthOfYear()); //365
因此我们先造出两个日期出来,然后求出他们的差值如下:
LocalDate start = LocalDate.of(2018, Month.DECEMBER, 1); LocalDate end = LocalDate.of(2020, Month.APRIL, 10);
有的人可能第一眼可能会想到用Period来做:
Period period = Period.between(start, end); System.out.println(period); //P1Y4M9D System.out.println(period.getYears()); //1 System.out.println(period.getMonths()); //4 System.out.println(period.getDays()); //9 //备注:Period period = start.until(end); //效果同上
我们会发现,根本就就不是我们想要的。其实这里需要注意一点:从输出的值可以看出,Period得到的是差值的绝对值,而并不表示真正的区间距离。因为它表示一个时段,所以肯定是绝对值含义。
所以我们想到可以如下处理(方法一):
//先计算出两个日期的像个 long distance = ChronoUnit.DAYS.between(start, end); //for循环往里面处理 for(int i = 0; i <= distance; i++){ start.plusDays(i); //...do the stuff with the new date... }
下面介绍一种更优雅的方案(方案二)
List<LocalDate> days = Stream.iterate(start, d -> d.plusDays(1)).limit(distance + 1).collect(toList());
采用迭代流来生成,显得逼格满满。
这里面穿插一下,ChronoUnit类。它像是一个单位类
start.plus(1,ChronoUnit.DAYS); //等价于 start.plusDays(1);
下面这个需要注意,LocalDate本身具备的一种能力:
long distance1 = start.until(end, ChronoUnit.DAYS); System.out.println(distance1); //496 long distance2 = ChronoUnit.DAYS.between(start, end); System.out.println(distance2); //496
大赞Java8 时间API的设计,条条大路通罗马啊
如何在两个日期之间获得所有的月份
有了上面的额例子,这个自然不在话下。那么就继续来上代码:
//获取开始、结束日期内所有的月份 long monthCount = ChronoUnit.MONTHS.between(start, end); Stream.iterate(start, x -> x.plusMonths(1)).limit(monthCount + 1).forEach(System.out::println);
照葫芦画瓢,只是简单的把单位换一下就ok了。
ZoneOffset 于 ZoneId
ZoneOffset 表示与UTC时区偏移的固定区域。
ZoneOffset不随着由夏令时导致的区域偏移的更改。
UTC是UTC的时区偏移常量(Z用作UtC时区的区域偏移指示符。)。MAX和MIN是最大和最小支持的区域偏移。
我们可以用小时,分钟和秒的组合创建 ZoneOffset 。
public static void main(String[] args) { //一般只会用到Hours的便宜 ZoneOffset zoneOffset1 = ZoneOffset.ofHours(-1); //-01:00 System.out.println(zoneOffset1); ZoneOffset zoneOffset2 = ZoneOffset.ofHoursMinutes(6, 30); //+06:30 System.out.println(zoneOffset2); ZoneOffset zoneOffset3 = ZoneOffset.ofHoursMinutesSeconds(9, 30, 45); //+09:30:45 System.out.println(zoneOffset3); }
以下代码显示如何从偏移创建区域偏移。
public static void main(String[] args) { ZoneOffset zoneOffset1 = ZoneOffset.of("+05:00"); //+05:00 ZoneOffset zoneOffset2 = ZoneOffset.of("Z"); //Z 效果同:ZoneOffset.UTC System.out.println(zoneOffset1); System.out.println(zoneOffset2); }
API支持-18:00到+18:00之间的区域偏移。
ZoneId 表示区域偏移及其用于更改区域偏移的规则夏令时。
每个时区都有一个ID,可以用三种格式定义:
在区域偏移中,可以是“Z”,“+ hh:mm:ss”或“-hh:mm:ss”,例如“+01:00”。
前缀为“UTC”,“GMT”或“UT”,后跟区域偏移量,例如“UTC + 01:00”。
在区域名称中,例如,“美洲/芝加哥”。(比较常用)
以下代码显示如何使用of()工厂方法创建ZoneId。
public static void main(String[] args) { //备注:此字符串必须合法 否则报错 ZoneId usChicago = ZoneId.of("Asia/Shanghai"); //Asia/Shanghai System.out.println(usChicago); ZoneId fixedZoneId = ZoneId.of("+01:00"); System.out.println(fixedZoneId); //+01:00 }
ZoneId 中的 getAvailableZoneIds()返回所有已知时区ID
public static void main(String[] args) { System.out.println(ZoneId.systemDefault()); //Asia/Shanghai System.out.println(ZoneId.getAvailableZoneIds()); //[Asia/Aden, America/Cuiaba, Etc/GMT+9, Etc/GMT+8 }
使用java8我们知道使用ZoneId.default()可以获得系统默认值ZoneId,但如何获取默认值ZoneOffset?我看到一个ZoneId有一些“规则”而且每个规则都有一个ZoneOffset,这意味着一个ZoneId可能有一个以上ZoneOffset吗?答案如下:
public static void main(String[] args) { System.out.println(ZoneOffset.of("+8")); //+08:00 System.out.println(ZoneOffset.ofHours(8)); //+08:00 //获取系统的默认值==================推荐使用 System.out.println(OffsetDateTime.now().getOffset()); //+08:00 System.out.println(ZoneId.systemDefault()); //Asia/Shanghai }
Spring MVC、MyBatis、Feign中使用JSR310的日期
首先你需要引入对应的Jar(这是很多人不知道怎么支持的最重要原因)
<-- 让Mybatis支持JSR310 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-typehandlers-jsr310</artifactId> <version>1.0.2</version> </dependency> <-- 让SpringMVC支持JSR310 --> <dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> <version>2.9.7</version> </dependency>
备注:
如果你是SpringBoot环境,SpringMVC依赖的版本号version都可以省略,而且建议省略。SpringBoot2.0以上版本,不需要自己再额外导入SpringMVC的那个JSR310依赖的jar,因为默认就自带了
如果你的Mybatis版本在3.4.0以上,导包就支持。如果在3.4.0一下版本,就需要自己手动配置文件里注册(不过我建议直接升MyBatis版本吧)
重点说明:MyBatis @since 3.4.5(2017.8月份发布)之后,就内置了对jsr310的支持,不用再额外导包了哦~
包名都没有改变,所以若你的MyBatis在3.4.5以上的版本,直接移除掉你jackson-datatype-jsr310这个pom就行了
建议以后放弃使用Date和Timestamp类型。
DB的entiry使用LocalDateTime对应sql的datetime、LocalDate对应date、LocalTime对应time 足够你用的了,而且安全性更高
为何能够处理这些时间?看到下面截图一目了然:
导入之后:SpringMVC传入参数如下:
{ "startDate" : "2018-11-01" //“2018/11/01”默认是非法的 }
服务端直接这样接受就行:
@NotNull private LocalDate startDate; //什么注解都不需要
注解@DateTimeFormat只对Date类型有效,对JSR类型都将无效
需要注意的是,LocalDate使用这种格式的串没问题。但LocalDateTime可不行。比如:
{ "startDateTime" : "2018-11-01 18:00:00" //这个是非法的 而"2018-11-24T09:04:16.383" 这种格式才是默认合法的 }
为什么呢?进源码看一下:LocalDateTimeSerializer类有这么一句
protected DateTimeFormatter _defaultFormatter() { return DateTimeFormatter.ISO_LOCAL_DATE_TIME; //它的值是形如这种格式的模版"2018-11-24T09:04:16.383" }
其实从他们的默认的toString()方法也能看出一点端倪:
public static void main(String[] args) { System.out.println(LocalDateTime.now()); //2018-11-24T17:12:27.395 System.out.println(LocalDate.now()); //2018-11-24 System.out.println(LocalTime.now()); //17:12:57.323 }
那么问题来了,怎么样才能让LocalDateTime友好的接受我们想的那种字符串呢?
方案一:自己写一个LocalDateTimeSerializer的实现,然后通过@JsonSerialize指定序列化器
方法二(推荐):在字段上面采用@JsonFormat指定序列化以及反序列化的格式
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime localDateTime;
小知识:
SpringMVC默认采用Jackson进行序列化和反序列话。对于时间类型的默认的序列化(序列化表示把对象对外输出,如SpringMVC的返回值就需要经过这个过程):
Date类型按照GMT标准时间 成时间戳
Timestamp类型按照GMT标准时间 成时间戳
LocalDate:“startDate”: [ 2018,11,1] 序列化成数组类型
显然LocalDate等类型序列化成数组,是不优雅的方案。而且如果你使用的是feign进行API调用的话,肯定报错。因为对方根本不能识别这个数组,我们希望序列化的结果是:“2018-11-01”这样子优雅,切feign也能正常使用了,咋办呢?
方案:
1、各种自定义类型转换器(这里不做过多讲解)
2、采用全局的converter转换器
3、采用@JsonFormat(pattern = “yyyy-MM-dd”) 注解标注字段输出(推荐)
@Bean public ObjectMapper serializingObjectMapper() { ObjectMapper objectMapper = new ObjectMapper(); objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); objectMapper.registerModule(new JavaTimeModule()); return objectMapper; }
SpringMVC Get请求中,LocalDateTime、LocalDate等JSR310的反序列化处理
本以为Get请求和上面一样,加一个@JsonFormat就可以了,但我这么做
@ApiOperation("测试接受时间类型Get") @PostMapping("/test/jsr310") Object testJsrGet(@RequestParam @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime localDateTime) { System.out.println(localDateTime); return localDateTime; }
客户端传值:
"startDateFrom" : "2018-11-01 18:00:00"
按照上面的理论,本以为没问题了,但奈何,还是出错了。怎么破?
Failed to convert value of type 'java.lang.String' to required type 'java.time.LocalDateTime';
杀千刀的,通过打断点跟踪发现,在解析时间的时候。SptingMVC调用的竟然是自己内部的解析器,根本就没有用到fastjson,因此那个注解自然而然没有作用,确实有点坑啊。
这里有一个类:TemporalAccessorParser:parse
@Override public TemporalAccessor parse(String text, Locale locale) throws ParseException { DateTimeFormatter formatterToUse = DateTimeContextHolder.getFormatter(this.formatter, locale); if (LocalDate.class == this.temporalAccessorType) { return LocalDate.parse(text, formatterToUse); } else if (LocalTime.class == this.temporalAccessorType) { return LocalTime.parse(text, formatterToUse); } else if (LocalDateTime.class == this.temporalAccessorType) { return LocalDateTime.parse(text, formatterToUse); } else if (ZonedDateTime.class == this.temporalAccessorType) { return ZonedDateTime.parse(text, formatterToUse); } else if (OffsetDateTime.class == this.temporalAccessorType) { return OffsetDateTime.parse(text, formatterToUse); } else if (OffsetTime.class == this.temporalAccessorType) { return OffsetTime.parse(text, formatterToUse); } else { throw new IllegalStateException("Unsupported TemporalAccessor type: " + this.temporalAccessorType); } }
我发现JSR310的类型都是交给他解析的,然后它使用的就是默认的模版。
那怎么办?怎么替换成我们自己的时间模版?所以我找到了它注册的地方:
@UsesJava8 public class DateTimeFormatterRegistrar implements FormatterRegistrar {}
看看注册的模版:
@Override public void registerFormatters(FormatterRegistry registry) { DateTimeConverters.registerConverters(registry); DateTimeFormatter df = getFormatter(Type.DATE); DateTimeFormatter tf = getFormatter(Type.TIME); DateTimeFormatter dtf = getFormatter(Type.DATE_TIME); // Efficient ISO_LOCAL_* variants for printing since they are twice as fast... registry.addFormatterForFieldType(LocalDate.class, new TemporalAccessorPrinter( df == DateTimeFormatter.ISO_DATE ? DateTimeFormatter.ISO_LOCAL_DATE : df), new TemporalAccessorParser(LocalDate.class, df)); registry.addFormatterForFieldType(LocalTime.class, new TemporalAccessorPrinter( tf == DateTimeFormatter.ISO_TIME ? DateTimeFormatter.ISO_LOCAL_TIME : tf), new TemporalAccessorParser(LocalTime.class, tf)); registry.addFormatterForFieldType(LocalDateTime.class, new TemporalAccessorPrinter( dtf == DateTimeFormatter.ISO_DATE_TIME ? DateTimeFormatter.ISO_LOCAL_DATE_TIME : dtf), new TemporalAccessorParser(LocalDateTime.class, dtf)); registry.addFormatterForFieldType(ZonedDateTime.class, new TemporalAccessorPrinter(dtf), new TemporalAccessorParser(ZonedDateTime.class, dtf)); registry.addFormatterForFieldType(OffsetDateTime.class, new TemporalAccessorPrinter(dtf), new TemporalAccessorParser(OffsetDateTime.class, dtf)); registry.addFormatterForFieldType(OffsetTime.class, new TemporalAccessorPrinter(tf), new TemporalAccessorParser(OffsetTime.class, tf)); registry.addFormatterForFieldType(Instant.class, new InstantFormatter()); registry.addFormatterForFieldType(Period.class, new PeriodFormatter()); registry.addFormatterForFieldType(Duration.class, new DurationFormatter()); registry.addFormatterForFieldType(YearMonth.class, new YearMonthFormatter()); registry.addFormatterForFieldType(MonthDay.class, new MonthDayFormatter()); registry.addFormatterForFieldAnnotation(new Jsr310DateTimeFormatAnnotationFormatterFactory()); }
这就无需多余解释了,都是采用的ISO标准模版。还好他给我们提供了对应的set方法,因此我想到了自定义
注册的地方DefaultFormattingConversionService:addDefaultFormatters
public static void addDefaultFormatters(FormatterRegistry formatterRegistry) { // Default handling of number values formatterRegistry.addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory()); // Default handling of monetary values if (jsr354Present) { formatterRegistry.addFormatter(new CurrencyUnitFormatter()); formatterRegistry.addFormatter(new MonetaryAmountFormatter()); formatterRegistry.addFormatterForFieldAnnotation(new Jsr354NumberFormatAnnotationFormatterFactory()); } // Default handling of date-time values if (jsr310Present) { // just handling JSR-310 specific date and time types new DateTimeFormatterRegistrar().registerFormatters(formatterRegistry); } if (jodaTimePresent) { // handles Joda-specific types as well as Date, Calendar, Long new JodaTimeFormatterRegistrar().registerFormatters(formatterRegistry); } else { // regular DateFormat-based Date, Calendar, Long converters new DateFormatterRegistrar().registerFormatters(formatterRegistry); } }
发现是new出来的,因此我们还不能直接从容器里面注入。确实不太好弄了。。。。
还好,经过我最终的源码跟踪,发现他解析了@DateTimeFormat
注解,因此我试试用了这个注解
@ApiOperation("测试接受时间类型Get") @PostMapping("/test/jsr310/get") Object testJsrGet(@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime localDateTime) { System.out.println(localDateTime); return localDateTime; }
bingo, 没毛病了,完美解决问题。
最后,我们发现。SpringMVC对body体里面的反序列化和对get请求参数的反序列化的机制是不一样的。因此大家使用的时候要倍加注意啊