BeanLint: un outil de dépannage JavaBeans, partie 1

Tous les deux mois, je reçois un e-mail paniqué ou déconcerté d'un néophyte JavaBeans qui essaie de créer un JavaBean contenant un Imageet qui n'arrive pas à comprendre pourquoi le BeanBox ne charge pas le bean. Le problème est que ce java.awt.Imagen'est pas le cas Serializable, donc tout ce qui contient un java.awt.Image, du moins sans sérialisation personnalisée.

J'ai moi-même passé d'innombrables heures à mettre des println()instructions dans le code BeanBox, puis à le recompiler, à essayer de comprendre pourquoi mes beans ne se chargent pas. Parfois, cela est dû à une chose simple et stupide - comme oublier de définir le constructeur à argument zéro, ou même la classe, comme public. D'autres fois, cela s'avère être quelque chose de plus obscur.

Le cas du haricot manquant

Bien que les exigences pour écrire une classe Java en tant que JavaBean soient simples et directes, il existe des implications cachées que de nombreux outils de création de bean ne traitent pas. Ces petits pièges peuvent facilement manger un après-midi, alors que vous parcourez votre code, en recherchant la raison pour laquelle votre outil de création ne peut pas trouver votre bean. Si vous avez de la chance, vous obtiendrez une boîte de dialogue contextuelle avec un message d'erreur cryptique - quelque chose du genre "NoSuchMethodException caught in FoolTool Introspection"Si vous êtes malchanceux, le JavaBean dans lequel vous avez versé tant de sueur refusera d'apparaître dans votre outil de création, et vous passerez l'après-midi à répéter le vocabulaire dont votre mère a tant essayé de vous guérir. Le BeanBox a a longtemps été un contrevenant flagrant à cet égard, et bien qu'il se soit amélioré, il perdra encore des propriétés et même des haricots entiers sans fournir au développeur un seul indice quant à la raison.

Ce mois-ci, je vais vous faire sortir du «pays du bean manquant» en introduisant un nouvel outil appelé, curieusement BeanLint, qui analyse les classes dans les fichiers jar, à la recherche de problèmes possibles qui rendraient les classes inutilisables en tant que beans. Bien que cet outil ne couvre pas tous les problèmes de bean possibles, il identifie certains des principaux problèmes courants qui rendent les beans déchargeables.

Afin de comprendre comment BeanLintfonctionne sa magie, ce mois-ci et le prochain, nous allons nous plonger dans certains des coins les moins connus de l'API Java standard:

  • Nous allons créer un chargeur de classe personnalisé , qui charge de nouvelles classes Java à partir d'un fichier jar

  • Nous utiliserons le mécanisme de réflexion , qui permet aux programmes Java d'analyser les classes Java, pour identifier le contenu de nos fichiers de classe

  • Nous utiliserons le Introspectorpour produire un rapport de toutes les propriétés beanlike de la classe pour toute classe du fichier jar qui passe tous les tests (et est donc un bean potentiel)

Lorsque nous aurons terminé, vous aurez un outil utile pour déboguer vos beans, vous comprendrez mieux les exigences des beans et vous en apprendrez plus sur certaines des nouvelles fonctionnalités intéressantes de Java en même temps.

Bases du haricot

Pour qu'un fichier de classe soit un JavaBean, il existe deux exigences simples:

  1. La classe doit avoir un constructeur public sans arguments (un constructeur à zéro argument )

  2. La classe doit implémenter l'interface de balise vide java.io.Serializable

C'est ça. Suivez ces deux règles simples, et votre classe sera un JavaBean. Le JavaBean le plus simple ressemble donc à ceci:

import java.io. *; La classe publique TinyBean implémente Serializable {public TinyBean () {}}

Bien sûr, le haricot ci-dessus n'est pas bon pour beaucoup, mais nous n'y avons pas mis beaucoup de travail. Essayez simplement d' écrire un composant de base comme celui-ci dans un autre framework de composants. (Et il n'est pas juste d'utiliser des «assistants» ou d'autres générateurs de code pour créer des classes wrapper ou des implémentations par défaut. Ce n'est pas une comparaison juste de l'élégance de JavaBeans par rapport à une autre technologie.)

La TinyBeanclasse n'a aucune propriété (sauf, peut-être, "nom"), aucun événement et aucune méthode. Malheureusement, il est toujours facile de créer accidentellement des classes qui semblent suivre les règles, mais qui ne fonctionnent pas correctement dans un conteneur JavaBeans tel que BeanBox ou votre IDE préféré (environnement de développement intégré).

Par exemple, le BeanBox ne chargerait pas notre TinyBeanci-dessus si nous avions oublié d'inclure le mot-clé publicdans la définition de classe. javaccréerait un fichier de classe pour la classe, mais le BeanBox refuserait de le charger, et (jusqu'à récemment de toute façon) ne donnerait aucune indication sur les raisons pour lesquelles il refuserait. Pour donner du crédit aux utilisateurs Java de Sun, le BeanBox indique désormais généralement la raison pour laquelle un bean ne se charge pas, ou la raison pour laquelle une propriété n'apparaît pas sur une feuille de propriétés, et ainsi de suite. Ne serait-ce pas bien, cependant, si nous avions un outil pour vérifier autant de choses que possible sur ces classes - et nous avertir de celles susceptibles de causer des problèmes lorsqu'elles sont utilisées dans un environnement JavaBeans? C'est le but deBeanLint: pour vous aider, en tant que programmeur JavaBeans, à analyser les beans dans leurs fichiers jar, à la recherche d'éventuels problèmes afin que vous puissiez les résoudre avant de les rencontrer dans le processus de test ou - pire encore - sur le terrain.

Problèmes potentiels de haricots

En développant des JavaBeans pour cette colonne, j'ai probablement fait la plupart des erreurs que l'on peut faire lors de l'écriture d'un JavaBean. D'une certaine manière, la nature taciturne de la BeanBox m'a obligé à en apprendre plus sur les beans - et sur Java - que je ne l'aurais autrement. La plupart des développeurs JavaBeans, cependant, préféreraient simplement produire des JavaBeans fonctionnels qui fonctionnent correctement, et sauver les «expériences de croissance» pour leur vie personnelle. J'ai rassemblé une liste de problèmes possibles avec un fichier de classe qui peuvent faire des ravages avec un JavaBean. Ces problèmes surviennent pendant le processus de chargement du grain dans un conteneur ou lors de l'utilisation du grain dans une application. Il est facile de rater des détails dans la sérialisation, nous accordons donc une attention particulière aux exigences de sérialisation.

Voici quelques problèmes courants qui ne provoquent pas d'erreurs de compilation, mais peuvent empêcher un fichier de classe d' être un JavaBean ou de ne pas fonctionner correctement une fois qu'il est chargé dans un conteneur:

  • La classe n'a pas de constructeur à argument nul. Il s'agit simplement d'une violation de la première exigence mentionnée ci-dessus et d'une erreur rarement rencontrée par les non-débutants.

  • La classe ne met pas en œuvre Serializable. Ceci est une violation de la deuxième exigence énumérée ci-dessus et est facile à repérer. Une classe peut prétendre mettre en œuvre Serializable, mais ne pas donner suite au contrat. Dans certains cas, nous pouvons détecter automatiquement lorsque cela s'est produit.

  • La classe elle-même n'est pas déclarée public.

  • La classe ne se charge pas pour une raison quelconque. Les classes lèvent parfois des exceptions lors de leur chargement. Cela est souvent dû au fait que les autres classes dont elles dépendent ne sont pas disponibles à partir de l' ClassLoaderobjet utilisé pour charger la classe. Nous allons écrire un chargeur de classe personnalisé dans cet article (voir ci-dessous).

  • La classe est abstraite. Alors qu'une classe de composant, en théorie, pourrait être abstraite, une instance en cours d'exécution réelle d'un JavaBean est toujours une instance d'une classe concrète (c'est-à-dire non abstraite). Les classes abstraites ne peuvent pas être instanciées, par définition, et nous ne considérerons donc pas les classes abstraites comme des candidats pour être des beans.

  • La classe implements Serializable, mais elle ou l'une de ses classes de base contient des champs non sérialisables. La conception du mécanisme de sérialisation Java par défaut permet à une classe d'être définie comme implements Serializable, mais lui permet d'échouer lorsque la sérialisation est réellement tentée. Notre BeanLintclasse garantit que tous les champs appropriés d'une Serializableclasse le sont réellement Serializable.

A class that fails any of the problems above can be fairly certain not to operate correctly as a JavaBean, even if the two basic bean requirements, stated at the outset, are met. For each of these problems, then, we'll define a test that detects the particular problem and reports it. In the BeanLint class, any class file in the jar file being analyzed that does pass all of these tests is then introspected (analyzed using the class java.beans.Introspector) to produce a report of the bean's attributes (properties, event sets, customizer, and so on). java.beans.Introspector is a class in the package java.beans that uses the Java 1.1 reflection mechanism to find (or create) a java.beans.BeanInfo object for a JavaBean. We'll cover reflection and introspection next month.

Now let's take a look at the source code for BeanLint to see how to analyze potential bean classes.

Introducing BeanLint

In the "good old days" (which usually means, "back when I still thought I knew everything"), C programmers on the Unix operating system would use a program called lint to look for potential runtime trouble spots in their C programs. In honor of this venerable and useful tool, I have called my humble bean-analysis class BeanLint.

Instead of presenting the entire source code in one huge, indigestible chunk, we're going to look at it one piece at a time, and I will explain along the way various idioms concerning how Java deals with class files. By the time we're through, we'll have written a class loader, used a respectable number of classes in java.lang.reflect, and have acquired a nodding acquaintance with the class java.beans.Introspector. First, let's have a look at BeanLint in action to see what it does, and then we'll delve into the details of its implementation.

Bad beans

In this section you'll see some class files with various problems, with the problem indicated below the code. We're going to create a jar file containing these classes, and see what BeanLint does with them.


import java.io.*;

public class w implements Serializable { w() { } }

Problem:

 Zero-argument constructor not

public


public class x { public x() { } } 

Problem:

 Not

Serializable.


import java.io.*;

public class y implements Serializable { public y(String y_) { } }

Problem:

 No zero-argument constructor.


import java.io.*;

class z implements Serializable { public z() { } }

Problem:

 Class not public.


import java.io.*; import java.awt.*;

class u0 implements Serializable { private Image i; public u0() { } }

public class u extends u0 implements Serializable { public u() { } }

Problem:

 Contains a nonserializable object or reference.


import java.io.*;

public class v extends java.awt.Button implements Serializable { public v() { } public v(String s) { super(s); } }

Problem:

 Nothing -- should work fine!


Each of these aspiring beans, except the last one, has potential problems. The last one not only is a bean, but operates as one. After compiling all of these classes, we create a jar file like this:

$ jar cvf BadBeans.jar *.class adding: u.class (in=288) (out=218) (deflated 24%) adding: u0.class (in=727) (out=392) (deflated 46% adding: w.class (in=302) (out=229) (deflated 24%) adding: x.class (in=274) (out=206) (deflated 24%) adding: y.class (in=362) (out=257) (deflated 29%) adding: z.class (in=302) (out=228) (deflated 24%) adding: v.class (in=436) (out=285) (deflated 34%) 

We aren't going to include a manifest file (which is a file inside a jar file that describes the jar file's contents -- see "Opening the jar" below) in the jar file because BeanLint doesn't deal with manifest files. Parsing the manifest file and comparing it to the contents of the jar would be an interesting exercise if you want to extend what BeanLint can do.

Let's run BeanLint on the jar file and see what happens:

=== Analyzing class u0 === class u0 is not a JavaBean because: the class is not public

=== Analyzing class z === class z is not a JavaBean because: the class is not public

=== Analyzing class y === class y is not a JavaBean because: it has no zero-argument constructor

=== Analyzing class x === class x is not a JavaBean because: the class is not Serializable

=== Analyzing class w === class w is not a JavaBean because: its zero-argument constructor is not public

=== Analyzing class v === Note: java.awt.Button defines custom serialization Note: java.awt.Component defines custom serialization v passes all JavaBean tests

Introspection Report -------------------- Class: v Customizer class: none

Properties: boolean enabled {isEnabled, setEnabled} (... many more properties)

Event sets: java.awt.event.MouseListener mouse (... many more event sets)

Methods: public boolean java.awt.Component.isVisible() (... many, many more methods -- sheesh!)

=== End of class v ===

=== Analyzing class u === class u is not a JavaBean because: the following fields of the class are not Serializable: class java.awt.Image i (defined in u0) === End of class u ===

La sortie a été quelque peu raccourcie car la liste des ensembles d'événements et des méthodes est très longue n'ajoute pas grand-chose à notre discussion ici. Vous pouvez voir la sortie entière dans le fichier output.html, si vous voulez avoir une idée de la quantité de choses produite BeanLint.

Notez que BeanLintcorrectement identifié les problèmes avec les fichiers de mauvaise classe:

la classe u0 n'est pas un JavaBean car: la classe n'est pas publique la classe z n'est pas un JavaBean car: la classe n'est pas publique la classe y n'est pas un JavaBean car: elle n'a pas de constructeur à argument nul la classe x n'est pas un JavaBean car: le class n'est pas Serializable classe w n'est pas un JavaBean car: son constructeur à argument nul n'est pas public class u n'est pas un JavaBean car: les champs suivants de la classe ne sont pas Serializable: class java.awt.Image i (définie en u0)