Waspadai bahaya Pengecualian generik

Saat mengerjakan proyek baru-baru ini, saya menemukan sepotong kode yang melakukan pembersihan sumber daya. Karena memiliki banyak panggilan berbeda, ini berpotensi menimbulkan enam pengecualian berbeda. Pemrogram asli, dalam upaya untuk menyederhanakan kode (atau hanya menyimpan ketikan), menyatakan bahwa metode melempar Exceptiondaripada enam pengecualian berbeda yang dapat dilemparkan. Ini memaksa kode panggilan untuk dibungkus dalam blok coba / tangkap yang tertangkap Exception. Pemrogram memutuskan bahwa karena kode itu untuk tujuan pembersihan, kasus kegagalan tidak penting, jadi blok penangkap tetap kosong saat sistem dimatikan.

Jelas, ini bukan praktik pemrograman terbaik, tetapi sepertinya tidak ada yang salah ... kecuali untuk masalah logika kecil di baris ketiga kode asli:

Daftar 1. Kode pembersihan asli

private void cleanupConnections () melempar ExceptionOne, ExceptionTwo {untuk (int i = 0; i <connection.length; i ++) {connection [i] .release (); // Melempar ExceptionOne, ExceptionTwo koneksi [i] = null; } koneksi = null; } abstract void cleanupFiles () melempar ExceptionThree, ExceptionFour; void abstrak yang dilindungi removeListeners () melempar ExceptionFive, ExceptionSix; public void cleanupEverything () memunculkan Exception {cleanupConnections (); cleanupFiles (); removeListeners (); } public void done () {try {doStuff (); cleanupEverything (); doMoreStuff (); } menangkap (Pengecualian e) {}}

Di bagian lain dari kode, connectionslarik tidak diinisialisasi hingga sambungan pertama dibuat. Tetapi jika koneksi tidak pernah dibuat, maka array koneksi adalah null. Jadi dalam beberapa kasus, panggilan untuk connections[i].release()menghasilkan file NullPointerException. Ini adalah masalah yang relatif mudah diperbaiki. Cukup tambahkan cek untuk connections != null.

Namun, pengecualian tersebut tidak pernah dilaporkan. Itu dilemparkan cleanupConnections(), dilemparkan lagi cleanupEverything(), dan akhirnya tertangkap done(). The done()Metode tidak melakukan apa-apa dengan pengecualian, bahkan tidak log itu. Dan karena cleanupEverything()hanya dipanggil done(), pengecualian tidak pernah terlihat. Jadi kodenya tidak pernah diperbaiki.

Jadi, dalam skenario kegagalan, metode cleanupFiles()dan removeListeners()tidak pernah dipanggil (sehingga sumber dayanya tidak pernah dirilis), dan doMoreStuff()tidak pernah dipanggil, dengan demikian, pemrosesan akhir done()tidak pernah selesai. Lebih buruk lagi, done()tidak dipanggil saat sistem dimatikan; melainkan dipanggil untuk menyelesaikan setiap transaksi. Jadi sumber daya bocor di setiap transaksi.

Masalah ini jelas merupakan masalah utama: kesalahan tidak dilaporkan dan sumber daya bocor. Tetapi kodenya sendiri tampaknya cukup polos, dan, dari cara kode itu ditulis, masalah ini terbukti sulit dilacak. Namun, dengan menerapkan beberapa pedoman sederhana, masalah dapat ditemukan dan diperbaiki:

  • Jangan abaikan pengecualian
  • Jangan menangkap generik Exceptions
  • Jangan membuang Exceptions generik

Jangan abaikan pengecualian

Masalah yang paling jelas dengan kode Daftar 1 adalah bahwa kesalahan dalam program diabaikan sepenuhnya. Pengecualian yang tidak terduga (pengecualian, menurut sifatnya, tidak terduga) sedang dilemparkan, dan kode tidak siap untuk menangani pengecualian tersebut. Pengecualian bahkan tidak dilaporkan karena kode mengasumsikan pengecualian yang diharapkan tidak akan memiliki konsekuensi.

Dalam kebanyakan kasus, pengecualian harus, paling tidak, dicatat. Beberapa paket pencatatan (lihat bilah sisi "Pengecualian Pencatatan") dapat mencatat kesalahan dan pengecualian sistem tanpa mempengaruhi kinerja sistem secara signifikan. Kebanyakan sistem pencatatan juga memungkinkan jejak tumpukan dicetak, sehingga memberikan informasi berharga tentang di mana dan mengapa pengecualian terjadi. Terakhir, karena log biasanya ditulis ke file, catatan pengecualian dapat ditinjau dan dianalisis. Lihat Listing 11 di sidebar untuk contoh logging jejak tumpukan.

Pengecualian logging tidak penting dalam beberapa situasi tertentu. Salah satunya adalah membersihkan sumber daya di klausa terakhir.

Pengecualian akhirnya

Pada Listing 2, beberapa data dibaca dari sebuah file. File harus ditutup terlepas dari apakah pengecualian membaca data, jadi close()metode ini dibungkus dengan klausa akhirnya. Tetapi jika kesalahan menutup file, tidak banyak yang bisa dilakukan:

Daftar 2

public void loadFile (String fileName) melempar IOException {InputStream in = null; coba {in = new FileInputStream (fileName); readSomeData (in); } akhirnya {if (in! = null) {try {in.close (); } catch (IOException ioe) {// Ignored}}}}

Perhatikan bahwa loadFile()masih melaporkan IOExceptionke metode panggilan jika pemuatan data yang sebenarnya gagal karena masalah I / O (input / output). Perhatikan juga bahwa meskipun pengecualian dari close()diabaikan, kode menyatakan itu secara eksplisit dalam komentar untuk menjelaskan kepada siapa pun yang mengerjakan kode. Anda dapat menerapkan prosedur yang sama ini untuk membersihkan semua aliran I / O, menutup soket dan koneksi JDBC, dan seterusnya.

Hal penting tentang mengabaikan pengecualian adalah memastikan bahwa hanya satu metode yang dibungkus dalam blok coba / tangkap pengabaian (sehingga metode lain di blok pelingkup masih dipanggil) dan bahwa pengecualian tertentu ditangkap. Keadaan khusus ini sangat berbeda dari menangkap generik Exception. Dalam semua kasus lainnya, pengecualian harus (paling tidak) dicatat dalam log, lebih disukai dengan pelacakan tumpukan.

Jangan menangkap Pengecualian umum

Seringkali dalam perangkat lunak yang kompleks, blok kode tertentu menjalankan metode yang memberikan berbagai pengecualian. Pemuatan secara dinamis kelas dan instantiate obyek dapat membuang beberapa pengecualian yang berbeda, termasuk ClassNotFoundException, InstantiationException, IllegalAccessException, dan ClassCastException.

Alih-alih menambahkan empat blok catch yang berbeda ke blok try, programmer yang sibuk dapat dengan mudah membungkus panggilan metode dalam blok coba / tangkap yang menangkap generik Exception(lihat Listing 3 di bawah). Meskipun tampaknya tidak berbahaya, beberapa efek samping yang tidak diinginkan dapat terjadi. Misalnya, jika className()null, Class.forName()akan melempar NullPointerException, yang akan ditangkap dalam metode.

Dalam hal ini, blok catch menangkap pengecualian yang tidak pernah dimaksudkan untuk ditangkap karena a NullPointerExceptionadalah subkelas RuntimeException, yang pada gilirannya, merupakan subkelas Exception. Jadi generik catch (Exception e)tangkapan semua subclass dari RuntimeException, termasuk NullPointerException, IndexOutOfBoundsException, dan ArrayStoreException. Biasanya, seorang programmer tidak bermaksud untuk menangkap pengecualian tersebut.

Dalam Kode 3, null classNamehasil di a NullPointerException, yang menunjukkan ke metode panggilan bahwa nama kelas tidak valid:

Daftar 3

public SomeInterface buildInstance (String className) {SomeInterface impl = null; coba {Class clazz = Class.forName (className); impl = (SomeInterface) clazz.newInstance (); } catch (Exception e) {log.error ("Error membuat kelas:" + className); } return impl; }

Konsekuensi lain dari klausa tangkapan umum adalah bahwa penebangan dibatasi karena catchtidak mengetahui pengecualian spesifik yang ditangkap. Beberapa programmer, ketika dihadapkan pada masalah ini, menggunakan tanda centang untuk melihat tipe pengecualian (lihat Listing 4), yang bertentangan dengan tujuan penggunaan blok catch:

Daftar 4

catch (Exception e) {if (e instanceof ClassNotFoundException) {log.error ("Nama kelas tidak valid:" + className + "," + e.toString ()); } else {log.error ("Tidak dapat membuat kelas:" + className + "," + e.toString ()); }}

Kode 5 memberikan contoh lengkap untuk menangkap pengecualian khusus yang mungkin menarik bagi pemrogram. instanceofOperator tidak diperlukan karena pengecualian khusus ditangkap. Setiap pengecualian diperiksa ( ClassNotFoundException, InstantiationException, IllegalAccessException) tertangkap dan ditangani. Kasus khusus yang akan menghasilkan ClassCastException(kelas memuat dengan benar, tetapi tidak mengimplementasikan SomeInterfaceantarmuka) juga diverifikasi dengan memeriksa pengecualian itu:

Daftar 5

public SomeInterface buildInstance(String className) { SomeInterface impl = null; try { Class clazz = Class.forName(className); impl = (SomeInterface)clazz.newInstance(); } catch (ClassNotFoundException e) { log.error("Invalid class name: " + className + ", " + e.toString()); } catch (InstantiationException e) { log.error("Cannot create class: " + className + ", " + e.toString()); } catch (IllegalAccessException e) { log.error("Cannot create class: " + className + ", " + e.toString()); } catch (ClassCastException e) { log.error("Invalid class type, " + className + " does not implement " + SomeInterface.class.getName()); } return impl; } 

In some cases, it is preferable to rethrow a known exception (or perhaps create a new exception) than try to deal with it in the method. This allows the calling method to handle the error condition by putting the exception into a known context.

Listing 6 below provides an alternate version of the buildInterface() method, which throws a ClassNotFoundException if a problem occurs while loading and instantiating the class. In this example, the calling method is assured to receive either a properly instantiated object or an exception. Thus, the calling method does not need to check if the returned object is null.

Note that this example uses the Java 1.4 method of creating a new exception wrapped around another exception to preserve the original stack trace information. Otherwise, the stack trace would indicate the method buildInstance() as the method where the exception originated, instead of the underlying exception thrown by newInstance():

Listing 6

public SomeInterface buildInstance(String className) throws ClassNotFoundException { try { Class clazz = Class.forName(className); return (SomeInterface)clazz.newInstance(); } catch (ClassNotFoundException e) { log.error("Invalid class name: " + className + ", " + e.toString()); throw e; } catch (InstantiationException e) { throw new ClassNotFoundException("Cannot create class: " + className, e); } catch (IllegalAccessException e) { throw new ClassNotFoundException("Cannot create class: " + className, e); } catch (ClassCastException e) { throw new ClassNotFoundException(className + " does not implement " + SomeInterface.class.getName(), e); } } 

In some cases, the code may be able to recover from certain error conditions. In these cases, catching specific exceptions is important so the code can figure out whether a condition is recoverable. Look at the class instantiation example in Listing 6 with this in mind.

In Listing 7, the code returns a default object for an invalid className, but throws an exception for illegal operations, like an invalid cast or a security violation.

Note:IllegalClassException is a domain exception class mentioned here for demonstration purposes.

Listing 7

public SomeInterface buildInstance(String className) throws IllegalClassException { SomeInterface impl = null; try { Class clazz = Class.forName(className); return (SomeInterface)clazz.newInstance(); } catch (ClassNotFoundException e) { log.warn("Invalid class name: " + className + ", using default"); } catch (InstantiationException e) { log.warn("Invalid class name: " + className + ", using default"); } catch (IllegalAccessException e) { throw new IllegalClassException("Cannot create class: " + className, e); } catch (ClassCastException e) { throw new IllegalClassException(className + " does not implement " + SomeInterface.class.getName(), e); } if (impl == null) { impl = new DefaultImplemantation(); } return impl; } 

When generic Exceptions should be caught

Certain cases justify when it is handy, and required, to catch generic Exceptions. These cases are very specific, but important to large, failure-tolerant systems. In Listing 8, requests are read from a queue of requests and processed in order. But if any exceptions occur while the request is being processed (either a BadRequestException or any subclass of RuntimeException, including NullPointerException), then that exception will be caught outside the processing while loop. So any error causes the processing loop to stop, and any remaining requests will not be processed. That represents a poor way of handling an error during request processing:

Listing 8

public void processAllRequests () {Request req = null; coba {while (true) {req = getNextRequest (); if (req! = null) {processRequest (req); // melempar BadRequestException} else {// Antrian permintaan kosong, harus dilakukan break; }}} catch (BadRequestException e) {log.error ("Permintaan tidak valid:" + req, e); }}