Des backing et managed beans bien écrits et optimisés

Les articles de cette série de trois seront les suivants :

  • Le premier porte sur des bonnes pratiques purement JSF (bonne utilisation des bindings JSF, bonne application des scopes)
  • Le deuxième sur la manière de bien stocker des états d’objets JSF au-delà du scope Request,
  • Le troisième sur les erreurs à ne pas commettre sur l’utilisation des objets ADF au sein d’un backing ou managed bean (par exemple, ne jamais mettre en cache une instance d’ApplicationModule)

Le binding JSF est le fait de lier un composant à un managed bean, ce qui produira les méthodes get et set pour ce composant. Lorsque le bean est géré (managed bean) par JSF, l’affectation est gérée automatiquement.
Dans beaucoup d’applications JSF, les erreurs d’utilisation du binding JSF suivantes sont constatées :

  • Oubli de déclarer les backing ou managed beans en classes sérialisables (du moins ceux destinés à être placés dans un scope plus large que Request).
  • Des managed beans contiennent des variables membres pointant directement sur les composants JSF eux-mêmes. Or, ces objets complexes ne sont pas sérialisables. Depuis quelques temps, l’implémentation JSF Trinidad propose la classe ComponentReference qui permet de ne stocker que la référence (sérialisable) plutôt que l’objet lui-même (le binding est regénéré depuis cette référence). A noter, que cette optimisation est valable quel que soit le scope (y compris Request).

Cet article traitera spécifiquement de cet aspect. Un article suivant, relié à l’article présent, traitera de la manière de conserver les états de vos objets JSF lorsque vous avez besoin de les conserver dans un scope plus grand que Request (rappelons qu’un composant d’interface JSF conserve son état seulement d’une request à l’autre et ça n’est pas parce que vous avez une variable membre en scope Session que l’état restera celui d’origine). A préciser qu’Oracle propose un moyen automatique et très puissant (couche MDS – MetaDatas Services, non comprise dans ADF Essentials) de gérer cela mais les principes que je donnerai sont applicables si vous ne voulez pas utiliser MDS, et aussi dans d’autres situations.
En effet, un autre conseil connexe est qu’il faut toujours définir le scope le plus restrictif possible pour vos backing ou managed beans. Lorsque vous définissez des beans dans un scope plus grand que Request, à savoir les scopes viewScope (ADF), pageFlowScope (ADF), Session (JEE) et Application (JEE), ceux-ci doivent être le plus léger possible (et donc sérialisables automatiquement). Bien sûr, vous pouvez faire communiquer un managed bean de scope Request avec un autre managed bean de scope plus large (l’inverse n’est pas vrai). Trop souvent, on constate des managed beans pléthoriques avec un scope beaucoup trop large, et on se sait pas trop pour quelle raison. Un managed bean n’est pas un fourre-tout qu’on met au scope le plus large par facilité ! Vous pouvez éclater un managed bean en plusieurs parties fonctionnelles en augmentant le scope d’une des parties que lorsque strictement nécessaire.
Rappelons également au passage qu’on ne doit trouver aucun code métier dans les backing ou managed beans mais seulement du code lié à la gestion de l’interface utilisateur.

Article 1 : ComponentReference

Une question que j’avais posée au concepteur de JHeadstart (Stevan Davelaar) de l’équipe A-Team d’Oracle sur le forum ADF a déclenché un échange entre plusieurs experts et ceci a abouti à une conclusion que voici.

Pourquoi les classes de backing et managed beans doivent être sérialisables ?

Tout d’abord quelques rappels sur la sérialisation. Mais le plus simple est de vous rendre au lien suivant pour les explications de notre « doudoux » national : http://www.jmdoudoux.fr/java/dej/chap-serialisation.htm.
Voici quelques précisions de ma part :

  • Les types simples sont sérialisables de fait.
  • Le fait d’ajouter l’interface java.io.Serializable à votre classe ne suffit pas à la rendre sérialisable. C’est juste une interface marqueur qui dit que l’état de la classe est susceptible de pouvoir être sauvegardé. Ainsi, le serveur d’application pourra tenter de sauvegarder l’état de l’objet pour le transférer à d’autres noeuds du cluster.
  • Si un objet membre n’est pas réellement sérialisable et que le serveur d’applications tente de le faire, une exception NotSerializableException sera lancée.
  • D’une manière automatique, ce sont les variables membres d’un objet ont besoin d’être sérialisées (celles conservant l’état de l’objet).

Tous les backing ou managed beans placés dans un scope supérieur à Request devraient être sérialisables (Je précise sur un scope supérieur à Request car dans ce cas il n’y a pas d’état spécifique à l’utilisateur ou à l’application mais seulement à la requête en cours).
Pour faire court, ceci permet au serveur d’applications, en cas de forte montée en charge ou de panne, de répliquer les beans sur d’autres nœuds d’un cluster. Mais même sur un serveur mono-instance, cela a un intérêt ; en effet, en se forçant à sérialiser, vous vous assurez d’avoir des objets légers et donc une mémoire bien gérée (c’est l’objet du deuxième article qui vous indiquera comment conserver des états d’objets JSF plutôt que l’objet composant JSF tout entier).

Erreur : variables membres pointant directement sur les composants JSF eux-mêmes

Les composants JSF et les ADF Faces Rich Component sont des objets complexes qui seraient trop lourds à sérialiser. C’est pourquoi ils ne sont pas sérialisables.
Or, on constate souvent que des managed beans contiennent des variables membres pointant directement sur les composants JSF eux-mêmes. Qui n’a pas fait cela :
Sur un composant af:table, nous demandons un binding JSF (propriété binding du composant)  :

<af:table var="row" rowBandingInterval="0" id="t1"
binding="#{UnManagedBean.maTable}">
<af:column sortable="false" headerText="col1" id="c3">
   <af:outputText value="#{row.col1}" id="ot5"/>
</af:column>
</af:table>

Déclaration du managed-bean dans le faces-config.xml (ou le fichier de configuration du TaskFlow unbounded ou bounded) :

<managed-bean>
  <managed-bean-name>UnManagedBean</managed-bean-name>
  <managed-bean-class>test.view.managed.UnManagedBean</managed-bean-class>
  <managed-bean-scope>session</managed-bean-scope>
</managed-bean>

Voici le code généré par l’IDE :

import java.io.Serializable;
import oracle.adf.view.rich.component.rich.data.RichTable;
public class UnManagedBean implements Serializable {
    private RichTable maTable;
    public void setMaTable(RichTable maTable) {
        this.maTable = maTable;
    }
    public RichTable getMaTable() {
        return maTable;
    }
}

Première conséquence : si ce managed-bean est placé en scope Session, l’application ne fonctionnera pas en mode cluster car à la première tentative de réplication du bean Session, une exception sera déclenchée sur les variables membres non sérialisables.
On pourrait se dire que si c’est important pour les beans placés en scope supérieur à Request, ça ne l’est pas sur les beans placés en scope inférieur ou égal à Request ?
Eh bien pas du tout, c’est également très important pour le scope Request ! Explications dans la section suivante …

Solution : remplacer la variable membre du composant par une référence à ce composant

La solution est de remplacer la variable membre du composant par une référence à ce composant grâce à la construction suivante :

import java.io.Serializable;
import oracle.adf.view.rich.component.rich.data.RichTable;
import org.apache.myfaces.trinidad.util.ComponentReference;
public class UnManagedBean implements Serializable {
  private ComponentReference maTable;
  public void setMaTable(RichTable maTable) {
    this.maTable = ComponentReference.newUIComponentReference(maTable);
  }
  public RichTable getMaTable() {
    if (this.maTable != null) {
      return (RichTable)this.maTable.getComponent();
    }
    return null;
  }
}

Vous pouvez constater que si l’implémentation interne est changée, rien n’est modifié du point de vue de l’extérieur. Donc appliquer cette règle est neutre par rapport à votre existant.
Vous pouvez constater que si l’implémentation interne est changée, rien n’est modifié du point de vue de l’extérieur. Donc appliquer cette règle est neutre par rapport à votre existant.
ComponentReference stocke un path pointant soit sur les positions d’index, soit sur les strings d’ID de composants, mais pas sur les instances de UIComponent. Trinidad (couche Open source d’ADF Faces) fait en sorte que ComponentReference trouve toujours l’instance correcte du composant concernant chaque vue.
Nous disions dans la section précédente, que si cette règle est importante pour les beans de scope Session, elle l’était également pour le scope Request. Pourquoi ?
En effet, au sein d’une même Request, on peut naviguer d’une vue JSF à une autre. Or, si les deux vues utilisent un binding de composant portant le même nom, alors potentiellement un composant UI pourrait être déplacé d’un arbre d’UI à un autre ce qui n’est pas permis, et pourrait résulter en un comportement imprévisible !
Par conséquent, ComponentReference doit également être utilisé en scope Request !
De plus, appliquer cette règle améliorera la gestion de la mémoire par java.

Autre solution historique (moins bonne)

Dans une telle situation de managed-bean placé en scope Session, et avant que ComponentReference ne puisse être mis en œuvre, une autre solution consistait à déclarer la variable du composant JSF comme transient au lieu de private.
Ainsi, le serveur d’applications en environnement cluster ne cherchait pas à sérialiser cette variable membre et le setter du binding réaffectait tout simplement la variable membre.
Cela fonctionnait mais du point de vue d’une meilleure gestion mémoire, la solution du ComponentReference est meilleure.

Pourquoi ce conseil sur ComponentReference n’est-il pas appliqué automatiquement par l’IDE JDev ?

Bonne question ! J’ai interrogé le team Oracle qui m’a répondu que c’était prévu mais pas encore implémenté. Espérons que ce soit le cas pour la 2ème release de JDev12c qui devrait apparaitre dans quelque temps.
Si cela a mis du temps, c’est que le ComponentReference n’a pas toujours bien fonctionné par le passé mais ceci n’est plus le cas aujourd’hui.

Conclusion

Appliquer toujours cette règle d’or :

  • Utiliser toujours ComponentReference quel que soit le scope

Autre manière de dire la même chose que cette règle : il ne faut jamais gérer un binding JSF via une variable membre directement attachée au composant UI, notamment pour les managed-beans de scope Session ou pageFlowScope (ADF) ou Application … mais également pour le scope Request !

Sujet du prochain article (relié à cette remarque de conclusion) :

Si vous avez besoin de conserver des états de composants UI (par exemple, l’état d’un onglet actif), le placer en Scope Session n’est pas une bonne solution (y compris via ComponentReference). En effet, les composants JSF ne sont tenus de conserver automatiquement leur état que d’une Request à l’autre. Or, si vous vous baladez d’un taksflow à un autre et que vous voulez revenir au dernier onglet actif du Taskflow parent, cet état pourrait être perdu.
Il y a une solution automatique dans ADF qui est la mise en oeuvre de la couche MDS (MetaDataService) qui permet de mémoriser automatiquement l’état des composants. Mais si vous ne voulez pas utiliser MDS, il y a une autre solution grâce à un pattern tout simple à appliquer, sujet du prochain article.