Spring et la JSR 310 (Date/Time)

Nov 10, 2017 · 625 words · 3 minutes read spring

Nous avons tous, rencontré un jour ou un autre, la problématique de stockage des dates et heures. L’API Date/Time de Java 8 nous a apporté (enfin) une API simple pour cela. Mais elle n’est pas prise en compte directement par Spring boot (1.X…​). Ici, je vais vous montrer comment stocker en base des champ avec un des type de l’API Date/Time de java 8, et de l’exposer au format JSON. Le tout avec 2 dépendances supplémentaire et une annotation.

Sérialisation JSON

L’état des lieux

Par défaut, Spring-boot version 1.5.8 (dernière release avant la version 2) embarque jackson grace à ces dependances :

mvn dependency:tree | grep jackson
[INFO] |  +- com.fasterxml.jackson.core:jackson-databind:jar:2.8.10:compile
[INFO] |  |  +- com.fasterxml.jackson.core:jackson-annotations:jar:2.8.0:compile
[INFO] |  |  \- com.fasterxml.jackson.core:jackson-core:jar:2.8.10:compile

Mais cette version de Jackson ne peut pas serialiser les dates, heure et timestamp de l’API Date/Time de Java 8 correctement, il renvoi la structure JSON de l’objet.

Prenons l’exemple d’un POJO embarquant une propriété de type LocalDateTime :

public class ToDoItem {

  private Long id;

  private String description;

  private LocalDateTime created;

}

Et voici sa sérialisation JSON obtenu depuis un endpoint au format JSON :

{
    "created": {
        "chronology": {
            "calendarType": "iso8601",
            "id": "ISO"
        },
        "dayOfMonth": 10,
        "dayOfWeek": "FRIDAY",
        "dayOfYear": 314,
        "hour": 23,
        "minute": 25,
        "month": "NOVEMBER",
        "monthValue": 11,
        "nano": 424000000,
        "second": 22,
        "year": 2017
    },
    "description": "Description de l'item 10",
    "id": 10
}

Nous avons bien notre date, mais jackson expose la sérialisation complete de l’objet LocalDateTime, c’est precis, mais, il faut en convenir pas très exploitable…​

Maitrisons un peu tout cela

Tout d’abord, je vais ajouter la dépendance Jackson qui permettra la possibilité de formatter les objets LocalDate, LocalTime et LocalDateTime de la JSR 310 :

<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
    <version>${jackson.version}</version>
</dependency>

La version de jackson est fournie par le pom parent de SpringBoot avec la propriété jackson.version.

Puis je vais annoter la propriété afin de definit le formattage de la date :

public class ToDoItem {
  ...
  @JsonFormat(
      shape = JsonFormat.Shape.STRING, // (1)
      pattern = "yyyyMMdd-HHmmSS" // (2)
  )
  private LocalDateTime created;
}
  1. type de la representation

  2. pattern de formattage de la date

Et voici le résultat de la sérialisation :

{
    "created": "20171110-233774",
    "description": "Description de l'item 10",
    "id": 10
}

C’est quand même un peu plus standard, non ?

Sérialisation DB

Comme pour la sérialisation JSON, l’API Date/Time n’est pas prise en compte directement par Hbernate (qui est l’implementation JPA proposée par Spring).

Pour preuve, voici la génération du schéma via hibernate (classique) :

drop table to_do_item if exists;
create table to_do_item (
    id bigint generated by default as identity (start with 1),
    created varbinary(255),
    description varchar(255), primary key (id)
);

Mon champ date/time (LocalDateTime) est interprété comme varbinary…​

Pour qu’hibernate prenne en compte cette problématique, il faut simplement activer la prise en charge de la JSR 310 via une dépendance particulière :

<dependency>
  <groupId>org.hibernate</groupId>
  <artifactId>hibernate-java8</artifactId>
</dependency>

Et maintenant la génération du schéma via hibernate est correcte :

drop table to_do_item if exists;
create table to_do_item (
    id bigint generated by default as identity (start with 1),
    created timestamp,
    description varchar(255), primary key (id)
);

Et cerise sur le gateau, plus la peine d’indiquer quelle type temporel nous attendons, hibernate l’interprete pour nous selon le type (de la JSR 310) du champs.

Exemples

Voici quelques exemple lié à l’execution des source afin de les tester.

Vous trouverez les sources sur mon repository : github.com::ptitbob/jsondateformat.

liste
curl -X GET http://localhost:8080/todo-items -H 'cache-control: no-cache' -H 'content-type: application/json'
création
curl -X POST http://localhost:8080/todo-items -H 'cache-control: no-cache' -H 'content-type: application/json' -d '{"description":"test"}' -i
consultation
curl -X GET http://localhost:8080/todo-items/1 -H 'cache-control: no-cache' -H 'content-type: application/json' -i
modification
curl -X PUT http://localhost:8080/todo-items/1 -H 'cache-control: no-cache' -H 'content-type: application/json' -i -d '{"id":1,"description":"test-22"}'
suppression
curl -X DELETE http://localhost:8080/todo-items/2 -H 'cache-control: no-cache' -H 'content-type: application/json' -i

have fun