Imaginez le Diagramme UML suivant :
Qui pourrait parfaitement représenter les Objets d’un Blog, mais pas que.
Imaginez que vous souhaitiez réaliser un API, compatible REST, qui vous permette de récupérer vos Posts et Comments au format JSon.
Nos Objets Métiers
Commençons tout d’abord par écrire nos objets Model :
app/models/Post.java
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
package models; import java.util.List; import javax.persistence.CascadeType; import javax.persistence.Entity; import javax.persistence.OneToMany; import play.data.validation.Required; import play.db.jpa.Model; @Entity public class Post extends Model { @Required public String author; @Required public String title; @OneToMany(mappedBy = "post", cascade = CascadeType.ALL) public List<Comment> comments; } |
app/models/Comment.java
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
package models; import javax.persistence.Entity; import javax.persistence.Lob; import javax.persistence.ManyToOne; import play.data.validation.MaxSize; import play.data.validation.Required; import play.db.jpa.Model; @Entity public class Comment extends Model { @Required public String author; @Lob @Required @MaxSize(10000) public String content; @ManyToOne @Required public Post post; } |
Données de référence & Configuration
Pour nous simplifier la vie, Play! prévoit un mécanisme très pratique d’import de données, basé sur l’utilisation d’un fichier au format YAML
conf/initial-data.yml
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
Post(post1): author: moi title: Post 1 Post(post2): author: moi title: Post 2 Comment(c1): author: moi content: du Contenu post: post1 Comment(c2): author: moi content: du Contenu post: post1 Comment(c3): author: moi content: du Contenu post: post2 |
conf/application.conf
|
1 2 3 |
... db=mem ... |
Bootstrap Job & Controller
Le Bootstrap Job va nous permettre d’importer nos données de référence sans effort, et notre contrôleur devra réaliser notre besoin initial, à savoir nous retourner la liste des Posts
app/controllers/Bootstrap.java
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
package controllers; import models.Post; import play.jobs.Job; import play.jobs.OnApplicationStart; import play.test.Fixtures; @OnApplicationStart public class Bootstrap extends Job { public void doJob() { if (Post.count() == 0) { Fixtures.loadModels("initial-data.yml"); } } } |
app/controllers/Application.java
|
1 2 3 4 5 6 7 8 9 10 11 12 |
package controllers; import java.util.List; import models.Post; import play.mvc.Controller; public class Application extends Controller { public static void index() { List<Post> posts = Post.findAll(); renderJSON(posts); } } |
Premier résultat
Après avoir accédé via votre navigateur à l’adresse http://localhost:9000, vous devriez avoir l’erreur suivante :

Et dans votre console :
|
1 2 3 4 5 |
@67jf5njpa Internal Server Error (500) for request GET / Execution exception (In /app/controllers/Application.java around line 12) IllegalStateException occured : circular reference error Offending field: post Offending object: preserveType: false, type: class models.Post, obj: Post[1] |
Corrections
La méthode renderJSON prévoit l’utilisation de vos propre JSonSerializer. Ce que nous allons faire pour nos Objets Post et Comment.
app/models/serializer/PostJSonSerializer.java
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
package models.serializer; import java.lang.reflect.Type; import models.Post; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializer; public class PostJSonSerializer implements JsonSerializer<Post> { public static PostJSonSerializer instance; private PostJSonSerializer() { } public static PostJSonSerializer get() { if (instance == null) { instance = new PostJSonSerializer(); } return instance; } public JsonElement serialize(Post post, Type type, JsonSerializationContext jsonSerializationContext) { JsonObject obj = new JsonObject(); obj.addProperty("author", post.author); obj.addProperty("title", post.title); obj.add("comments", jsonSerializationContext.serialize(post.comments)); return obj; } } |
app/models/serializer/CommentJSonSerializer.java
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
package models.serializer; import java.lang.reflect.Type; import models.Comment; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializer; public class CommentJSonSerializer implements JsonSerializer<Comment> { public static CommentJSonSerializer instance; private CommentJSonSerializer() { } public static CommentJSonSerializer get() { if (instance == null) { instance = new CommentJSonSerializer(); } return instance; } public JsonElement serialize(Comment comment, Type type, JsonSerializationContext jsonSerializationContext) { JsonObject obj = new JsonObject(); obj.addProperty("author", comment.author); obj.addProperty("content", comment.content); return obj; } } |
On modifie par la suite l’appel à la méthode renderJSon dans le fichier app/controllers/Application.java:
|
1 2 3 4 5 6 |
... public static void index() { List<Post> posts = Post.findAll(); renderJSON(posts, PostJSonSerializer.get(), CommentJSonSerializer.get()); } ... |
Résultat final
Rafraîchissez l’adresse http://localhost:9000 pour obtenir le résultat suivant :
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
[ { "author":"moi", "title":"Post 1", "comments":[ { "author":"moi", "content":"du Contenu" }, { "author":"moi", "content":"du Contenu" } ] }, { "author":"moi", "title":"Post 2", "comments":[ { "author":"moi", "content":"du Contenu" } ] } ] |
ps: Je vous livre ici mes observations sur le fonctionnement des JSonSerializer. Si vous voyez une façon de mieux les écrire, ou de mieux faire n’hésitez pas à commenter ce billet.
Ta méthode fonctionne bien. Par contre elle est un peu lourde à mettre en oeuvre. Si le problème ne se pose que sur une seule classe comme ici, alors ça va. Si ça doit être appliqué à plusieurs autres classes, il faudrait utiliser une méthode un peu plus transparente (et industrialisée).
Play est un peu léger sur ce point mais ça peut facilement être complété de façon élégante. Je vois 2 solutions possibles.
* Solution 1 : Compléter Play en continuant à utiliser Gson.
- Créer une nouvelle annotation runtime (par exemple NotSerialized)
- Créer une
ExclusionStrategyGson qui se base sur la présence de cette annotation pour exclure de la sérialisation (c’est en fait l’inverse de la stratégie par défaut utilisée par Gson avec son annotation@Exposeau travers de sa méthodeGsonBuilder.excludeFieldsWithoutExposeAnnotation()).- Créer une nouvelle implémentation de
play.mvc.result.Resultpour remplacerplay.mvc.result.JsonResulten s’inspirant de son code ou en l’étendant (par exemple MyJsonResult).
- Créer un nouveau contrôleur abstrait qui fait un extend de celui de base et redéfinit les fonctions renderJson afin que celles-ci utilisent
MyJsonResultau lieu deJsonResult(par exemple MyController).Il suffit ensuite d’utiliser systématiquement ce nouveau contrôleur et d’annoter les attributs qui posent problème avec
@NoSerialize. Ce qui donne:package models;
import javax.persistence.Entity;
import javax.persistence.Lob;
import javax.persistence.ManyToOne;
import play.data.validation.MaxSize;
import play.data.validation.Required;
import play.db.jpa.Model;
@Entity
public class Comment extends Model {
@Required
public String author;
@Lob
@Required
@MaxSize(10000)
public String content;
@ManyToOne
@Required
@NoSerialize
public Post post;
}
et:
package controllers;
import java.util.List;
import models.Post;
import play.mvc.Controller;
public class Application extends MyController {
public static void index() {
List posts = Post.findAll();
renderJSON(posts);
}
}
Ainsi, plus de code à écrire pour obtenir le même résultat que dans ton exemple.
* Solution 2 : Compléter Play en remplaçant Gson par FlexJSON
Là je ne vais pas tout expliquer mais, globalement le principe peut être le même en surface en utilisant des annotations. L’intérêt majeur de FlexJSON par rapport à Gson est qu’il permet de prendre en compte le fait que pour une entité donnée il peut exister plusieurs vues différentes suivant le contexte d’usage. Pour ce faire, il se base sur un contexte se sérialisation qui lui dit quoi inclure ou exclure dans un contexte donné (un
JSONSerializer).Par défaut FlexJSON propose des annotations un peu limitées (
@JSON) qui permettent d’inclure ou d’exclure des attributs. Ce qui revient grosso modo à l’annotation@NoSerializeque je proposais pour Gson.Par contre, il est très facile de créer des annotations plus complètes qui permettraient de prendre en compte un contexte en construisant automatiquement des
JSONSerializerà partir des infos portée par les annotations.Par exemple, en générique on aurait:
/** Notion de contexte de sérialisation **/
public interface SerializationContext {}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface NoSerialize {
Class[] context default "";
}
Alors en reprenant le même principe que précédemment, a l’usage ça pourrait donner:
/** Contexte de sérialisation spécifique à la vue "liste des commentaires **/
public interface ListeCommentaires extends Serialization Context {}
@Entity
public class Comment extends Model {
@Required
public String author;
@Lob
@Required
@MaxSize(10000)
@NoSerialize( context={ ListeCommentaires.class } ) // toujours sérialisé, sauf pour ListeCommentaires
public String content;
@ManyToOne
@Required
@NoSerialize // jamais sérialisé
public Post post;
}
Il suffirait alors d’ajouter une variante de
renderJsonadmettant un argument de type Class pour pouvoir sérialiser pour un contexte particulier. Exemple:public class Application extends Controller {
public static void index() {
List posts = Post.findAll();
renderJSON(posts, ListeCommentaires.class );
}
}
Alors qu’un appel à
renderJsonsans cet argument supplémentaire ferait un sérialisation pour le contexte par défaut.Ça permettrait donc de prendre en compte le fait que suivant le contexte d’utilisation un objet n’est pas toujours sérialisé de la même façon, problème que l’on rencontre obligatoirement à un moment donné si on ne veut pas transférer de données inutiles ou non souhaitables (droits d’usage/sécurité par exemple).
Merci Laurent pour ce commentaire.
Je vais essayer tout ça très rapidement. Ca a l’air sympa!
Merci de partager tout ça ici !
bonjour,
Un truc sympa si l’on veut enrichir son objet plutôt que le tronquer en supprimant des données.
Il suffit de remplacer dans la fonction serialize(..)
JsonObject obj = new JsonObject();
par
JsonObject obj = new Gson().toJsonTree(obj).getAsJsonObject(); // on récupère l’objet déja parsé
Par exemple, si ‘lon veut ajouter le type d’une classe complexe.
public JsonElement serialize(Objet obj, Type type, JsonSerializationContext jsonSerializationContext) {
JsonObject obj = new Gson().toJsonTree(obj).getAsJsonObject();
obj.addProperty(“type”, obj.getClass().getName());
return obj;
}
Perso, le problème s’est posé avec la manipulation d’objets complexes ayant un meme parent. Le parsage Json étant effectué sur le type du parent,
il était un peu dommage de rajouter l’information de type de classe dans l’objet a parser. Cela aurait fait redondance.
thank you… you save my day…