| « Hibernate4gwt : 4 mois plus tard | Les limites du "miracle GWT" » |
GWT : enrichir l'émulation JRE
Avant-propos (ou le pourquoi du comment)
La sérialisation GWT répond à une règle stricte : seules quelques classes issues de la Java Runtime Environment sont supportées, et malheur à quiconque s’en écarte. De manière cohérente, certaines classes de base sont supportées. Par contre, rien n’assure que les classes dérivées le soient.
C’est justement ce comportement, au demeurant fort logique, qui est la cause de nombreuses SerializationException lors de l’envoi de date du serveur vers le client.
En effet, il est fort probable que le moteur de persistance, peu au fait des subtilités de sérialisation Javascript, aie tôt fait de remplacer votre instance de java.util.Date par une dérivation SQL (java.sql.Date ou java.sql.Timestamp) plus proche de son modèle relationnel.
Généralement, le contournement préconisé revient à créer un objet Date standard à partir de sa variation SQL afin de ne pas heurter môssieur GWT ;). Sauf que… la classe Timestamp est plus précise que la classe Date, et donc que cette conversion peut poser quelques problèmes de synchronisation au retour sur le serveur. Je pense notamment aux malheureux qui ont basé la gestion de version sur un champ Timestamp et qui se retrouvent bien embêtés avec leur valeur de retour tronquée…
Ce problème ayant été relevé par un utilisateur de ma librairie hibernate4gwt, j’ai décidé de m’attaquer au problème.
Portage du timestamp : premier essai
Dans ma grande naïveté (je suis parfois d’un optimisme béat), j’ai cru qu’en définissant une classe d’émulation classique _ comprenez implémentant (Is)Serializable _ le tour serait joué.
Hélas, trois fois hélas, la sérialisation JavaScript semble assez mal s’accommoder de l’héritage nécessaire entre Timestamp et Date. J’ai d’ailleurs ouvert une fiche de bug (n°1164) concernant ce problème… sans réponse à ce jour :'(
Portage du timestamp : seconde tentative
Etant d’un naturel têtu, j’ai parcouru les sources de GWT à la recherche du mécanisme permettant la sérialisation de l’objet java.util.Date.
En fait, un objet émulé nécessite deux classes :
- Une classe d’émulation JSNI, situé dans le dossier 'com/google/gwt/emul/<package-emulé>' ou un de vos répertoires propre en suivant la même nomenclature
- Un serialiseur dédié, portant le même nom que la classe, agrémenté de « _CustomFieldSerializer ».
L’écriture de la classe d’émulation JSNI s’est avéré relativement simple, en m’appuyant sur l’existant Date, seul le champ « nanoseconds » devant être ajouté.
Le sérialiseur quand à lui répond à un certain nombre de règles plus ou moins tacites (il n’existe pas d’interface à implémenter) :
- Il doit être situé soit au même niveau que la classe concernée, soit dans le package ‘com.google.gwt.user.client.rpc.core.<package.émulé>’
- Il doit implémenter les méthodes « serialize » et « deserialize », celles-ci prenant en charge le type associé.
- Si la classe associée n’a pas de constructeur vide, l’interface doit prendre en charge la construction de l’instance par le biais de la méthode « instantiate »
- Bien entendu, les appels à la sérialisation et à la désérialisation doivent s’effectuer dans le même ordre, sous peine de mauvaise surprise
public final class Timestamp_CustomFieldSerializer
{
public static void deserialize(SerializationStreamReader streamReader,
Timestamp instance)
throws SerializationException
{
// no fields
}
public static Timestamp instantiate(SerializationStreamReader streamReader)
throws SerializationException
{
Timestamp instance = new Timestamp(streamReader.readLong());
instance.setNanos(streamReader.readInt());
return instance;
}
public static void serialize(SerializationStreamWriter streamWriter,
Timestamp instance)
throws SerializationException
{
streamWriter.writeLong(instance.getTime());
streamWriter.writeInt(instance.getNanos());
}
}
Notez bien que les méthodes utilisent et renvoie des objets de type Timestamp et non de simples objets, ce qui explique qu’il n’y ait pas d’interface générique à implémenter.
Et ça marche !
Dernière remarque : concernant le portage en Javascript d’une classe existante, c’est cette dernière qui est utilisée en Hosted Mode, et qu’il faut déployer l’application GWT en mode Web pour voir s’exécuter le code JSNI et valider son fonctionnement.