L’exception java.io.NotSerializable se rencontre tard le soir au détour d’une petite période d’inactivité avec votre belle application J2EE… Le serveur essaye de passiver un EJB et boum, voici notre belle exception qui sort du bois. Vous pouvez aussi rencontrer notre amie alors que vous essayez d’envoyer par le réseau un objet java. Nous allons voir comment dompter la bête et aussi facilement identifier ce qui n’est pas sérialisable. Je commence par faire un rappel sur la sérialisation, et ensuite je parlerai de JBoss pour vous montrer comment configurer celui-ci pour identifier les problèmes de passivation avec les EJB.
Rappel rapide sur la sérialisation en Java: dans le domaine de la programmation, du stockage de données et de la transmission par le réseau, la sérialisation consiste à sauver un objet Java au format binaire. Il est ensuite possible de transmettre cette objet via le réseau, ou de le sauvegarder sur le disque par exemple. Les données binaires respectent un format spécial qui permet à Java de recréer un Objet identique avec le même état que l’original.En anglais, sérialiser un objet se dit « marshalling » et le désérialiser se dit « unmarshalling ».
Avant tout, certains objets en Java ne sont pas sérialisables: Socket, Thread, OutputStream ou Image par exemple. Cela peut poser un problème à votre application: que se passe-t-il si votre code contient une instance de l’une de ces classes non sérialisable ? Java vous le signale avec l’exception java.io.NotSerializable. Il existe cependant des solutions à ces problèmes.
La sérialisation par défaut
Pour persister un objet à l’exécution, nous devons le rendre sérialisable. Java fait appel à un design pattern appelé « Marker ». Ce design pattern consiste à créer une Interface java vide sans méthodes afin de marquer une class Java comme étant capable d’effectuer un certain traitement. J’aime bien demander en entretien d’embauche: « Peut-on avoir une Interface sans méthodes, sans instances ? Si oui, à quoi cela sert-il ? »
Prenons une class SoccerPlayer qui vient de mes cours sur java:
import java.util.Date; import java.io.Serializable; /** * Created by IntelliJ IDEA. * User: nicolasmartignole * Date: 22 janv. 2008 * Time: 23:07:31 */ public class SoccerPlayer implements Serializable { private String name; private Date birthDate; private int number; public SoccerPlayer(String name, Date birthDate, int number) { this.name = name; this.birthDate = birthDate; this.number = number; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Date getBirthDate() { return birthDate; } public void setBirthDate(Date birthDate) { this.birthDate = birthDate; } public int getNumber() { return number; } public void setNumber(int number) { this.number = number; } }
En marquant notre classe avec l’interface java.io.Serializable, celle-ci est prête à être sérialisée. Nous allons écrire un peu de code pour tester en l’état la sérialisation de cette class.
import java.util.Locale; import java.text.DateFormat; import java.io.FileOutputStream; import java.io.File; import java.io.ObjectOutputStream; /** * This class marshalls the SoccerPlayer class to a file. * User: nicolasmartignole * Date: 22 janv. 2008 * Time: 23:11:07 * * See online Touilleur Express http://www.touilleur-express.fr */ public class FileSystemStorageEngine { private static DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT, Locale.FRANCE); /** * Creates a new player, performs a serialiation test. * @param args is cl args. * @throws Exception for any Exception, I don't want to deal with it in my sample. */ public static void main(String[] args) throws Exception{ SoccerPlayer player = new SoccerPlayer("Nicolas Martignole", df.parse("24/09/1975"), 10); FileOutputStream fos=new FileOutputStream(new File("test.ser")) ; ObjectOutputStream out=new ObjectOutputStream(fos); out.writeObject(player); } }
La class ObjetOutputStream est la classe qui se charge de sérialiser notre class SoccerPlayer vers le fichier. Si la class SoccerPlayer n’était pas marquée avec l’interface Serializable, Java lève une « java.io.NotSerializable » exception. La class SoccerPlayer est simple, et ses variables d’instance (une String et une Date) peuvent être sérialisées par Java sans problèmes.
Désérialiser
La désérialisation s’effectue en utilisant la class ObjectInputStream. Dans l’exemple ci-dessous, je relis le fichier test.ser et je vais afficher le nom du joueur dans la console.
Nous modifions un peu notre class afin d’appeler une méthode chargée de désérialiser notre fichier. Dans mon exemple je ne fais aucuns traitements d’erreur, et donc si le fichier n’existe pas le programme lévera une FileNotFoundException. Maisi ici le plus important c’est juste de montrer qu’il est simple de recharger un Objet Java en mémoire.
public static void main(String[] args) throws Exception{ SoccerPlayer loaded=reloadSoccerPlayer(); System.out.println("Soccer player name: "+loaded.getName()); } private static SoccerPlayer reloadSoccerPlayer() throws IOException, ClassNotFoundException { FileInputStream fis=new FileInputStream(new File("test.ser")) ; ObjectInputStream in=new ObjectInputStream(fis); SoccerPlayer sp=(SoccerPlayer)in.readObject(); return sp; }
L’exécution de mon code sur Mac donne
/System/Library/Frameworks/JavaVM.framework/Versions/1.5.0/Home/bin/java -Dfile.encoding=MacRoman -classpath /System/Library/Frameworks/JavaVM.framework/Versions/1.5.0/Home/lib/deploy.jar:/System/Library/Frameworks/JavaVM.framework/Versions/1.5.0/Home/lib/dt.jar:/System/Library/Frameworks/JavaVM.framework/Versions/1.5.0/Home/lib/javaws.jar:/System/Library/Frameworks/JavaVM.framework/Versions/1.5.0/Home/lib/jce.jar:/System/Library/Frameworks/JavaVM.framework/Versions/1.5.0/Home/lib/plugin.jar:/System/Library/Frameworks/JavaVM.framework/Versions/1.5.0/Home/lib/sa-jdi.jar:/System/Library/Frameworks/JavaVM.framework/Versions/1.5.0/Home/../Classes/charsets.jar:/System/Library/Frameworks/JavaVM.framework/Versions/1.5.0/Home/../Classes/classes.jar:/System/Library/Frameworks/JavaVM.framework/Versions/1.5.0/Home/../Classes/dt.jar:/System/Library/Frameworks/JavaVM.framework/Versions/1.5.0/Home/../Classes/jce.jar:/System/Library/Frameworks/JavaVM.framework/Versions/1.5.0/Home/../Classes/jconsole.jar:/System/Library/Frameworks/JavaVM.framework/Versions/1.5.0/Home/../Classes/jsse.jar:/System/Library/Frameworks/JavaVM.framework/Versions/1.5.0/Home/../Classes/laf.jar:/System/Library/Frameworks/JavaVM.framework/Versions/1.5.0/Home/../Classes/ui.jar:/System/Library/Frameworks/JavaVM.framework/Versions/1.5.0/Home/lib/ext/apple_provider.jar:/System/Library/Frameworks/JavaVM.framework/Versions/1.5.0/Home/lib/ext/dnsns.jar:/System/Library/Frameworks/JavaVM.framework/Versions/1.5.0/Home/lib/ext/localedata.jar:/System/Library/Frameworks/JavaVM.framework/Versions/1.5.0/Home/lib/ext/sunjce_provider.jar:/System/Library/Frameworks/JavaVM.framework/Versions/1.5.0/Home/lib/ext/sunpkcs11.jar:/Users/nicolasmartignole/IdeaProjects/Test2/out/production/Test2 FileSystemStorageEngine Soccer player name: Nicolas Martignole Process finished with exit code 0
Lorsque la class n’est pas sérialisable
Nous allons parler de l’exception java.io.NotSerializable. Mais avant tout, je dois continuer à rappeler quelques mécanismes de Java.
Reprenons notre class SoccerPlayer et modifions-là afin qu’elle ne soit pas sérialisable. Je vais créer un objet « PlayerPosition » qui représente sur le terrain le poste du joueur. Cet objet ne sera pas sérialisable car il est dynamique. Un joueur défenseur peut jouer aillier (enfin je crois) et donc nous imaginons qu’il n’est pas possible de sérialiser cette information.
Peu importe le code de la class SoccerPosition, nous avons simplement besoin d’avoir une instance de celle-ci dans la class SoccerPlayer. La class SoccerPosition n’implémente pas l’interface java.io.Serializable.
La class PlayerPosition est très simple:
public class PlayerPosition { String positionName; public PlayerPosition(String s) { positionName =s; } public String getPositionName() { return positionName; } public void setPositionName(String positionName) { this.positionName = positionName; } }
Nous ajoutons une instance dans SoccerPlayer:
public class SoccerPlayer implements Serializable { private String name; private Date birthDate; private int number; private PlayerPosition position; public SoccerPlayer(String name, Date birthDate, int number, PlayerPosition p) { this.name = name; this.birthDate = birthDate; this.number = number; this.position=p; }
Je modifie le constructeur de SoccerPlayer afin de passer une instance de PlayerPosition. Voyons maintenant lorsque l’on relance le programme pour sérialiser ce qui se passe:
public class FileSystemStorageEngine { private static DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT, Locale.FRANCE); /** * Creates a new player, performs a serialiation test. * @param args is cl args. * @throws Exception for any Exception, I don't want to deal with it in my sample. */ public static void main(String[] args) throws Exception{ SoccerPlayer player = new SoccerPlayer("Nicolas Martignole", df.parse("24/09/1975"), 10, new PlayerPosition("Goal") ); performSerialization(player); SoccerPlayer loaded=reloadSoccerPlayer(); System.out.println("Soccer player name: "+loaded.getName()); } private static void performSerialization(SoccerPlayer player) throws ParseException, IOException { FileOutputStream fos=new FileOutputStream(new File("test.ser")) ; ObjectOutputStream out=new ObjectOutputStream(fos); out.writeObject(player); } private static SoccerPlayer reloadSoccerPlayer() throws IOException, ClassNotFoundException { FileInputStream fis=new FileInputStream(new File("test.ser")) ; ObjectInputStream in=new ObjectInputStream(fis); SoccerPlayer sp=(SoccerPlayer)in.readObject(); return sp; } }
Avant tout, notez que si vous n’instanciez pas de PlayerPosition pour la class SoccerPlayer, tout va bien se passer. C’est pour cette raison que j’ai mis à jour le constructeur de SoccerPlayer pour forcer l’affectation d’une PlayerPosition.
Après avoir compilé, le programme plante:
Exception in thread "main" java.io.NotSerializableException: PlayerPosition at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1081) at java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1375) at java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1347) at java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1290) at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1079) at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:302) at FileSystemStorageEngine.performSerialization(FileSystemStorageEngine.java:34) at FileSystemStorageEngine.main(FileSystemStorageEngine.java:24)
Ici Java identifie facilement que la class PlayerPosition ne peut pas être sérialisée. Là où souvent vous aller avoir des soucis, c’est avec un serveur d’application comme JBoss ou Weblogic. Le mécanisme de passivation permet à un serveur d’application de libérer de la mémoire en stockant sur le disque des objets sérialisés. Lorsque la passivation échoue, les objets de la session EJB de l’utilisateur ne pourront pas être restaurés. D’où cette exception.
Avec un serveur J2EE et des EJB 2.1, comment retrouver quelle classe n’est pas sérialisable ?
Voici l’astuce du jour (et l’objet de ce billet):
- Lancer le serveur d’application en mode debug sur une socket
- Se connecter via Eclipse ou IntelliJ sur le serveur J2EE en démarrant une session de remote debug avec Java 5.
- Charger les écrans dans l’application Web en se connectant
- Configurer Eclipse ou IDEA IntelliJ pour que le debugeur s’active lorsque l’exception java.io.NotSerializable est levée (voir plus bas)
- Attendre que l’EJB se passive
- Votre IDE doit alors se réveiller et vous êtes arretés à l’endroit où la Thread du serveur d’application est entrain de faire la sérialisation. Avec l’inspecteur d’objet d’IntelliJ il est facile d’identifier quelle classe n’est pas serializable
Sur JBoss la passivation d’un Session Statefull Bean est reglé à 10 minutes, je ne vais pas attendre aussi longtemps
En effet, il est facile de configurer JBoss afin de passiver tout de suite des Beans. Je vais donner l’explication pour JBoss 4.2.2.
Ouvrez le fichier $JBOSS_HOME/server/default/conf/standardjboss.xml avec votre éditeur de texte favori (vi). Dans ce fichier de configuration, la section //container-configuration// définit la configuration du containeur. Chaque configuration spécifie des éléments tels que l’invoquer à utiliser, la politique d’interception de jboss, le nombre d’instances de caches et les files d’attente de thread, la configuration du persistence manager et enfin la partie sécurité.
Nous allons regarder plus en détail la section « container-cache-conf ». Cet élément est passé à l’implémentation de l’InstanceCache si celle-ci supporte l’interface XmlLoadable. Vous pouvez donc implémenter votre propre mécanisme de cache si vous le désirez. Les implémentations actuelles de Joss InstanceCache dérivent de la classe abstraite « org.jboss.ejb.plugins.AbstractInstanceCache ». Cette classe implémente l’interface XmlLoadable et donc sera capable de charger notre configuration.
Examinons le fichier standardjboss.xml
org.jboss.ejb.plugins.LRUEnterpriseContextCachePolicy 50 1000000 300 600 400 60 1 0.75
Deux implémentations possible de CachePolicy sont utilisées dans le fichier standardjboss.xml avec le support d’une configuration que nous allons voir dans un instant
Les deux implementations sont
- org.jboss.ejb.plugins.LRUEnterpriseContextCachePolicy
- org.jboss.ejb.plugins.LRUStatefulContextCachePolicy
Dans JBoss 4.2, les Entity beans utilisent LRUEnterpriseContextCachePolicy. Les stateful session bean utilisent LRUStatefulContextCachePolicy.
Voici ce qu’il est possible de configurer:
- min-capacity nombre d’éléments minimum du cache
- max-capacity nombre maximum d’éléments du cache
- overager-period : durée en secondes entre 2 exécutions du système de nettoyage du cache. Lorsque ce système s’active, les beans dont l’âge est supérieur à max-bean-age sont retirés du cache pour être passivé. Vous voyez donc où nous allons en venir.
- max-bean-age : donne le temps maximum en seconde Durant lequel un bean peut être inactive avant d’être passive.
- resizer-period: durée en secondes entre 2 exécutions de la thread du “resizer”. Cette tâche se charge de redimensionner le cache en se basant sur les 3 proprietés suivantes. Lorsque cette tâche s’exécute, elle vérifie le temps écoulé entre 2 caches miss et si cette période est inférieure à « min-cache-miss-period » alors le cache est agrandit à la valeur « max-capacity » en utilisant le facteur « cache-load-factor ». Si la période par contre entre 2 miss caches est plus grande que la valeur de “max-cache-miss-period » alors la taille du cache est réduite en utilisant la valeur de « cache-load-factor ». Ce système permet d’avoir un cache de beans adapté à la charge du serveur avec JBoss.
En plus des paramètres précedents, LRUStatefulContextCachePolicy pour les Session Stateful Bean a les éléments suivants
- remover-period Spécifie une période en secondes à attendre entre l’exécution de la tâche de nettoyage du cache. La tâche « remover task » se charge de passiver les EJB qui n’ont pas eu d’accès depuis « max-bean-life » secondes. Ce système différent permet de vider les EJB qui ont été passivés afin d’éviter de remplir par exemple le disque. Ce n’est pas le mécanisme de la passivation. C’est un mécanisme de nettoyage des beans sérialisés sur le disque et qui n’ont pas été réactivé depuis « max-bean-life ».
- max-bean-life Spécifie la période maximale en seconde d’inactivité pour un bean avant que celui-ci ne soit détruit de l’espace de passivation.
Comment voir si un EJB peut être passivé et restauré facilement avec JBoss ?
Nous arrivons enfin à l’explication, après avoir parlé sérialisation et JBoss. Editez la section container-cache-conf pour le container « Standard Stateful SessionBean ».
Vous pouvez limiter le nombre de beans en cache, et donc forcer la passivation pour les beans les plus anciens :
Ici je déclare que les beans doivent être passivés après 30 secondes (max-bean-age), et qu’ils ont une durée de vie de 1800 secondes(max-bean-life). Cela me laisse du temps pour tester l’activation après passivation. Toutes les 25 secondes je vérifie mon cache afin de voir ce que je dois passiver (overage-period). Toutes les 120 secondes (remover-period) je regarde si je ne peux pas effacer du disque de vieux beans passivés. Je ne touche pas à ce qui est redimensionnement du cache (resizer-period).
org.jboss.ejb.plugins.LRUStatefulContextCachePolicy 10 30 25 30 400 60 1 0.75 120 1800
Comment désactiver la passivation des EJB avec JBoss ?
C’est possible, quoique par forcément souhaitable. JBoss fournit une implémentation de CachePolicy appelée org.jboss.ejb.plugins.NoPassivationCachePolicy.
Cette police ne passivera jamais une instance. Elle est basée sur une Map en mémoire qui ne retire jamais les beans sauf si ceux-ci sont explicitement retirés
Pour désactiver la passivation :
Standard Stateful SessionBean org.jboss.ejb.plugins.LRUStatefulContextCachePolicy
J’espère que ce petit guide vous sera utile, n’hésitez pas à me contacter si vous chercher un expert JBoss pour faire du tuning ou si vous avez besoin de conseils pour votre application.
0 no like
oh … j’ai mal a la tete
Ne pas oublier quand on manipule des données sérializables d’ajouter l’attribut « private static final long serialVersionUID » qui permet de versionner la classe (Cet attribut est sinon généré avec une valeur aléatoire par la JVM au moment du chargement de la classe).
Ainsi la JVM qui desérialise l’objet vérifiera que la version de la classe qu’elle connait correspond à la version de la classe qu’elle essaie de desérialiser.
Très bon article.
En complément, je conseille à tous d’utiliser conjointement la classe NonSerializableAttributeTester qui permet de vérifier la sérialisation de tout object (collections comprises ) passé en attribut de session (HttpSession.setAttribute()).
Pour plus d’infos :
http://wiki.jboss.org/wiki/Wiki.jsp?page=DebuggingSerializationErrors