2019年9月10日 星期二

[Java] Jackson binding Date , Instant

Jackson 可以對 Date 綁定,缺點是 Json 用了毫秒。由於一般儲存到資料庫都是用 Unix 時間戳,計算單位是秒,這時候其實不適用。

// Foo.java
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.Data;
@Data
class Foo {
   @JsonProperty("create_time")
   private Date createTime;

   public static void main(String[] args) throws Throwable {
      Foo foo = new Foo();
      foo.setCreateTime(new Date());

      ObjectMapper mapper = new ObjectMapper();
      String s = mapper.writeValueAsString(foo); // {"create_time":1568024055895}

      Foo foo2 = mapper.readValue(s, Foo.class); // Foo(createTime=Mon Sep 09 18:14:15 CST 2019)
      foo.equals(foo2); // true
   }
}

所以需要自己寫轉換類。

// Foo.java
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import lombok.Data;
@Data
class Foo {
    @JsonProperty("create_time")
    @JsonDeserialize(using = DefaultDateDeserializer.class)
    @JsonSerialize(using = DefaultDateSerializer.class)
    private Date createTime;
}

// DefaultDateDeserializer.java
import java.io.IOException;
import java.util.Date;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
class DefaultDateDeserializer extends JsonDeserializer {
    @Override
    public Date deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        return new Date(p.getLongValue() * 1000);
    }
}

// DefaultDateSerializer.java
import java.io.IOException;
import java.util.Date;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
class DefaultDateSerializer extends JsonSerializer {
    @Override
    public void serialize(Date value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        gen.writeNumber(value.toInstant().getEpochSecond());
    }
}

這時候的結果如下,會發現 Json 是我們要的,但是最後是 false。 原因是創建 Date 實例含有毫秒,不過轉到 Json 的時候我們去掉了毫秒; 從 Json 轉回 Date 則損失了這些毫秒,所以兩者才會不相等。

{"create_time":1568024055}
Foo(createTime=Mon Sep 09 18:14:15 CST 2019)
false

那麼接著要把 Date 改成 Instant,因為 Date 很多方法都棄用了,Instant 有很多方法比較好用,而且轉成 ISO8601 或是 epoch timestamp 比較方便。

// build.gradle
implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.9.9"

// Foo.java
import java.time.Instant;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import lombok.Data;
@Data
class Foo {
    @JsonProperty("create_time")
    @JsonDeserialize(using = DefaultInstantDeserializer.class)
    @JsonSerialize(using = DefaultInstantSerializer.class)
    private Instant createTime;

   public static void main(String[] args) throws Throwable {
      Foo foo = new Foo();
      foo.setCreateTime(Instant.now());

      ObjectMapper mapper = new ObjectMapper();
      String s = mapper.writeValueAsString(foo); // {"create_time":1568024055}

      Foo foo2 = mapper.readValue(s, Foo.class); // Foo(createTime=2019-09-09T10:14:15Z)
      foo.equals(foo2); // true
   }
}

// DefaultInstantDeserializer.java
import java.io.IOException;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.Objects;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.datatype.jsr310.deser.InstantDeserializer;
@SuppressWarnings("serial")
class DefaultInstantDeserializer extends InstantDeserializer {
    public DefaultInstantDeserializer() {
        super(Instant.class,
                DateTimeFormatter.ISO_INSTANT,
                Instant::from,
                a -> Instant.ofEpochMilli(a.value),
                a -> Instant.ofEpochSecond(a.integer, a.fraction),
                null,
                true
        );
    }

    @Override
    public Instant deserialize(JsonParser parser, DeserializationContext context) throws IOException {
        if (Objects.isNull(parser.getText()) || parser.getValueAsLong() == 0) {
            return null;
        }
        return super.deserialize(parser, context);
    }
}

// DefaultInstantSerializer.java
import java.io.IOException;
import java.time.Instant;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
class DefaultInstantSerializer extends InstantDeserializer {
    @Override
    public void serialize(Instant value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        gen.writeNumber(value.getEpochSecond());
    }
}

DefaultInstantSerializer 繼承 InstantDeserializer 是因為這樣可以接受多種格式,例如 1568024055.000000000。

參考資料:

[Java] Invalid HTTP method: PATCH

最近系統需要使用 Netty4,所以把衝突的 Netty3 拆掉,然後就出現了例外。 pom.xml <dependency> <groupId>com.ning</groupId> <artifactId>as...