LocalTime的序列化和存储

By | 2021年12月31日

1 使用转换器读取与存储LocalTime

java.time里有一个 LocalTime,可用来表示时分秒,如:

#02:10:30
LocalTime localTime1 = LocalTime.parse("02:30:40");
LocalTime localTime2 =LocalTime.of(2, 30, 40)

但我们不能用这个字段来建模,因为存储到数据库时会变成"startingDailyAt" : ISODate("2018-12-26T02:30:40.000+0000"),自动加上年月日了。如果非要使用 LocalTime来作为我们模型的字段类型,且要将它的值按字符串02:30:40格式存储到数据库,该如何处理呢?

这里我们讲解基于 SpringData的做法,在SpringData中,我们可以使用贴有@ReadingConverter和@WritingConverter的转换器来进行自定义类型处理,他们分别表示数据库到模型和模型到数据库的数据转换。废话少说,先把模型贴出来:

@Document( collection = "SCHEDULE_OBJECT")
public class ScheduleObject extends AuditBaseEntity implements UuidHolder {
    @Indexed(unique = true)
    @NotBlank
    private String name;

    @NotNull
    private SystemUnitEnum owner;

    private LocalTime startingDailyAt;
    ......
}

模型中的 startingDailyAt 字段就是 LocalTime 类型,下面我们就用它来实现一把上面说的两个转换器。

1.1 使用@ReadingConverter读取数据库中的字符串值,并转成模型中的LocalTime

LocalTimeReadConverter.class

import org.bson.Document;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.ReadingConverter;
import org.springframework.util.StringUtils;

import java.time.LocalTime;

@ReadingConverter
public class LocalTimeReadConverter implements Converter<String, LocalTime> {

  @Override
  public LocalTime convert(String source) {
    if (source == null) return null;

    return LocalTime.parse(source);
  }
}

注:
必须满足数据库中的类型为 string,并且实体的类型为 LocalTime时,才会被触发
如果数据库中的类型为嵌套对象,这里要用Document作为source(未测试)。

1.2 使用@WritingConverter将模型中LocalTime字段值存储到数据库中

LocalTimeWriteConverter.class

import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.WritingConverter;

import java.time.LocalTime;

@WritingConverter
public class LocalTimeWriteConverter implements Converter< LocalTime,String> {

  @Override
  public String convert(LocalTime source) {
    if (source == null) return null;

    return source.toString();
  }
}

1.3 使转换器生效,并测试

FalSpringMongoConfig.class

@Configuration
@AutoConfigureBefore(SpringMongoConfig.class)
@EnableMongoAuditing(auditorAwareRef = "userAuditor")
@EnableMongoRepositories(basePackages = { "com.falsec.pom.policy.repository"}, repositoryBaseClass = FalRepositoryImpl.class)
@ConfigurationProperties(prefix = "spring.data.mongodb.data")
public class FalSpringMongoConfig extends AbstractMongoConfiguration {
    private String host, database;
    private int port;

    @Bean
    @ConditionalOnMissingBean
    public MongoClient mongoClient() {
        MongoClientOptions opt = MongoClientOptions.builder().writeConcern(WriteConcern.JOURNALED).build();
        System.out.println( ">>> mongo database: " + database );
        return new MongoClient(new ServerAddress(host, port), opt);
    }

    @Primary
    @Bean
    @ConditionalOnSingleCandidate
    public MongoTemplate mongoTemplate() throws Exception {
        MongoTemplate my = super.mongoTemplate();
        my.setWriteResultChecking(WriteResultChecking.EXCEPTION);
        return my;
    }

    @Bean
    public MappingMongoConverter mappingMongoConverter() throws Exception {
        DefaultDbRefResolver dbRefResolver = new DefaultDbRefResolver(this.mongoDbFactory());
        MappingMongoConverter converter = new MappingMongoConverter(dbRefResolver, this.mongoMappingContext());
        List<Converter<?, ?>> list = new ArrayList<>();

        list.add(new LocalTimeReadConverter());
        list.add(new LocalTimeWriteConverter());

        converter.setCustomConversions(new MongoCustomConversions(list));
        converter.setMapKeyDotReplacement("\\[dot\\]");

        return converter;
    }
}

测试OK

// 插入一条数据
LocalTime localTime = LocalTime.parse("02:30:40");
ScheduleObject obj = ScheduleObject.createDailySchedule("每隔1小时执行", localTime, new Date(), null, SystemUnitEnum.FAL_POS);
posScheduleObjectRepository.save(obj);

//读取插入的数据
obj = posScheduleObjectRepository.findAll().get(0);

查看Mongodb中已经存储的数据

{ 
    "_id" : ObjectId("5c2467595e7f2f3a3074713a"), 
    "name" : "每隔1小时执行", 
    "uuid" : "958137ca07574ff1af7401615b8b4fbd", 
    "owner" : "FAL_POS", 
    "recurrence" : "DAILY", 
    "startingDailyAt" : "02:11:20", 
    "startTime" : ISODate("2018-12-27T05:47:02.817+0000"), 
    "endTime" : ISODate("2999-12-30T17:00:00.000+0000"), 
    "createdTime" : ISODate("2018-12-27T05:47:05.470+0000"), 
    "lastModifiedTime" : ISODate("2018-12-27T05:47:05.470+0000"), 
    "version" : NumberLong(0), 
    "_class" : "com.falsec.nsm.common.domain.ScheduleObject"
}

2 使用自定义Json序列化器实现LocalTime的序列化与反序列化

上面讲的是如何将LocalTime存储到数据库,并从数据库中恢复数据到模型。这只是后端到数据库的处理过程,那么要想完整处理LocalTime,还需要实现前端到后端的处理。
接下来我们就将下前端表单提交上来的 10:20:00 字符串格式,如何反序列化成LocalTime,以及如何将 LocalTime序列化成字符串 10:20:00 返回到前端。

2.1 LocalTime序列化

默认情况下,jackson序列化带有LocalTime类型字段的实体时,由于LocalTime也是个对象,因此会把它序列化成单独的json,下面的 startingDailyAt属性就是LocalTime类型:

{
    "id" : "5c258dd3cd8c7b2938688eb4",
    "version" : 0,
    "name" : "test",
    "recurrence" : "每天",
    "startTime" : "2018-12-28T11:00:00.000+0800",
    "endTime" : "2018-12-28T12:00:00.000+0800",
    "startingDailyAt" : {
        "hour" : 10,
        "minute" : 20,
        "second" : 0,
        "nano" : 0
    }
}

实际上,我们希望返回 "startingDailyAt":"10:20:00" 这种格式。

2.1.1 通过@JsonSerialize(using = MyLocalTimeSerializer.class)—推荐

public class FalLocalTimeSerializer extends JsonSerializer<LocalTime> {

    @Override
    public void serialize(LocalTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        //String fieldName = gen.getOutputContext().getCurrentName();
        gen.writeString(value.toString());
    }
}

必须在DTO字段上添加注解:

@JsonSerialize(using = LocalTimeSerializer.class)
@JsonDeserialize(using = LocalTimeDeserializer.class)
private LocalTime startingDailyAt;

返回到前端格式:

{
    "startingDailyAt" : "10:20"
}

这里顺便说一下,自定义序列化时如果需要返回的startingDailyAt值是对象,就需要使用 gen.writeStartObject()、gen.writeEndObject(),数组同理。

2.2 LocalTime反序列化

2.2.1 通过@JsonDeserialize(using = MyLocalTimeDeserializer.class)—推荐

public class LocalTimeDeserializer extends JsonDeserializer<LocalTime> {
    @Override
    public LocalTime deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
        String value = jp.getText();
        if (StringUtils.isBlank(value)) return null;

        return LocalTime.parse(value);
    }
}

2.2.2 通过@JsonCreator实现

如果DTO的字段类型是LocalTime,我们使用 @JsonCreator 可以将前端提交上来的 10:20:00字符串转换成LocalTime类型的数据,如下:

@JsonCreator
public ScheduleObjectDto(@JsonProperty("startingDailyAt") String startingDailyAt) {
    if (!StringUtils.isEmpty(startingDailyAt)) {
        this.startingDailyAt = LocalTime.parse(startingDailyAt);
    }
}

使用@JsonCreator时,我们仅提供一个 @JsonProperty(“startingDailyAt”)参数,意思是说反序列化时,json中的这个属性要用我这里的代码来处理,其他属性如 startTime、endTime等不用这个代码处理,而是和平常一样处理,即其他属性也会反序列化的。

发表评论

您的电子邮箱地址不会被公开。