BeanLint: Alat pemecahan masalah JavaBeans, Bagian 1

Setiap beberapa bulan, saya menerima email panik atau bingung dari orang baru JavaBeans yang mencoba membuat JavaBean yang berisi Imagedan tidak tahu mengapa BeanBox tidak memuat kacang. Masalahnya adalah itu java.awt.Imagetidak Serializable, oleh karena itu tidak ada apa pun yang berisi java.awt.Image, setidaknya tanpa serialisasi khusus.

Saya sendiri telah menghabiskan waktu berjam-jam untuk memasukkan println()pernyataan ke dalam kode BeanBox kemudian mengompilasinya kembali, mencoba mencari tahu mengapa kacang saya tidak dapat dimuat. Terkadang itu karena beberapa hal yang sederhana dan bodoh - seperti lupa untuk mendefinisikan konstruktor tanpa argumen, atau bahkan kelas, sebagai public. Di lain waktu, ternyata ada sesuatu yang lebih kabur.

Kasus kacang yang hilang

Meskipun persyaratan untuk menulis kelas Java sebagai JavaBean sederhana dan mudah, ada beberapa implikasi tersembunyi yang tidak ditangani oleh banyak alat pembuat kacang. Gotcha kecil ini dapat dengan mudah menghabiskan sore hari, saat Anda memburu kode Anda, mencari alasan alat pembuat Anda tidak dapat menemukan kacang Anda. Jika beruntung, Anda akan mendapatkan kotak dialog pop-up dengan pesan kesalahan samar - sesuatu di sepanjang baris "NoSuchMethodException caught in FoolTool Introspection. "Jika Anda tidak beruntung, JavaBean yang telah Anda curahkan begitu banyak keringat akan menolak untuk muncul di alat pembuat Anda, dan Anda akan menghabiskan sore hari untuk melatih kosakata yang dicoba keras oleh ibu Anda untuk menyembuhkan Anda. BeanBox memiliki telah lama menjadi pelanggar berat dalam hal ini, dan meskipun telah diperbaiki, properti dan bahkan biji utuh masih akan hilang tanpa memberikan petunjuk satu pun kepada pengembang tentang alasannya.

Bulan ini, saya akan memandu Anda keluar dari "tanah kacang yang hilang" dengan memperkenalkan alat baru yang disebut, anehnya BeanLint, yang menganalisis kelas dalam file jar, mencari kemungkinan masalah yang akan membuat kelas tidak dapat digunakan sebagai kacang. Meskipun alat ini tidak mencakup setiap masalah kacang yang mungkin terjadi, alat ini mengidentifikasi beberapa masalah umum utama yang membuat biji tidak dapat dimuat.

Untuk memahami cara BeanLintkerja sihirnya, bulan ini dan selanjutnya kita akan mempelajari beberapa sudut yang kurang dikenal dari API Java standar:

  • Kami akan membuat pemuat kelas khusus , yang memuat kelas Java baru dari file jar

  • Kami akan menggunakan mekanisme refleksi , yang memungkinkan program Java menganalisis kelas Java, untuk mengidentifikasi apa yang ada di dalam file kelas kami

  • Kita akan menggunakan Introspectoruntuk menghasilkan laporan dari semua properti kelas seperti kacang untuk setiap kelas dalam file jar yang lolos semua tes (dan, oleh karena itu, kacang potensial)

Pada saat kami selesai, Anda akan memiliki alat yang berguna untuk men-debug kacang Anda, Anda akan lebih memahami persyaratan kacang, dan Anda akan belajar tentang beberapa fitur baru Java yang keren pada saat yang sama.

Dasar-dasar kacang

Agar file kelas menjadi JavaBean, ada dua persyaratan sederhana:

  1. Kelas harus memiliki konstruktor publik tanpa argumen ( konstruktor nol- argumen )

  2. Kelas harus mengimplementasikan antarmuka tag kosong java.io.Serializable

Itu dia. Ikuti dua aturan sederhana tersebut, dan kelas Anda akan menjadi JavaBean. JavaBean yang paling sederhana, terlihat seperti ini:

impor java.io. *; public class TinyBean mengimplementasikan Serializable {public TinyBean () {}}

Tentu saja, kacang di atas tidak baik untuk banyak hal, tapi kemudian kami tidak bekerja keras untuk itu. Hanya mencoba menulis komponen dasar seperti ini dalam rangka komponen lain. (Dan tidak adil menggunakan "wizards" atau generator kode lain untuk membuat kelas pembungkus atau implementasi default. Itu bukan perbandingan yang adil antara keanggunan JavaBeans versus teknologi lain.)

The TinyBeankelas tidak memiliki sifat (kecuali, mungkin, "nama"), ada kegiatan, dan tidak ada metode. Sayangnya, masih mudah untuk secara tidak sengaja membuat kelas yang tampaknya mengikuti aturan, namun tidak beroperasi dengan benar dalam wadah JavaBeans seperti BeanBox atau IDE favorit Anda (lingkungan pengembangan terintegrasi).

Misalnya, BeanBox tidak akan memuat di TinyBeanatas jika kita lupa memasukkan kata kunci publicke definisi kelas. javacakan membuat file kelas untuk kelas tersebut, tetapi BeanBox akan menolak untuk memuatnya, dan (sampai saat ini) tidak akan memberikan indikasi mengapa ia akan menolak. Untuk memberikan kredit kepada masyarakat Java Sun, BeanBox sekarang biasanya melaporkan alasan kacang tidak dapat dimuat, atau alasan properti tidak muncul di lembar properti, dan seterusnya. Bukankah lebih baik, jika kita memiliki alat untuk memeriksa sebanyak mungkin hal tentang kelas-kelas tersebut - dan memperingatkan kita tentang hal-hal yang mungkin menyebabkan masalah ketika digunakan dalam lingkungan JavaBeans? Itulah tujuan dariBeanLint: untuk membantu Anda, sebagai programmer JavaBeans, menganalisis bean di dalam file jar mereka, mencari kemungkinan masalah sehingga Anda dapat memperbaikinya sebelum Anda menemui mereka dalam proses pengujian atau - bahkan lebih buruk - di lapangan.

Masalah kacang potensial

Saat saya mengembangkan JavaBeans untuk kolom ini, saya mungkin telah membuat sebagian besar kesalahan yang dapat dilakukan seseorang saat menulis JavaBean. Di satu sisi, sifat pendiam BeanBox telah memaksa saya untuk belajar lebih banyak tentang kacang - dan tentang Java - daripada yang seharusnya saya lakukan. Namun, sebagian besar pengembang JavaBeans lebih suka hanya menghasilkan JavaBeans yang berfungsi dan beroperasi dengan benar, dan menyimpan "pengalaman berkembang" untuk kehidupan pribadi mereka. Saya telah mengumpulkan daftar kemungkinan masalah dengan file kelas yang dapat mendatangkan malapetaka dengan JavaBean. Masalah ini terjadi selama proses memuat kacang ke dalam wadah, atau dalam menggunakan kacang dalam aplikasi. Sangat mudah untuk melewatkan detail dalam serialisasi, jadi kami memberikan perhatian khusus pada persyaratan serialisasi.

Berikut adalah beberapa masalah umum yang tidak menyebabkan kesalahan saat kompilasi tetapi dapat menyebabkan file kelas baik untuk tidak menjadi sebuah JavaBean, atau untuk tidak beroperasi dengan benar setelah itu dimuat ke dalam wadah:

  • Kelas tidak memiliki konstruktor tanpa argumen. Ini hanyalah pelanggaran persyaratan pertama yang tercantum di atas, dan merupakan kesalahan yang tidak sering ditemui oleh non-pemula.

  • Kelas tidak menerapkan Serializable. Ini adalah pelanggaran persyaratan kedua yang tercantum di atas dan mudah dikenali. Kelas dapat mengklaim untuk menerapkan Serializable, namun tidak menindaklanjuti kontrak. Dalam beberapa kasus, kami dapat mendeteksi secara otomatis saat ini telah terjadi.

  • Kelas itu sendiri tidak dideklarasikan public.

  • Kelas gagal memuat karena beberapa alasan. Kelas terkadang menampilkan pengecualian saat dimuat. Seringkali, ini karena kelas lain yang mereka andalkan tidak tersedia dari ClassLoaderobjek yang digunakan untuk memuat kelas. Kami akan menulis pemuat kelas khusus di artikel ini (lihat di bawah).

  • Kelasnya abstrak. Sementara sebuah kelas komponen, secara teori, bisa jadi abstrak, sebuah instance JavaBean yang sedang berjalan selalu merupakan instance dari beberapa kelas konkret (yaitu, non-abstrak). Kelas abstrak tidak dapat dibuat instance-nya, menurut definisi, jadi kami tidak akan menganggap kelas abstrak sebagai kandidat sebagai kacang.

  • Kelas implements Serializable, namun itu atau salah satu kelas dasarnya berisi bidang yang tidak dapat diserialkan. Desain mekanisme serialisasi Java default memungkinkan kelas untuk didefinisikan sebagai implements Serializable, tetapi mengizinkannya untuk gagal ketika serialisasi benar-benar dicoba. BeanLintKelas kami memastikan bahwa semua bidang yang sesuai dari Serializablekelas benar-benar ada Serializable.

Kelas yang gagal dari salah satu masalah di atas dapat dipastikan tidak akan beroperasi dengan benar sebagai JavaBean, bahkan jika dua persyaratan kacang dasar, yang dinyatakan di awal, terpenuhi. Untuk setiap masalah ini, kami akan menentukan tes yang mendeteksi masalah tertentu dan melaporkannya. Di BeanLintkelas, setiap file kelas dalam makhluk file jar dianalisis bahwa tidak lulus semua tes ini kemudian introspected (dianalisis menggunakan kelas java.beans.Introspector) untuk menghasilkan laporan dari atribut kacang (properti, set acara, Customizer, dan sebagainya). java.beans.Introspectoradalah kelas package java.beansyang menggunakan mekanisme refleksi Java 1.1 untuk menemukan (atau membuat) java.beans.BeanInfoobjek untuk JavaBean. Kami akan membahas refleksi dan introspeksi bulan depan.

Sekarang mari kita lihat kode sumber untuk BeanLintmelihat bagaimana menganalisis kelas kacang potensial.

Memperkenalkan BeanLint

Di "masa lalu yang indah" (yang biasanya berarti, "saat saya masih berpikir saya tahu segalanya"), pemrogram C pada sistem operasi Unix akan menggunakan program yang dipanggil lintuntuk mencari potensi titik masalah runtime dalam program C mereka. Untuk menghormati alat yang mulia dan berguna ini, saya telah memanggil kelas analisis kacang saya yang sederhana BeanLint.

Alih-alih menyajikan seluruh kode sumber dalam satu potongan besar yang tidak dapat dicerna, kita akan melihatnya satu per satu, dan saya akan menjelaskan di sepanjang jalan berbagai idiom tentang bagaimana Java menangani file kelas. Pada saat kita selesai, kita akan menulis pemuat kelas, menggunakan sejumlah kelas yang terhormat java.lang.reflect, dan telah memperoleh kenalan yang mengangguk dengan kelas java.beans.Introspector. Pertama, mari kita lihat cara BeanLintkerjanya untuk melihat apa yang dilakukannya, dan kemudian kita akan mempelajari detail implementasinya.

Kacang busuk

Di bagian ini Anda akan melihat beberapa file kelas dengan berbagai masalah, dengan masalah yang ditunjukkan di bawah kode. Kita akan membuat file jar yang berisi kelas-kelas ini, dan melihat apa BeanLintfungsinya.


impor 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 ===

Outputnya agak dipersingkat karena daftar set acara dan metode yang sangat panjang tidak menambah banyak diskusi kita di sini. Anda dapat melihat keseluruhan output di file output.html, jika Anda ingin mengetahui jumlah barang yang BeanLintdikeluarkan.

Perhatikan bahwa BeanLintdengan benar mengidentifikasi masalah dengan file kelas yang buruk:

class u0 bukan JavaBean karena: class bukan public class z bukan JavaBean karena: class bukan public class y bukan JavaBean karena: tidak ada konstruktor argumen nol class x bukan JavaBean karena: kelas tidak berseri Kelas w bukan JavaBean karena: konstruktor nol-argumennya bukan kelas publik u bukan JavaBean karena: bidang kelas berikut tidak dapat berseri: kelas java.awt.Image i (didefinisikan dalam u0)