【小家java】java8新特性之---全新的日期、时间API(JSR 310规范),附SpringMVC、Mybatis中使用JSR310的正确姿势(下)

简介: 【小家java】java8新特性之---全新的日期、时间API(JSR 310规范),附SpringMVC、Mybatis中使用JSR310的正确姿势(下)

获取一周、一个月、一年、一小时、一分钟后的日期等


LocalDate是用来表示无时间的日期,他又一个plus()方法可以用来增加日,星期,月,ChronoUnit则用来表示时间单位


image.png

表示和处理固定的日期,比如信用卡过期时间


YearMonth是另外一个组合,可以很好处理信用卡有效期只有年、月的问题。LengthOfMonth()这个方法返回的是这个YearMonth实例有多少天,这对于检查2月是否润2月很有用


image.png


两个日期之间包含多少天,多少月(这个非常实用)


计算两个日期之间包含多少天、周、月、年。可以用java.time.Period类完成该功能。下面例子中将计算日期与将来的日期之间一共有几个月


image.png


带时区的日期与时间(以后处理时区问题,还是用ZoneDateTime吧)


在java8中,可以使用ZoneOffset来代表某个时区,可以使用它的静态方法ZoneOffset.of()方法来获取对应的时区,只要获得了这个偏移量,就可以用这个偏移量和LocalDateTime创建一个新的OffsetDateTime


image.png


说明: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 足够你用的了,而且安全性更高


为何能够处理这些时间?看到下面截图一目了然:


image.png


导入之后: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请求参数的反序列化的机制是不一样的。因此大家使用的时候要倍加注意啊

相关文章
|
3天前
|
数据采集 存储 Java
Java爬虫获取微店店铺所有商品API接口设计与实现
本文介绍如何使用Java设计并实现一个爬虫程序,以获取微店店铺的所有商品信息。通过HttpClient发送HTTP请求,Jsoup解析HTML页面,提取商品名称、价格、图片链接等数据,并将其存储到本地文件或数据库中。文中详细描述了爬虫的设计思路、代码实现及注意事项,包括反爬虫机制、数据合法性和性能优化。此方法可帮助商家了解竞争对手,为消费者提供更全面的商品比较。
|
7天前
|
缓存 Java 应用服务中间件
java语言后台管理若依框架-登录提示404-接口异常-系统接口404异常如何处理-登录验证码不显示prod-api/captchaImage 404 (Not Found) 如何处理-解决方案优雅草卓伊凡
java语言后台管理若依框架-登录提示404-接口异常-系统接口404异常如何处理-登录验证码不显示prod-api/captchaImage 404 (Not Found) 如何处理-解决方案优雅草卓伊凡
35 5
|
30天前
|
算法 Java API
Java 方法注释:规范、实用和高质量的写法
本文深入探讨了如何编写高质量的 Java 方法注释
53 11
|
2月前
|
JSON Java Apache
Java基础-常用API-Object类
继承是面向对象编程的重要特性,允许从已有类派生新类。Java采用单继承机制,默认所有类继承自Object类。Object类提供了多个常用方法,如`clone()`用于复制对象,`equals()`判断对象是否相等,`hashCode()`计算哈希码,`toString()`返回对象的字符串表示,`wait()`、`notify()`和`notifyAll()`用于线程同步,`finalize()`在对象被垃圾回收时调用。掌握这些方法有助于更好地理解和使用Java中的对象行为。
|
2月前
|
存储 Java 开发者
什么是java的Compact Strings特性,什么情况下使用
Java 9引入了紧凑字符串特性,优化了字符串的内存使用。它通过将字符串从UTF-16字符数组改为字节数组存储,根据内容选择更节省内存的编码方式,通常能节省10%至15%的内存。
|
2月前
|
算法 Java API
如何使用Java开发获得淘宝商品描述API接口?
本文详细介绍如何使用Java开发调用淘宝商品描述API接口,涵盖从注册淘宝开放平台账号、阅读平台规则、创建应用并申请接口权限,到安装开发工具、配置开发环境、获取访问令牌,以及具体的Java代码实现和注意事项。通过遵循这些步骤,开发者可以高效地获取商品详情、描述及图片等信息,为项目和业务增添价值。
121 10
|
2月前
|
存储 Java 数据挖掘
Java 8 新特性之 Stream API:函数式编程风格的数据处理范式
Java 8 引入的 Stream API 提供了一种新的数据处理方式,支持函数式编程风格,能够高效、简洁地处理集合数据,实现过滤、映射、聚合等操作。
103 6
|
2月前
|
Java API 开发者
Java中的Lambda表达式与Stream API的协同作用
在本文中,我们将探讨Java 8引入的Lambda表达式和Stream API如何改变我们处理集合和数组的方式。Lambda表达式提供了一种简洁的方法来表达代码块,而Stream API则允许我们对数据流进行高级操作,如过滤、映射和归约。通过结合使用这两种技术,我们可以以声明式的方式编写更简洁、更易于理解和维护的代码。本文将介绍Lambda表达式和Stream API的基本概念,并通过示例展示它们在实际项目中的应用。
|
3月前
|
安全 Java API
Java中的Lambda表达式与Stream API的高效结合####
探索Java编程中Lambda表达式与Stream API如何携手并进,提升数据处理效率,实现代码简洁性与功能性的双重飞跃。 ####
44 0
|
3月前
|
安全 IDE Java
Java常见规范及易忘点
遵循Java编程规范和注意易忘点是提高代码质量和可维护性的关键。通过规范的命名、格式、注释和合理的代码组织,可以让代码更加清晰和易于维护。同时,注意空指针检查、线程安全、集合框架和字符串操作等常见易忘点,可以减少程序错误,提高运行效率。结合单一职责原则、面向接口编程和合理的异常处理,能够编写出高质量的Java代码。希望本文能够帮助Java开发者提升编码水平,写出更高效、更可靠的代码。
46 2

热门文章

最新文章