Tutoriel sur l'exploitation de l'API Date and Time de Java 8

| Gérald Quintana (Zenika) | Zenika |

Ce tutoriel s'intéresse à décrire les bonnes pratiques quant à l'usage de la nouvelle API pour manipuler les dates et heures avec Java 8.

Pour réagir au contenu de cet article, un espace de dialogue vous est proposé sur le forum Commentez . ♪

- Titre : Tutoriel sur l'exploitation de l'API Date and Time de Java 8

- Auteurs : Gérald Quintana (Zenika), Zenika

- Parution : 16 mars 2015

- Licence : Copyright ® 2015 Zenika. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. Persistance avec Hibernate

- III. Persistance avec JDBC

- IV. Persistance avec Spring JDBC

- V. Sérialisation JSON

- VI. Sérialisation XML

- VII. Validation

- VIII. Conclusion

- IX. Remerciements

I - Introduction

Java 8 apporte une nouvelle API pour manipuler les dates et heures en Java, la Date and Time API, aussi connue comme JSR 310. L'objectif de cet article n'est pas de présenter cette API, mais de montrer ce que l'on doit faire aujourd'hui, pour l'intégrer avec les bibliothèques habituelles comme Spring ou Hibernate, et se débarrasser des java.util.Date et autres java.util.GregorianCalendar.

Si vous utilisez encore Java 7, cet article est probablement transposable sur Joda Time (qui a inspiré la JSR 310) ou sur le « backport » ThreeTen.

Pour commencer, on va persister en base une entité Utilisateur avec un attribut dateNaissance de type java.time.LocalDate. Au niveau de JDBC, cela signifie qu'il faudra être capable de convertir ce type depuis et vers une java.sql.Date.

II - Persistance avec Hibernate

Avec Hibernate, pour faire en sorte que le type java.time.LocalTime soit reconnu, il faut implémenter et utiliser un UserType. Par chance, une telle implémentation existe déjà dans une bibliothèque nommée Jadira UserType Extended.

1. <dependency>

2. <groupId>org.jadira.usertype</groupId>

3. <artifactId>usertype.extended</artifactId>

4. <version>3.2.0.GA</version>

5. </dependency>

Avec cette bibliothèque, on annotera juste la date de naissance au niveau de l'entité pour indiquer les UserType :

1. @Entity

2. public class Utilisateur implements Serializable {

3. ...

4. @Type(type="org.jadira.usertype.dateandtime.threeten.PersistentLocalDate")

5. private LocalDate dateNaissance;

III - Persistance avec JDBC

Si l'on dispose d'un driver compatible JDBC 4.2 (la version de JDBC incluse à Java 8), on doit pouvoir en théorie écrire :

1. // Dans les ResultSet

2. LocalDate localDate = resultSet.getObject("date_naissance", LocalDate.class);

4. // Pour les PreparedStatement

5. preparedStatement, setObject(3, localDate, Types.DATE);

En pratique, peu de bases de données fournissent un driver compatible JDBC 4.2.

- Oracle 10.1 (i.e. 12c) : JDBC 4.1

- PostgreSQL 9.3 : JDBC 4.1

- MySQL Connector/J 5.1 : JDBC 4.0

- H2 1.4 : JDBC 4.0

- HSQL 2.3 : JDBC 4.0

- Apache Derby 10.11 : JDBC 4.2

Même la base JavaDB, incluse à Java 8 et issue de Apache Derby, qui implémente pourtant JDBC 4.2 ne semble pas s'intégrer avec la JSR 310. Il faudra donc écrire quelques méthodes utilitaires pour convertir, extraire et injecter les dates en passant par le type java.sql.Date :

1. // Pour les ResultSet

2. public static LocalDate toLocalDate(java.sql.Date sqlDate) {

3. if (sqlDate == null) {

4. return null;

5. } else {

6. return Instant.ofEpochMilli(utilDate.getTime()).atZone(ZoneId.systemDefault()).toLocalDate();

7. }

8. }

10. public static LocalDate getLocalDate(ResultSet resultSet, String columnLabel) throws SQLException {

11. return toLocalDate(resultSet.getDate(columnLabel));

12. }

14. // Pour les PreparedStatement

15. public static java.sql.Date toSqlDate(LocalDate localDate) {

16. return new java.sql.Date(localDate.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());

17. }

18. public static void setLocalDate(PreparedStatement preparedStatement, int parameterIndex, LocalDate localDate) throws SQLException {

19. if (localDate == null) {

20. preparedStatement.setNull(parameterIndex, Types.DATE);

21. } else {

22. preparedStatement.setDate(parameterIndex, toSqlDate(localDate));

23. }

24. }

IV - Persistance avec Spring JDBC

Grâce aux méthodes utilitaires précédentes, le code Spring JDBC coule de source. Les lambdas permettent même de raccourcir l'écriture des RowMapper.

1. utilisateurs = jdbcTemplate.query(

2. // SQL

3. "select * from utilisateur " +

4. "where date_naissance between ? And ?",

5. // Paramètres

6. new Object[]{toSqlDate(dateMin), toSqlDate(dateMax)},

7. // RowMapper

8. (resultSet, rowNum) -> {

9. Utilisateur utilisateur = new Utilisateur();

10. // ...

11. utilisateur.setDateNaissance(getLocalDate(resultSet, "date_naissance"));

12. return utilisateur;

13. });

En utilisant le BeanPropertyRowMapper pour peupler automatiquement les attributs, on peut automatiser la conversion. On commence par enregistrer un Converter dans le ConversionService de Spring, on ne peut pas utiliser une lambda ici, sinon Spring n'arrive pas à déterminer les types sources/cibles par réflexion. On lie ensuite le ConversionService au BeanPropertyRowMapper via le BeanWrapper :

1. @Bean

2. public ConversionService conversionService() {

3. DefaultConversionService conversionService = new DefaultConversionService();

4. conversionService.addConverter(new Converter<Date, LocalDate>() {

5. public LocalDate convert(Date date) {

6. return toLocalDate(date);

7. }

8. });

9. return conversionService;

10. }

13. RowMapper<Utilisateur> rowMapper = new BeanPropertyRowMapper(Utilisateur.class){

14. protected void initBeanWrapper(BeanWrapper bw) {

15. bw.setConversionService(conversionService);

16. }

17. };

Nous savons à présent lire/écrire des LocalDate avec Hibernate et Spring JDBC. Voyons à présent comment les utiliser lors des échanges REST/JSON et SOAP/XML.

V - Sérialisation JSON

Pour traiter le cas du JSON, avec Jackson c'est immédiat, il y a un module dédié :

1. <dependency>

2. <groupId>com.fasterxml.jackson.datatype</groupId>

3. <artifactId>jackson-datatype-jsr310</artifactId>

4. <version>${jackson.version}</version>

5. </dependency>

Il suffit de l'enregistrer dans l'ObjectMapper. On peut personnaliser le format de la sérialisation en activant/désactivant l'option WRITE_DATES_AS_TIMESTAMPS :

1. ObjectMapper objectMapper = new ObjectMapper();

2. objectMapper.registerModule(new JSR310Module());

3. objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

Pour peu qu'il détecte ce qu'il faut sur le classpath, Spring Boot s'occupe de tout (voir JacksonAutoConfiguration).

En ce qui concerne le passage de dates dans les URL HTTP, Spring MVC 4.1 supporte nativement les types JSR 310, il faut juste mettre une annotation @DateTimeFormat pour stipuler le format :

1. @RequestMapping(value="/utilisateur", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)

3. public List<Utilisateur> findAllWithDateNaissance(

4. @RequestParam("dateMin") @DateTimeFormat(iso=DateTimeFormat.ISO.DATE) LocalDate dateMin,

5. @RequestParam("dateMax") @DateTimeFormat(iso=DateTimeFormat.ISO.DATE) LocalDate dateMax) {

VI - Sérialisation XML

Passons à présent aux échanges XML avec JAXB. On écrit un XmlAdapter capable de convertir la LocalDate en XMLGregorianCalendar :

1. public class LocalDateXmlAdapter extends XmlAdapter<XMLGregorianCalendar, LocalDate> {

2. private final DatatypeFactory datatypeFactory;

3. public LocalDateXmlAdapter() throws DatatypeConfigurationException{

4. this.datatypeFactory = DatatypeFactory.newInstance();

5. }

6. public LocalDate unmarshal(XMLGregorianCalendar xmlDate) throws Exception {

7. return LocalDate.of(xmlDate.getYear(), xmlDate.getMonth(), xmlDate.getDay());

8. }

9. public XMLGregorianCalendar marshal(LocalDate localDate) throws Exception {

10. return datatypeFactory.newXMLGregorianCalendarDate(localDate.getYear(), localDate.getMonth().getValue(), localDate.getDayOfMonth(),  DatatypeConstants.FIELD_UNDEFINED);

11. }

12. }

Puis on applique cet adaptateur sur les attributs de type LocalDate avec l'annotation @XmlJavaTypeAdapter :

1. @XmlJavaTypeAdapter(LocalDateXmlAdapter.class)

2. @XmlSchemaType(name = "date")

3. protected LocalDate dateNaissance;

Afin d'automatiser la génération des classes Java depuis les XSD avec XJC, on mettra dans le fichier de binding :

1. <jaxb:globalBindings>

2. <xjc:javaType name="java.time.LocalDate" xmlType="xs:date"

3. adapter="com.zenika.test.jsr310.xml.LocalDateXmlAdapter" />

4. </jaxb:globalBindings>

VII - Validation

On souhaite à présent valider le champ dateNaissance avec Hibernate Validator :

@Past
private LocalDate dateNaissance;

Pour que cela fonctionne, il y a deux solutions. Soit on a le goût du risque et on prend la version 5.2.0.Alpha1 d'Hibernate Validator (voir HV-874 et le blog in.relation.to). Soit on se retrouve contraint d'écrire un validateur personnalisé :

1. @Target({ ElementType.FIELD })

2. @Retention(RetentionPolicy.RUNTIME)

3. @Constraint(validatedBy = PastValidator.class)

4. @Documented

5. public @interface Past {

6. String message() default "com.zenika.test.jsr310.entity.Past.message";

7. Class<?>[] groups() default {};

8. Class<? extends Payload>[] payload() default {};

9. }

11. public class PastValidator implements ConstraintValidator<Past, LocalDate> {

12. public void initialize(Past past) {}

14. public boolean isValid(LocalDate localDate, ConstraintValidatorContext context) {

15. return localDate == null || localDate.isBefore(LocalDate.now());

16. }

17. }

VIII - Conclusion

L'effort à fournir pour éradiquer les java.util.Date n'est pas négligeable, vivement que les bibliothèques classiques intègrent ces types nativement. On soulignera toutefois l'effort fourni par les équipes Spring et Jackson.

IX - Remerciements

Cet article a été publié avec l'aimable autorisation de Zenika, experts en technologies Open Source et méthodes Agiles.

Nous tenons à remercier Orhleil pour sa relecture orthographique attentive de cet article et Malick SECK pour la mise au gabarit.