Programování

Dejte si pozor na nebezpečí generických výjimek

Při práci na nedávném projektu jsem našel kus kódu, který provedl vyčištění prostředků. Protože měl mnoho různorodých hovorů, mohl potenciálně vyvolat šest různých výjimek. Původní programátor ve snaze zjednodušit kód (nebo jen uložit psaní) prohlásil, že metoda vyvolá Výjimka spíše než šest různých výjimek, které by mohly být vyvolány. To přinutilo volající kód zabalit do bloku try / catch, který se zachytil Výjimka. Programátor se rozhodl, že protože kód byl pro účely vyčištění, případy selhání nebyly důležité, takže blok zachycení zůstal prázdný, protože se systém stejně vypnul.

Je zřejmé, že nejde o nejlepší programovací postupy, ale nic se nezdá být strašně špatné ... až na malý logický problém ve třetím řádku původního kódu:

Výpis 1. Původní čisticí kód

private void cleanupConnections () hodí ExceptionOne, ExceptionTwo {for (int i = 0; i <connections.length; i ++) {connection [i] .release (); // Vyhodí ExceptionOne, ExceptionTwo připojení [i] = null; } připojení = null; } protected abstract void cleanupFiles () hází ExceptionThree, ExceptionFour; protected abstract void removeListeners () hází ExceptionFive, ExceptionSix; public void cleanupEverything () vyvolá výjimku {cleanupConnections (); cleanupFiles (); removeListeners (); } public void done () {try {doStuff (); cleanupEverything (); doMoreStuff (); } úlovek (výjimka e) {}} 

V další části kódu je připojení pole není inicializováno, dokud není vytvořeno první připojení. Pokud se ale připojení nikdy nevytvoří, je pole připojení null. V některých případech tedy volání na připojení [i] .release () vede k a NullPointerException. Toto je relativně snadný problém opravit. Jednoduše přidejte šek připojení! = null.

Výjimka se však nikdy nehlásí. Je to hozeno cleanupConnections (), opět hozen vyčištěníVše ()a nakonec se chytil Hotovo(). The Hotovo() metoda s výjimkou nic nedělá, ani to nezaznamenává. A protože vyčištěníVše () je povolán pouze prostřednictvím Hotovo(), výjimka není nikdy vidět. Takže kód se nikdy neopraví.

Ve scénáři selhání tedy cleanupFiles () a removeListeners () metody se nikdy nevolají (aby se jejich prostředky nikdy neuvolnily) a doMoreStuff () se nikdy nevolá, tedy konečné zpracování v Hotovo() nikdy nedokončí. Aby toho nebylo málo, Hotovo() není zavolán, když se systém vypne; místo toho se volá dokončit každou transakci. V každé transakci tedy dochází k úniku zdrojů.

Tento problém je jasně hlavní: chyby nejsou hlášeny a dochází k úniku zdrojů. Samotný kód se však zdá být docela nevinný a ze způsobu, jakým byl kód napsán, se ukázalo, že je tento problém obtížně dohledatelný. Použitím několika jednoduchých pokynů však lze problém najít a opravit:

  • Neignorujte výjimky
  • Nechytejte obecné Výjimkas
  • Nehazujte obecné Výjimkas

Neignorujte výjimky

Nejviditelnějším problémem kódu Výpisu 1 je, že chyba v programu je zcela ignorována. Je vyvolána neočekávaná výjimka (výjimky jsou ze své podstaty neočekávané) a kód není připraven se s touto výjimkou vypořádat. Výjimka není ani hlášena, protože kód předpokládá, že očekávané výjimky nebudou mít žádné důsledky.

Ve většině případů by výjimka měla být přinejmenším zaznamenána. Několik balíčků protokolování (viz postranní panel „Výjimky protokolování“) může zaznamenávat systémové chyby a výjimky, aniž by to významně ovlivnilo výkon systému. Většina systémů protokolování také umožňuje tisk trasování zásobníku, čímž poskytuje cenné informace o tom, kde a proč došlo k výjimce. Nakonec, protože protokoly se obvykle zapisují do souborů, lze zkontrolovat a analyzovat záznam výjimek. Příklad výpisu trasování zásobníku viz Výpis 11 v postranním panelu.

Výjimky z protokolování nejsou v několika konkrétních situacích kritické. Jedním z nich je čištění zdrojů v klauzuli nakonec.

Konečně výjimky

V seznamu 2 jsou některá data čtena ze souboru. Soubor je třeba zavřít bez ohledu na to, zda výjimka čte data, takže zavřít() metoda je zabalena do klauzule finally. Pokud však soubor zavře chyba, nelze s ní dělat mnoho:

Výpis 2

public void loadFile (String fileName) vyvolá IOException {InputStream in = null; zkuste {in = new FileInputStream (fileName); readSomeData (in); } konečně {if (in! = null) {try {in.close (); } catch (IOException ioe) {// Ignored}}}} 

Všimněte si, že loadFile () stále hlásí IOException na volací metodu, pokud skutečné načtení dat selže kvůli problému I / O (vstup / výstup). Všimněte si také, že i když výjimka z zavřít() je ignorováno, kód uvádí, že výslovně v komentáři, aby bylo jasné, komukoli, kdo pracuje na kódu. Stejný postup můžete použít k vyčištění všech I / O streamů, uzavření soketů a připojení JDBC atd.

Důležitou věcí při ignorování výjimek je zajištění toho, že do bloku ignorování try / catch bude zabalena pouze jedna metoda (takže se stále volají jiné metody v uzavírajícím bloku) a že se zachytí konkrétní výjimka. Tato zvláštní okolnost se výrazně liší od chytání generika Výjimka. Ve všech ostatních případech by výjimka měla být (přinejmenším) protokolována, nejlépe pomocí trasování zásobníku.

Nezachyťte obecné výjimky

V komplexním softwaru často daný blok kódu provádí metody, které vyvolávají různé výjimky. Dynamické načítání třídy a vytváření instancí objektu může vyvolat několik různých výjimek, včetně ClassNotFoundException, InstantiationException, IllegalAccessException, a ClassCastException.

Namísto přidání čtyř různých bloků catch do bloku try může zaneprázdněný programátor jednoduše zabalit volání metody do bloku try / catch, který zachytí obecné Výjimkas (viz Výpis 3 níže). I když se to zdá být neškodné, mohou nastat nežádoucí nežádoucí účinky. Například pokud jméno třídy() je null, Class.forName () hodí NullPointerException, který bude chycen v metodě.

V takovém případě blok catch zachytí výjimky, které nikdy neměl v úmyslu chytit, protože a NullPointerException je podtřída RuntimeException, což je zase podtřída Výjimka. Takže obecný úlovek (výjimka e) chytí všechny podtřídy RuntimeException, počítaje v to NullPointerException, IndexOutOfBoundsException, a ArrayStoreException. Programátor obvykle nemá v úmyslu tyto výjimky zachytit.

V seznamu 3 je null className vede k a NullPointerException, což označuje volající metodě, že název třídy je neplatný:

Výpis 3

public SomeInterface buildInstance (String className) {SomeInterface impl = null; try {Class clazz = Class.forName (className); impl = (SomeInterface) clazz.newInstance (); } catch (Výjimka e) {log.error ("Chyba při vytváření třídy:" + className); } návrat impl; } 

Dalším důsledkem obecné klauzule o úlovku je, že protokolování je omezeno, protože chytit nezná zachycenou konkrétní výjimku. Někteří programátoři, když čelí tomuto problému, se uchylují k přidání kontroly, aby viděli typ výjimky (viz Výpis 4), což je v rozporu s účelem použití bloků úlovku:

Výpis 4

catch (Výjimka e) {if (e instanceof ClassNotFoundException) {log.error ("Neplatný název třídy:" + className + "," + e.toString ()); } else {log.error ("Nelze vytvořit třídu:" + className + "," + e.toString ()); }} 

Výpis 5 poskytuje kompletní příklad zachycení konkrétních výjimek, které by programátora mohly zajímat instanceof operátor není vyžadován, protože jsou zachyceny konkrétní výjimky. Každá z kontrolovaných výjimek (ClassNotFoundException, InstantiationException, IllegalAccessException) je chycen a řešen. Zvláštní případ, který by vytvořil a ClassCastException (třída se načte správně, ale neimplementuje Některé rozhraní rozhraní) se také ověří kontrolou této výjimky:

Výpis 5

public SomeInterface buildInstance (String className) {SomeInterface impl = null; try {Class clazz = Class.forName (className); impl = (SomeInterface) clazz.newInstance (); } catch (ClassNotFoundException e) {log.error ("Neplatný název třídy:" + className + "," + e.toString ()); } catch (InstantiationException e) {log.error ("Cannot create class:" + className + "," + e.toString ()); } catch (IllegalAccessException e) {log.error ("Nelze vytvořit třídu:" + className + "," + e.toString ()); } catch (ClassCastException e) {log.error ("Neplatný typ třídy," + className + "neimplementuje" + SomeInterface.class.getName ()); } návrat impl; } 

V některých případech je vhodnější znovu vytvořit známou výjimku (nebo případně vytvořit novou výjimku), než se s ní pokusit vypořádat v metodě. To umožňuje volající metodě zpracovat chybový stav uvedením výjimky do známého kontextu.

Výpis 6 níže poskytuje alternativní verzi buildInterface () metoda, která vyvolá a ClassNotFoundException pokud dojde k problému při načítání a instanci třídy. V tomto příkladu je metoda volání zajištěna pro příjem správně vytvořeného objektu nebo výjimky. Metoda volání tedy nemusí kontrolovat, zda je vrácený objekt null.

Všimněte si, že tento příklad používá metodu Java 1.4 k vytvoření nové výjimky zabalené kolem jiné výjimky, aby se zachovaly původní informace o trasování zásobníku. Jinak by trasování zásobníku označilo metodu buildInstance () jako metoda, kde výjimka vznikla, místo základní výjimky vyvolané newInstance ():

Výpis 6

public SomeInterface buildInstance (String className) hodí ClassNotFoundException {try {Class clazz = Class.forName (className); návrat (SomeInterface) clazz.newInstance (); } catch (ClassNotFoundException e) {log.error ("Neplatný název třídy:" + className + "," + e.toString ()); hod 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) {hodit novou ClassNotFoundException (className + "neimplementuje" + SomeInterface.class.getName (), e); }} 

V některých případech může být kód schopen se zotavit z určitých chybových podmínek. V těchto případech je důležité zachytit konkrétní výjimky, aby kód mohl zjistit, zda je podmínka obnovitelná. Podívejte se na příklad instance třídy ve Výpisu 6 s ohledem na tuto skutečnost.

V seznamu 7 vrátí kód výchozí objekt pro neplatný jméno třídy, ale vyvolá výjimku pro nelegální operace, jako je neplatné obsazení nebo narušení zabezpečení.

Poznámka:IllegalClassException je třída výjimek domény uvedená zde pro demonstrační účely.

Výpis 7

public SomeInterface buildInstance (String className) hodí IllegalClassException {SomeInterface impl = null; try {Class clazz = Class.forName (className); návrat (SomeInterface) clazz.newInstance (); } catch (ClassNotFoundException e) {log.warn ("Neplatný název třídy:" + className + ", výchozí"); } catch (InstantiationException e) {log.warn ("Neplatný název třídy:" + className + ", výchozí)"; } catch (IllegalAccessException e) {hodit novou IllegalClassException ("Nelze vytvořit třídu:" + className, e); } catch (ClassCastException e) {throw new IllegalClassException (className + "does not implement" + SomeInterface.class.getName (), e); } if (impl == null) {impl = new DefaultImplemantation (); } návrat impl; } 

Kdy by měly být zachyceny obecné výjimky

Určité případy ospravedlňují, kdy je užitečné a nutné chytit obecné informace Výjimkas. Tyto případy jsou velmi specifické, ale důležité pro velké systémy odolné vůči poruchám. V seznamu 8 jsou požadavky čteny z fronty požadavků a zpracovávány v pořadí. Pokud se ale během zpracování požadavku vyskytnou nějaké výjimky (buď a BadRequestException nebo žádný podtřída RuntimeException, počítaje v to NullPointerException), pak bude tato výjimka chycena mimo smyčka zpracování while. Jakákoli chyba způsobí zastavení zpracovávací smyčky a všechny zbývající požadavky nebude být zpracovány. To představuje špatný způsob zpracování chyby během zpracování požadavku:

Výpis 8

public void processAllRequests () {Request req = null; try {while (true) {req = getNextRequest (); if (req! = null) {processRequest (req); // hodí BadRequestException} else {// Fronta požadavků je prázdná, musí být provedena break; }}} catch (BadRequestException e) {log.error ("Neplatný požadavek:" + req, e); }}