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.
2.
3.
4.
5.
<dependency>
<groupId>
org.jadira.usertype</groupId>
<artifactId>
usertype.extended</artifactId>
<version>
3.2.0.GA</version>
</dependency>
Avec cette bibliothèque, on annotera juste la date de naissance au niveau de l'entité pour indiquer les UserType :
2.
3.
4.
5.
@Entity
public
class
Utilisateur implements
Serializable {
...
@Type
(
type=
"org.jadira.usertype.dateandtime.threeten.PersistentLocalDate"
)
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 :
2.
3.
4.
5.
// Dans les ResultSet
LocalDate localDate =
resultSet.getObject
(
"date_naissance"
, LocalDate.class
);
// Pour les PreparedStatement
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
// Pour les ResultSet
public
static
LocalDate toLocalDate
(
java.sql.Date sqlDate) {
if
(
sqlDate ==
null
) {
return
null
;
}
else
{
return
Instant.ofEpochMilli
(
utilDate.getTime
(
)).atZone
(
ZoneId.systemDefault
(
)).toLocalDate
(
);
}
}
public
static
LocalDate getLocalDate
(
ResultSet resultSet, String columnLabel) throws
SQLException {
return
toLocalDate
(
resultSet.getDate
(
columnLabel));
}
// Pour les PreparedStatement
public
static
java.sql.Date toSqlDate
(
LocalDate localDate) {
return
new
java.sql.Date
(
localDate.atStartOfDay
(
).atZone
(
ZoneId.systemDefault
(
)).toInstant
(
).toEpochMilli
(
));
}
public
static
void
setLocalDate
(
PreparedStatement preparedStatement, int
parameterIndex, LocalDate localDate) throws
SQLException {
if
(
localDate ==
null
) {
preparedStatement.setNull
(
parameterIndex, Types.DATE);
}
else
{
preparedStatement.setDate
(
parameterIndex, toSqlDate
(
localDate));
}
}
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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
utilisateurs =
jdbcTemplate.query
(
// SQL
"select * from utilisateur "
+
"where date_naissance between ? And ?"
,
// Paramètres
new
Object[]{
toSqlDate
(
dateMin), toSqlDate
(
dateMax)}
,
// RowMapper
(
resultSet, rowNum) ->
{
Utilisateur utilisateur =
new
Utilisateur
(
);
// ...
utilisateur.setDateNaissance
(
getLocalDate
(
resultSet, "date_naissance"
));
return
utilisateur;
}
);
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
@Bean
public
ConversionService conversionService
(
) {
DefaultConversionService conversionService =
new
DefaultConversionService
(
);
conversionService.addConverter
(
new
Converter<
Date, LocalDate>(
) {
public
LocalDate convert
(
Date date) {
return
toLocalDate
(
date);
}
}
);
return
conversionService;
}
RowMapper<
Utilisateur>
rowMapper =
new
BeanPropertyRowMapper
(
Utilisateur.class
){
protected
void
initBeanWrapper
(
BeanWrapper bw) {
bw.setConversionService
(
conversionService);
}
}
;
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é :
2.
3.
4.
5.
<dependency>
<groupId>
com.fasterxml.jackson.datatype</groupId>
<artifactId>
jackson-datatype-jsr310</artifactId>
<version>
${jackson.version}</version>
</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 :
2.
3.
ObjectMapper objectMapper =
new
ObjectMapper
(
);
objectMapper.registerModule
(
new
JSR310Module
(
));
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 :
2.
3.
4.
5.
@RequestMapping
(
value=
"/utilisateur"
, method =
RequestMethod.GET, produces =
MediaType.APPLICATION_JSON_VALUE)
public
List<
Utilisateur>
findAllWithDateNaissance
(
@RequestParam
(
"dateMin"
) @DateTimeFormat
(
iso=
DateTimeFormat.ISO.DATE) LocalDate dateMin,
@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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
public
class
LocalDateXmlAdapter extends
XmlAdapter<
XMLGregorianCalendar, LocalDate>
{
private
final
DatatypeFactory datatypeFactory;
public
LocalDateXmlAdapter
(
) throws
DatatypeConfigurationException{
this
.datatypeFactory =
DatatypeFactory.newInstance
(
);
}
public
LocalDate unmarshal
(
XMLGregorianCalendar xmlDate) throws
Exception {
return
LocalDate.of
(
xmlDate.getYear
(
), xmlDate.getMonth
(
), xmlDate.getDay
(
));
}
public
XMLGregorianCalendar marshal
(
LocalDate localDate) throws
Exception {
return
datatypeFactory.newXMLGregorianCalendarDate
(
localDate.getYear
(
), localDate.getMonth
(
).getValue
(
), localDate.getDayOfMonth
(
), DatatypeConstants.FIELD_UNDEFINED);
}
}
Puis on applique cet adaptateur sur les attributs de type LocalDate avec l'annotation @XmlJavaTypeAdapter :
2.
3.
@XmlJavaTypeAdapter
(
LocalDateXmlAdapter.class
)
@XmlSchemaType
(
name =
"date"
)
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 :
2.
3.
4.
<
jaxb
:
globalBindings>
<
xjc
:
javaType
name
=
"java.time.LocalDate"
xmlType
=
"xs:date"
adapter
=
"com.zenika.test.jsr310.xml.LocalDateXmlAdapter"
/>
</
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é :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
@Target
({
ElementType.FIELD }
)
@Retention
(
RetentionPolicy.RUNTIME)
@Constraint
(
validatedBy =
PastValidator.class
)
@Documented
public
@interface
Past {
String message
(
) default
"com.zenika.test.jsr310.entity.Past.message"
;
Class<
?>
[] groups
(
) default
{}
;
Class<
? extends
Payload>
[] payload
(
) default
{}
;
}
public
class
PastValidator implements
ConstraintValidator<
Past, LocalDate>
{
public
void
initialize
(
Past past) {}
public
boolean
isValid
(
LocalDate localDate, ConstraintValidatorContext context) {
return
localDate ==
null
||
localDate.isBefore
(
LocalDate.now
(
));
}
}
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.