BeanLint: Ein JavaBeans-Tool zur Fehlerbehebung, Teil 1

Alle paar Monate erhalte ich eine panische oder verwirrte E-Mail von einem JavaBeans-Neuling, der versucht, eine JavaBean mit einer zu erstellen, Imageund der nicht herausfinden kann, warum die BeanBox die Bean nicht lädt. Das Problem ist, dass dies java.awt.Imagenicht der SerializableFall ist, daher ist auch nichts enthalten java.awt.Image, zumindest ohne benutzerdefinierte Serialisierung.

Ich selbst habe unzählige Stunden damit verbracht, println()Anweisungen in den BeanBox-Code einzufügen und ihn dann neu zu kompilieren, um herauszufinden, warum meine Beans nicht geladen werden. Manchmal liegt es an einer einfachen, dummen Sache - wie dem Vergessen, den Null-Argument-Konstruktor oder sogar die Klasse als zu definieren public. In anderen Fällen stellt sich heraus, dass es etwas Dunkleres ist.

Der Fall der fehlenden Bohne

Während die Anforderungen zum Schreiben einer Java-Klasse als JavaBean einfach und unkompliziert sind, gibt es einige versteckte Auswirkungen, die viele Bean Builder-Tools nicht berücksichtigen. Diese kleinen Fallstricke können leicht einen Nachmittag verschlingen , wenn Sie Ihren Code durchsuchen und nach dem Grund suchen, warum Ihr Builder-Tool Ihre Bohne nicht finden kann. Wenn Sie Glück haben, wird ein Popup-Dialogfeld mit einer kryptischen Fehlermeldung angezeigt - etwas in der Art von "NoSuchMethodException caught in FoolTool Introspection"Wenn Sie Pech haben, wird die JavaBean, in die Sie so viel Schweiß gegossen haben, nicht in Ihrem Builder-Tool angezeigt, und Sie werden den Nachmittag damit verbringen, das Vokabular zu proben, von dem Ihre Mutter so sehr versucht hat, Sie zu heilen. Die BeanBox hat war in dieser Hinsicht lange Zeit ein ungeheuerlicher Straftäter, und obwohl es sich verbessert hat, wird es immer noch Eigenschaften und sogar ganze Bohnen verlieren, ohne dem Entwickler einen einzigen Hinweis darauf zu geben, warum.

Diesen Monat werde ich Sie aus dem "Land der fehlenden Bohne" herausführen, indem ich ein neues Tool namens "seltsamerweise" einführe BeanLint, das Klassen in JAR-Dateien analysiert und nach möglichen Problemen sucht, die die Klassen als Beans unbrauchbar machen würden. Dieses Tool deckt zwar nicht alle möglichen Bean-Probleme ab, identifiziert jedoch einige der häufigsten Probleme, die Beans entladbar machen.

Um zu verstehen, wie es BeanLintfunktioniert, werden wir uns diesen und den nächsten Monat mit einigen weniger bekannten Aspekten der Standard-Java-API befassen:

  • Wir erstellen einen benutzerdefinierten Klassenlader , der neue Java-Klassen aus einer JAR-Datei lädt

  • Wir werden den Reflexionsmechanismus verwenden, mit dem Java-Programme Java-Klassen analysieren können, um zu identifizieren, was in unseren Klassendateien enthalten ist

  • Wir werden das verwenden Introspector, um einen Bericht über alle Bean-ähnlichen Eigenschaften der Klasse für jede Klasse in der JAR-Datei zu erstellen, die alle Tests besteht (und daher eine potenzielle Bean ist).

Wenn wir fertig sind, verfügen Sie über ein nützliches Tool zum Debuggen Ihrer Beans, verstehen die Bean-Anforderungen besser und lernen gleichzeitig einige der coolen neuen Funktionen von Java kennen.

Bean Grundlagen

Damit eine Klassendatei eine JavaBean ist, gibt es zwei einfache Anforderungen:

  1. Die Klasse muss einen öffentlichen Konstruktor ohne Argumente haben (einen Konstruktor mit null Argumenten ).

  2. Die Klasse muss die leere Tag-Schnittstelle implementieren java.io.Serializable

Das ist es. Befolgen Sie diese beiden einfachen Regeln, und Ihre Klasse wird eine JavaBean sein. Die einfachste JavaBean sieht also ungefähr so ​​aus:

import java.io. *; öffentliche Klasse TinyBean implementiert Serializable {public TinyBean () {}}

Natürlich ist die obige Bohne nicht viel, aber dann haben wir nicht viel Arbeit investiert. Versuchen Sie einfach , eine solche Basiskomponente in ein anderes Komponentenframework zu schreiben. (Und es ist nicht fair, "Assistenten" oder andere Codegeneratoren zum Erstellen von Wrapper-Klassen oder Standardimplementierungen zu verwenden. Dies ist kein fairer Vergleich der Eleganz von JavaBeans mit einer anderen Technologie.)

Die TinyBeanKlasse hat keine Eigenschaften (außer vielleicht "Name"), keine Ereignisse und keine Methoden. Leider ist es immer noch einfach, versehentlich Klassen zu erstellen, die den Regeln zu folgen scheinen, jedoch in einem JavaBeans-Container wie der BeanBox oder Ihrer bevorzugten IDE (integrierte Entwicklungsumgebung) nicht ordnungsgemäß funktionieren.

Zum Beispiel würde die BeanBox unser TinyBeanoben genanntes nicht laden, wenn wir vergessen hätten, das Schlüsselwort publicin die Klassendefinition aufzunehmen. javacwürde eine Klassendatei für die Klasse erstellen, aber die BeanBox würde sich weigern, sie zu laden, und (bis vor kurzem sowieso) keinen Hinweis darauf geben, warum sie sich weigern würde. Um den Java-Mitarbeitern von Sun Anerkennung zu verschaffen, meldet die BeanBox jetzt normalerweise den Grund, warum eine Bean nicht geladen wird, oder den Grund, warum eine Eigenschaft nicht auf einem Eigenschaftenblatt angezeigt wird, und so weiter. Wäre es nicht schön, wenn wir ein Tool hätten, mit dem wir so viele Dinge wie möglich über solche Klassen überprüfen könnten - und uns vor solchen warnen könnten, die in einer JavaBeans-Umgebung Probleme verursachen könnten? Das ist das Ziel vonBeanLint: Um Ihnen als JavaBeans-Programmierer dabei zu helfen, Beans in ihren JAR-Dateien zu analysieren und nach möglichen Problemen zu suchen, damit Sie sie beheben können, bevor Sie sie im Testprozess oder - noch schlimmer - vor Ort antreffen.

Mögliche Bohnenprobleme

Da ich JavaBeans für diese Spalte entwickelt habe, habe ich wahrscheinlich die meisten Fehler gemacht, die man beim Schreiben einer JavaBean machen kann. In gewisser Weise hat mich die schweigende Natur der BeanBox gezwungen, mehr über Bohnen - und über Java - zu lernen, als ich es sonst hätte tun können. Die meisten JavaBeans-Entwickler würden es jedoch vorziehen, einfach funktionierende JavaBeans zu erstellen, die ordnungsgemäß funktionieren, und die "Wachstumserfahrungen" für ihr persönliches Leben zu speichern. Ich habe eine Liste möglicher Probleme mit einer Klassendatei zusammengestellt, die mit einer JavaBean Chaos anrichten kann. Diese Probleme treten beim Laden der Bean in einen Container oder bei der Verwendung der Bean in einer Anwendung auf. Bei der Serialisierung können Details leicht übersehen werden. Daher achten wir besonders auf die Anforderungen an die Serialisierbarkeit.

Hier sind einige häufige Probleme , die nicht Ursache Kompilierung-Fehler zu tun , sondern eine Klassendatei entweder nicht verursachen kann sein ein JavaBean, oder nicht korrekt funktionieren , wenn es in einen Behälter geladen wird :

  • Die Klasse hat keinen Konstruktor mit Nullargumenten. Dies ist lediglich ein Verstoß gegen die oben aufgeführte erste Anforderung und ein Fehler, auf den Nicht-Anfänger nicht häufig stoßen.

  • Die Klasse wird nicht implementiert Serializable. Dies ist ein Verstoß gegen die oben aufgeführte zweite Anforderung und leicht zu erkennen. Eine Klasse kann behaupten , Serializableden Vertrag umzusetzen und ihn dennoch nicht einzuhalten. In einigen Fällen können wir automatisch erkennen, wann dies geschehen ist.

  • Die Klasse selbst ist nicht deklariert public.

  • Die Klasse kann aus irgendeinem Grund nicht geladen werden. Klassen lösen manchmal Ausnahmen aus, wenn sie geladen werden. Dies liegt häufig daran, dass andere Klassen, von denen sie abhängen, für ClassLoaderdas zum Laden der Klasse verwendete Objekt nicht verfügbar sind . In diesem Artikel wird ein benutzerdefinierter Klassenlader geschrieben (siehe unten).

  • Die Klasse ist abstrakt. Während eine Komponentenklasse theoretisch abstrakt sein könnte, ist eine tatsächlich ausgeführte Instanz einer JavaBean immer eine Instanz einer konkreten (dh nicht abstrakten) Klasse. Abstrakte Klassen können per Definition nicht instanziiert werden, daher betrachten wir abstrakte Klassen nicht als Kandidaten für Beans.

  • Die Klasse implements Serializable, aber sie oder eine ihrer Basisklassen enthält nicht serialisierbare Felder. Das Standarddesign des Java-Serialisierungsmechanismus ermöglicht die Definition einer Klasse als implements Serializable, lässt jedoch zu, dass sie fehlschlägt, wenn tatsächlich eine Serialisierung versucht wird. Unsere BeanLintKlasse stellt sicher, dass alle geeigneten Felder einer SerializableKlasse tatsächlich vorhanden sind 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 ===

Die Ausgabe wurde etwas verkürzt, da die Auflistung der Ereignissätze und Methoden sehr lang ist und nicht viel zu unserer Diskussion hier beiträgt. Sie können die gesamte Ausgabe in der Datei output.html sehen, wenn Sie eine Vorstellung von der Menge der ausgegebenen Inhalte haben möchten BeanLint.

Beachten Sie, dass BeanLintdie Probleme mit den fehlerhaften Klassendateien korrekt identifiziert wurden:

Klasse u0 ist keine JavaBean, weil: die Klasse nicht öffentlich ist Klasse z ist keine JavaBean, weil: die Klasse nicht öffentlich ist Klasse y ist keine JavaBean, weil: sie keinen Null-Argument-Konstruktor hat Klasse x ist keine JavaBean, weil: die Klasse ist nicht serialisierbar Klasse w ist keine JavaBean, weil: ihr Nullargument-Konstruktor nicht öffentlich ist Klasse u ist keine JavaBean, weil: die folgenden Felder der Klasse nicht serialisierbar sind: Klasse java.awt.Image i (definiert in u0)