IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> 从零开始 Spring Boot 24:处理时间 -> 正文阅读

[Java知识库]从零开始 Spring Boot 24:处理时间

从零开始 Spring Boot 24:处理时间

spring boot

图源:简书 (jianshu.com)

本文示例基于从零开始 Spring Boot 23:MyBatis - 红茶的个人站点 (icexmoon.cn)的最终示例代码修改而来,可以从learn_spring_boot/ch23 (github.com)获取完整示例。

从零开始 Spring Boot 16:枚举 - 红茶的个人站点 (icexmoon.cn)中我详细说明了如何在Spring Boot项目中处理枚举类型,其中包含在接口的输入和输出阶段处理枚举,除了枚举以外,通常我们还需要处理时间类型,具体来说就是标准类库中的LocalDateTimeLocalDate类。

LocalDateTimeLocalDate是JDK8引入的时间类,相比DateDateTime,它们本身包含了时区概念,不需要额外处理时区的问题,而且它们的相关格式化处理函数都是线程安全的。所以Java程序中的时间都应该使用这两种类型来处理。

一般的,我们会在在VO和DTO类中将时间相关属性定义为字符串形式,并借助工具函数进行转换,比如:

package cn.icexmoon.books2.book.entity.dto;
// ...
@Data
public class CouponDTO {
    private Integer addUserId;
    private Double amount;
    private String expireTime;
    private Double enoughAmount;
    private CouponType type;
}
package cn.icexmoon.books2.book.service.impl;
// ...
@Service
public class CouponServiceImpl implements CouponService {
    @Autowired
    private CouponMapper couponMapper;

    @Override
    public Coupon getCouponById(int id) {
        return couponMapper.getCouponById(id);
    }

    @Override
    public int addCoupon(CouponDTO dto) {
        Coupon coupon;
        switch (dto.getType()) {
            case FREE_COUPON:
                coupon = new FreeCoupon();
                break;
            case ENOUGH_COUPON:
                coupon = new EnoughCoupon()
                        .setEnoughAmount(dto.getEnoughAmount());
                break;
            default:
                throw new RuntimeException("不正确的优惠券类型");
        }
        coupon.setAddTime(LocalDateTime.now())
                .setAddUserId(dto.getAddUserId())
                .setAmount(dto.getAmount())
                .setExpireTime(MyTimeUtil.convert2DateTime(dto.getExpireTime()))
                .setType(dto.getType());
        couponMapper.addCoupon(coupon);
        return coupon.getId();
    }
}

因为DTO中时间是字符串,所以这里需要通过工具类转换:

MyTimeUtil.convert2DateTime(dto.getExpireTime())

相应的时间工具函数:

package cn.icexmoon.books2.system.util;
// ...
public class MyTimeUtil {
    private static DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    private static DateTimeFormatter dayFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
	// ...

    /**
     * 将时间字符串转换为LocalDateTime
     *
     * @param time 时间字符串
     * @return
     */
    public static LocalDateTime convert2DateTime(String time) {
        return LocalDateTime.parse(time, timeFormatter);
    }
	// ...
}

当然,在Entity类中时间是LocalDateTime,在持久层MyBatis可以正常处理这种类型的读写,无需我们做额外处理:

package cn.icexmoon.books2.book.entity;
// ...
@Data
@Accessors(chain = true)
public class Coupon {
    private Integer id;
    private Integer addUserId;
    private LocalDateTime addTime;
    private LocalDateTime expireTime;
    private CouponType type;
    private Double amount;
}

@JsonFormat

虽然这样做也没什么太大问题,但需要额外的类型处理依然不是很方便,实际上我们可以借助Jackson在HTTP request body转换为对象时就可以生成正确的时间类型:

package cn.icexmoon.books2.book.entity.dto;
// ...
@Data
public class CouponDTO {
    private Integer addUserId;
    private Double amount;
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private LocalDateTime expireTime;
    private Double enoughAmount;
    private CouponType type;
}

注解@JsonFormat可以让时间类型的属性正确从JSON中解析出来或者解析成JSON。

其中shape属性指定的是JSON中的原始类型,pattern是时间模式,timezone是时区。

绝大多数情况原始类型都是String,模式是yyyy-MM-dd HH:mm:ss,时区是东八区,即:

@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")

在实际使用中,时间相应的入参都是String,且时区使用默认值即可,所以可以简写为:

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")

@DateTimeFormat

对于复杂传参,一般推荐用JSON作为请求报文体传递,不推荐使用查询字符串,因为基于HTTP协议规范,后者有长度限制,且可能被URL编码。但如果通过后者传递时间参数,服务端如何处理?

package cn.icexmoon.books2.book.controller;
// ...
@RestController
@RequestMapping("/book/coupon")
public class CouponController {
    // ...

    @PostMapping("/params-add")
    Result addCouponWithParams(@RequestParam Integer addUserId,
                               @RequestParam Double amount,
                               @RequestParam LocalDateTime expireTime,
                               @RequestParam Double enoughAmount,
                               @RequestParam CouponType type){
        CouponDTO dto = new CouponDTO()
                .setAddUserId(addUserId)
                .setAmount(amount)
                .setExpireTime(expireTime)
                .setEnoughAmount(enoughAmount)
                .setType(type);
        return Result.success(couponService.addCoupon(dto));
    }
}

这里处理器方法addCouponWithParams以查询字符串方式接受入参,并且其中有一个时间类型的参数expireTime

默认配置下的Spring Boot不能正常进行类型转换,会报错:

org.springframework.web.method.annotation.MethodArgumentTypeMismatchException: Failed to convert value of type 'java.lang.String' to required type 'java.time.LocalDateTime';

这个问题可以通过使用@DateTimeFormat注解来解决,该注解是Spring的一个注解。

package cn.icexmoon.books2.book.controller;

// ...
@RestController
@RequestMapping("/book/coupon")
public class CouponController {
    // ...

    @PostMapping("/params-add")
    Result addCouponWithParams(@RequestParam Integer addUserId,
                               @RequestParam Double amount,
                               @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
                               @RequestParam LocalDateTime expireTime,
                               @RequestParam Double enoughAmount,
                               @RequestParam CouponType type){
        // ...
    }
}

@JsonFormat注解类似,通过pattern属性为@DateTimeFormat注解指定一个时间模式即可让框架正确地将查询字符串中的入参转换为时间类型。

入参中还包含枚举类型CouponType,这需要一些额外处理,详情可以阅读从零开始 Spring Boot 16:枚举 - 红茶的个人站点 (icexmoon.cn)

修改默认配置

上面的两种方式相结合,已经可以解决问题,但是需要在项目中添加大量的注解。如果是一个现有项目,可能这样做是合适的,因为修改默认配置可能会引发一些未知的bug。但如果是一个新项目,完全可以通过修改Jackson的默认配置来实现这一点,进而避免添加大量的注解。

可以通过注入一个Jackson2ObjectMapperBuilderCustomizer类型的JavaBean来修改默认的Jackson的解析行为,在应用启动时,Jackson会加载所有类型为Jackson2ObjectMapperBuilderCustomizer的Bean,然后通过其customer方法对用于解析的相关核心组件进行设置。

package cn.icexmoon.books2.system;
// ...
@Configuration
public class MyJacksonConfig {
    @Bean
    @Order(1)
    public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilder() {
        return jacksonObjectMapperBuilder -> {
            //针对于Date类型,文本格式化
            jacksonObjectMapperBuilder.simpleDateFormat("yyyy-MM-dd HH:mm:ss");

            //针对于JDK新时间类。序列化时带有T的问题,自定义格式化字符串
            JavaTimeModule javaTimeModule = new JavaTimeModule();
            javaTimeModule.addSerializer(LocalDateTime.class,new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
            javaTimeModule.addDeserializer(LocalDateTime.class,new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
            jacksonObjectMapperBuilder.modules(javaTimeModule);
        };
    }
}

为了确保自定义的Jackson2ObjectMapperBuilderCustomizer在系统自动生成的Bean之后注入,这里指定其顺序@Order(1)(系统自定义的顺序为0)。

这样设置好后就可以正确处理LocalDateTime类型的JSON解析和编码。

以上这种方式是Spring Boot推荐的在不破坏自动配置机制的情况下修改Jackson编码行为的方式,如果不起作用,可以检查下你搭的应用是否屏蔽了自动配置机制,比如我的示例中就因为在添加Converter时采用以下方式引入配置类:

@Configuration
public class MyWebAppConfigurer extends WebMvcConfigurationSupport {
	// ...
}

导致了自动配置功能被屏蔽,进而导致上边修改Jackson配置的代码不起作用。

上边修改Jackson配置的示例使用了给jacksonObjectMapperBuilder添加Model的方式,这是Jackson官方推荐的方式,除此以外,也可以直接按照待处理类型来添加解析器和编码器:

package cn.icexmoon.books2.system;
// ...
@Configuration
public class MyJacksonConfig {
    @Bean
    @Order(1)
    public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilder() {
        return jacksonObjectMapperBuilder -> {
            //针对于Date类型,文本格式化
            jacksonObjectMapperBuilder.simpleDateFormat("yyyy-MM-dd HH:mm:ss");

            //针对于JDK新时间类。序列化时带有T的问题,自定义格式化字符串
            jacksonObjectMapperBuilder.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
            jacksonObjectMapperBuilder.deserializerByType(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        };
    }
}

这两种方式效果是相同的。

如果想通过jacksonObjectMapperBuilder修改Jackson的其它配置,可以参考Jackson序列化(3)— Jackson中ObjectMapper配置详解 - 简书 (jianshu.com)

@JsonComponent

除了上边常规方式以外,Spring Boot本身还提供一个注解@JsonComponent,可以通过这个注解以更简单直观的方式给特定类型加上特殊的JSON编码/解码行为:

package cn.icexmoon.books2.system;
// ...
@JsonComponent
public class DateTimeJsonComponent {
    public static class Serializer extends JsonSerializer<LocalDateTime> {


        @Override
        public void serialize(LocalDateTime localDateTime, JsonGenerator jgen, SerializerProvider serializerProvider) throws IOException {
            jgen.writeString(MyTimeUtil.convert2timeStr(localDateTime));
        }
    }

    public static class Deserializer extends JsonDeserializer<LocalDateTime> {


        @Override
        public LocalDateTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JacksonException {
            return MyTimeUtil.convert2DateTime(jsonParser.getText());
        }
    }
}

谢谢阅读。

最终的完整示例代码可以从learn_spring_boot/ch24 (github.com)获取。

参考资料

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-10-31 11:40:47  更:2022-10-31 11:45:13 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/19 9:09:23-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码