使用Jackson将java持续时间序列化/反序列化为JSON,格式为自定义hh:mm

ippsafx7  于 2023-01-10  发布在  Java
关注(0)|答案(1)|浏览(145)

说明

我是Java和Jackson的新手,我尝试将java.time.duration保存到JSON中,格式为漂亮且可读的hh:mm(小时:分钟),以便存储和检索。
在我的项目中,我使用:

  • Jacksoncom.fasterxml.jackson.core:jackson-databind:2.14.1
  • Jackson com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.14.1,用于支持较新的Java 8时间/日期类。

最小工作示例:

考虑以下示例类:

public class Book {

    private Duration timeToComplete;

    public Book(Duration durationToComplete) {
        this.timeToComplete = durationToComplete;
    }

    // default constructor + getter & setter
}

如果我尝试将book示例序列化为JSON,如以下代码部分所示

public class JavaToJson throws JsonProcessingException {

    public static void main(String[] args) {
        
        // create the instance of Book, duration 01h:11min
        LocalTime startTime = LocalTime.of(13,30);
        LocalTime endTime = LocalTime.of(14,41);
        Book firstBook = new Book(Duration.between(startTime, endTime));

        // create the mapper, add the java8 time support module and enable pretty parsing
        ObjectMapper objectMapper = JsonMapper.builder()
                .addModule(new JavaTimeModule())
                .build()
                .enable(SerializationFeature.INDENT_OUTPUT);

        // serialize and print to console
        System.out.println(objectMapper.writeValueAsString(firstBook));
    }

}

它给出的持续时间单位是秒而不是01:11

{
  "timeToComplete" : 4740.000000000
}

如何将JSON输出更改为hh:mm格式?

直到现在我所尝试的

我考虑过在ObjectMapper的示例化过程中添加一个自定义的串行化器/反串行化器(可能是DurationSerializer?),但似乎我无法使格式工作...

ObjectMapper objectMapper = JsonMapper.builder()
                .addModule(new JavaTimeModule())

                // add the custom serializer for the duration
                .addModule(new SimpleModule().addSerializer(new DurationSerializer(){
                    
                    @Override
                    protected DurationSerializer withFormat(Boolean useTimestamp, DateTimeFormatter dtf, JsonFormat.Shape shape) {
                    // here I try to change the formatting
                    DateTimeFormatter dtf = DateTimeFormatter.ofPattern("HH:mm");
                        return super.withFormat(useTimestamp, dtf, shape);
                    }
                }))
                .build()
                .enable(SerializationFeature.INDENT_OUTPUT);

它所做的只是将其更改为Duration的以下奇怪文本表示:

{
  "timeToComplete" : "PT1H11M"
}

所以看起来我还没有完全关闭,但是格式仍然没有。也许有人可以帮助序列化/反序列化?
多谢了

zxlwwiss

zxlwwiss1#

Jackson不支持hh:mm,因为默认情况下Java不识别该格式。我们需要自定义序列化/反序列化机制并提供自定义实现。
请看:

使用链接文章中的一些例子,我创建了自定义的序列化器和反序列化器。它们不能处理所有可能的情况,但应该能满足您的需求:

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.DurationDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.DurationSerializer;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DurationFormatUtils;

import java.io.IOException;
import java.time.Duration;
import java.time.LocalTime;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

public class DurationApp {
    public static void main(String[] args) throws JsonProcessingException {
        LocalTime startTime = LocalTime.of(13, 30);
        LocalTime endTime = LocalTime.of(14, 41);

        Book firstBook = new Book(Duration.between(startTime, endTime));

        // create the mapper, add the java8 time support module and enable pretty parsing
        ObjectMapper objectMapper = JsonMapper.builder()
                .addModule(new JavaTimeModule())
                .addModule(new SimpleModule()
                        .addSerializer(Duration.class, new ApacheDurationSerializer())
                        .addDeserializer(Duration.class, new ApacheDurationDeserializer()))
                .build()
                .enable(SerializationFeature.INDENT_OUTPUT);

        String json = objectMapper.writeValueAsString(firstBook);
        System.out.println(json);
        Book deserialisedBook = objectMapper.readValue(json, Book.class);
        System.out.println(deserialisedBook);
    }
}

@Data
@NoArgsConstructor
@AllArgsConstructor
class Book {
    @JsonFormat(pattern = "HH:mm")
    private Duration duration;
}

class ApacheDurationSerializer extends DurationSerializer {

    private final String apachePattern;

    public ApacheDurationSerializer() {
        this(null);
    }

    ApacheDurationSerializer(String apachePattern) {
        this.apachePattern = apachePattern;
    }

    @Override
    public void serialize(Duration duration, JsonGenerator generator, SerializerProvider provider) throws IOException {
        if (Objects.nonNull(apachePattern) && Objects.nonNull(duration)) {
            String value = DurationFormatUtils.formatDuration(duration.toMillis(), apachePattern);

            generator.writeString(value);
        } else {
            super.serialize(duration, generator, provider);
        }
    }

    @Override
    public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
        JsonFormat.Value format = findFormatOverrides(prov, property, handledType());
        if (format != null && format.hasPattern() && isApacheDurationPattern(format.getPattern())) {
            return new ApacheDurationSerializer(format.getPattern());
        }

        return super.createContextual(prov, property);
    }

    private boolean isApacheDurationPattern(String pattern) {
        try {
            DurationFormatUtils.formatDuration(Duration.ofDays(1).toMillis(), pattern);
            return true;
        } catch (Exception e) {
            return false;
        }
    }
}

class ApacheDurationDeserializer extends DurationDeserializer {
    private final String apachePattern;
    private final int numberOfColonsInPattern;

    public ApacheDurationDeserializer() {
        this(null);
    }

    ApacheDurationDeserializer(String apachePattern) {
        this.apachePattern = apachePattern;
        this.numberOfColonsInPattern = countColons(apachePattern);
    }

    @Override
    public Duration deserialize(JsonParser parser, DeserializationContext context) throws IOException {
        if (Objects.nonNull(apachePattern)) {
            String value = parser.getText();
            if (this.numberOfColonsInPattern != countColons(value)) {
                throw new JsonMappingException(parser, String.format("Pattern '%s' does not match value '%s'!", apachePattern, value));
            }
            if (numberOfColonsInPattern == 0) {
                return Duration.ofSeconds(Long.parseLong(value.trim()));
            }
            String[] parts = value.trim().split(":");
            return switch (parts.length) {
                case 1 -> Duration.ofSeconds(Long.parseLong(value.trim()));
                case 2 -> Duration.ofSeconds(TimeUnit.HOURS.toSeconds(Long.parseLong(parts[0]))
                        + TimeUnit.MINUTES.toSeconds(Long.parseLong(parts[1])));
                case 3 -> Duration.ofSeconds(TimeUnit.HOURS.toSeconds(Long.parseLong(parts[0]))
                        + TimeUnit.MINUTES.toSeconds(Long.parseLong(parts[1]))
                        + Long.parseLong(parts[2]));
                default ->
                        throw new JsonMappingException(parser, String.format("Pattern '%s' does not match value '%s'!", apachePattern, value));
            };
        } else {
            return super.deserialize(parser, context);
        }
    }

    @Override
    public Duration deserialize(JsonParser p, DeserializationContext ctxt, Duration intoValue) throws IOException {
        return super.deserialize(p, ctxt, intoValue);
    }

    @Override
    public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException {
        JsonFormat.Value format = findFormatOverrides(ctxt, property, handledType());
        if (format != null && format.hasPattern() && isApacheDurationPattern(format.getPattern())) {
            return new ApacheDurationDeserializer(format.getPattern());
        }

        return super.createContextual(ctxt, property);
    }

    private boolean isApacheDurationPattern(String pattern) {
        try {
            DurationFormatUtils.formatDuration(Duration.ofDays(1).toMillis(), pattern);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    private static int countColons(String apachePattern) {
        return StringUtils.countMatches(apachePattern, ':');
    }
}

以上代码打印:

{
  "duration" : "01:11"
}
Book(duration=PT1H11M)

相关问题