MongoDB日期格式踩坑:从String到Date说起

·MongoDB ·SpringBoot ·时区

昨天,我在spring boot项目里使用mongoTemplate操作mongodb的comment集合。代码看起来一起正常,但当mongoTemplate从集合中获取到数据并尝试赋值给Comment.java类时抛出了一个异常:

Failed to convert from type [java.lang.String] to type [java.util.Date]
。这让我头疼不已。乍一看,应该是实体中时间这个字段,spring无法把它从字符串转为日期。我就纳闷了,这常见操作spring Data mongodb难道没有封装吗?于是我谷歌百度了一圈,打算设置自定义转换器来解决这个问题。

我debug了一圈,看到spring Data mongodb源代码确实没有string到date的直接转换器,但有string到LocalhostTime的转换啊,好吧,先不管了,我用以下代码成功添加了自定义转换器:
// /**
//  * @author BlueSky
//  * @date 2023/2/19
//  */
// @Configuration
// public class MongoConfig {
//     @Autowired
//     private MongoDatabaseFactory mongoDatabaseFactory;
//
//
//     /**
//      * 为MongoTemplate设置自定转换对象。
// * add DateTextStringToDateCustomConverter:解决日期字符串到java Date对象转换失败的问题 // * @return MongoCustomConversions自定义转换对象 // * @note 此MongoCustomConversions对象必须为bean,否则mongoTemplate.setCustomConversions方法添加抛出convert等异常 // */ // @Bean // public MongoCustomConversions customConversions() { // List> converters = new ArrayList<>(); // converters.add(DateTextStringToDateCustomConverter.INSTANCE); // return new MongoCustomConversions(converters); // } // // /** // * 获取配置后的MongoTemplate // * @return // */ // @Bean // public MongoTemplate mongoTemplate() { // MongoTemplate mongoTemplate = new MongoTemplate(mongoDatabaseFactory); // // MongoTemplate mongoTemplate = context.getBean(MongoTemplate.class); // MappingMongoConverter mongoMapping = (MappingMongoConverter) mongoTemplate.getConverter(); // mongoMapping.setCustomConversions(customConversions()); // // 最终调用,否则无法成功添加自定义转换器 // mongoMapping.afterPropertiesSet(); // return mongoTemplate; // // } // // /** // * 将日期字符串转换为日期对象的转换器 // */ // private enum DateTextStringToDateCustomConverter implements Converter { // INSTANCE; // @Override // public Date convert(@NotNull String source) { // Date date; // try { // date = TimeUtils.dateTextConvertToDate(source); // } catch (ParseException e) { // throw new RuntimeException(e); // } // return date; // } // } // }
其中的DateTextStringToDateCustomConverter实现了Converter接口,并且将STring转为Date返回,这很简单,我用我的TimeUtils封装工具类完成这个操作。
当我成功添加这个转换器后,再调用接口获取数据,这个时候就正常了,数据能够被解析并返回给前端。但当我插入的时候,又报了一个错:无法把"localhost"从字符串转为日期,这个提示很明显,“localhost”肯定不是正常的日期字符串,要转也转不了日期啊,可是“localhost”这个值对应的字段是客户端IP,本就是字符串类型,没错啊,mongoTemplate怎么会把它当做是日期去转换呢,这肯定不对。我一开始以为是我的条件Critical表达式写错了,如下:
CriteriaDefinition criteria = new Criteria().and("xxxIp").is(ip)
            .andOperator(
                Criteria.where("xxxTime").gt(nowDate),
                Criteria.where("xxxTime").lt(nextDate)
            );

        Integer cCount = this.commentRepository.conditionWhereCount(super.beanUtils.getMongoTemplate(), criteria, CoreComment.class);
对比了一圈,也都没错,打断点获取Query的mongo命令,应该是正确的。但还是这个问题抛出,无暇思索时,我用DataGrid查看集合字段类型,发现评论日期是字符串类型,我才明白,问题应该可以从这里入手。

原来我从mysql中迁移数据到mongo时,把日期类型直接用字符串代替了,这样不对,应该用new ISODate()来定义日期类型,注意不能是Date()函数,因为这会变为日期格式的字符串类型,仍然是字符串。所以我尝试用这段命令来批量转字符串为日期:
db.collectionName.find().forEach(function(data) {
    db.collectionName.updateOne({_id:data._id},{$set:{xxxTime:new Date(data.xxxTime)}});
});
并且删除掉我一开始折腾设置的自定义转换器,然后重新运行,这个时候查询数据以及插入数据,都不会再抛出字符串日期异常了,真是绕了一大圈浪费时间!

但是还有一个问题出现,就是明明在Java代码中得到的是CST北京时间,但是插入到mongodb里头,显示的时间是GMT也就是ISO标准时间,这会少了8小时,这怎么好呢?其实这个问题并不是问题,你要所是问题,那它也算是问题。我认为不是什么大问题,为什么呢?因为mongo数据库存进去的时候,会自动转为ISO标准时间,这个ISO时间基和格林威治时间GMT一样的,这对于不同时区的用户进行换算,是很方便的。而且我们在业务代码里头,甚至不需要去换算,因为MongoTempale获取到时间时,会自动按计算机的本地时间进行换算(这里是CST北京时间)。但是如果springMVC接口返回json给客户端时,又会被转为ISO时间,因此最简单有效的就是采用格式化注解(Jackson库):
   @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    /*
    * 设置时间格式,返回前端东八区北京时间。mongodb存储时间date为ISO,java实体获取按本地时间处理,
    因此内部仍是正确的本地时间(东八区)。但返回json时,仍会显示ISO格式,所以指定此@JsonFormat
     */
    private Date xxxTime;

@JsonFormat指定格式化日期字符串,并设置时区为东八区。如果想在Mongodb里直接存储本地时间,可以自己写逻辑+8h。参考文章可以看这篇博客:Monodb日期存储差8小时分析与解决,同时引出时间分析
另外,不建议在java中使用Date类库,因为SimpleDateFormat是线程不安全的,JDK1.8早已引入LocalDateTime类库,替换了过时的Date,并且API更加友好。

来自:Java
更新于2023-02-22 20:23:47 发表于2023-02-21 00:19:13


发表您的评论





公元2024年甲辰龍年,平安健康、龍行天下!