Programovanie

Dávajte pozor na nebezpečenstvo všeobecných výnimiek

Pri práci na nedávnom projekte som našiel kúsok kódu, ktorý vykonal čistenie prostriedkov. Pretože mal veľa rôznorodých hovorov, mohol potenciálne vyvolať šesť rôznych výnimiek. Pôvodný programátor v snahe zjednodušiť kód (alebo iba uložiť zadanie) vyhlásil, že metóda hodí Výnimka skôr ako šesť rôznych výnimiek, ktoré by sa mohli hodiť. To prinútilo volací kód zabaliť do bloku try / catch, ktorý sa zachytil Výnimka. Programátor sa rozhodol, že pretože kód slúžil na vyčistenie, prípady zlyhania neboli dôležité, takže blokovanie chyby zostalo po vypnutí systému rovnako prázdne.

Je zrejmé, že nejde o najlepšie programovacie postupy, ale zdá sa, že nie je nič strašne zlé ... až na malý logický problém v treťom riadku pôvodného kódu:

Zoznam 1. Originálny čistiaci kód

private void cleanupConnections () hodí ExceptionOne, ExceptionTwo {for (int i = 0; i <connections.length; i ++) {connection [i] .release (); // Vyhodí ExceptionOne, ExceptionTwo pripojenie [i] = null; } spojenia = null; } protected abstract void cleanupFiles () hodí ExceptionThree, ExceptionFour; chránená abstraktná neplatnosť removeListeners () hodí ExceptionFive, ExceptionSix; public void cleanupEverything () vyvolá výnimku {cleanupConnections (); cleanupFiles (); removeListeners (); } public void done () {try {doStuff (); vyčistenieVšetko (); doMoreStuff (); } úlovok (Výnimka e) {}} 

V inej časti kódu je spojenia pole sa inicializuje až po vytvorení prvého spojenia. Ale ak sa spojenie nikdy nevytvorí, potom je pole connections null. V niektorých prípadoch teda hovor na pripojenia [i] .vydanie () má za následok a NullPointerException. Toto je pomerne ľahký problém na odstránenie. Stačí pridať šek pripojenia! = null.

Výnimka sa však nikdy nehlási. Vhadzuje sa to cleanupConnections (), znovu vyhodený upratovanieVšetko (), a nakoniec sa chytil hotový(). The hotový() metóda nerobí nič s výnimkou, ani to neprihlási. A preto upratovanieVšetko () sa volá iba prostredníctvom hotový(), výnimka sa nikdy nevidí. Takže kód sa nikdy neopraví.

V scenári zlyhania teda: cleanupFiles () a removeListeners () metódy sa nikdy nevolajú (aby sa ich zdroje nikdy neuvoľnili) a doMoreStuff () sa nikdy nevolá, teda konečné spracovanie v hotový() nikdy nedokončí. Aby toho nebolo málo, hotový() nie je volané, keď sa systém vypne; namiesto toho sa volá dokončenie každej transakcie. Takže pri každej transakcii unikajú zdroje.

Tento problém je jednoznačne závažný: chyby sa nehlásia a dochádza k úniku zdrojov. Samotný kód sa však zdá byť dosť nevinný a ukázalo sa, že od zistenia, ako bol tento kód napísaný, je ťažké ho vystopovať. Použitím niekoľkých jednoduchých pokynov však možno problém nájsť a opraviť:

  • Neignorujte výnimky
  • Nechytajte všeobecné Výnimkas
  • Nehádžte generické Výnimkas

Neignorujte výnimky

Najviditeľnejším problémom kódu výpisu 1 je to, že chyba v programe je úplne ignorovaná. Vyvoláva sa neočakávaná výnimka (výnimky sú svojou povahou neočakávané) a kód nie je pripravený na riešenie tejto výnimky. Výnimka sa ani nehlási, pretože kód predpokladá, že očakávané výnimky nebudú mať žiadne následky.

Vo väčšine prípadov by sa mala zaznamenať výnimka. Niekoľko protokolovacích balíkov (pozri bočný panel „Výnimky z protokolovania“) dokáže protokolovať systémové chyby a výnimky bez výrazného ovplyvnenia výkonu systému. Väčšina protokolovacích systémov umožňuje tlačiť aj stopy zásobníka, čím poskytujú cenné informácie o tom, kde a prečo došlo k výnimke. Nakoniec, pretože protokoly sa zvyčajne zapisujú do súborov, je možné skontrolovať a analyzovať záznam výnimiek. Príklad záznamu protokolovania zásobníka nájdete v zozname 11 na bočnom paneli.

Výnimky z protokolovania nie sú v niektorých konkrétnych situáciách rozhodujúce. Jedným z nich je čistenie zdrojov v konečnej klauzule.

Výnimky konečne

V zozname 2 sú niektoré údaje načítané zo súboru. Súbor sa musí uzavrieť bez ohľadu na to, či výnimka číta údaje, takže Zavrieť() metóda je zabalená v záverečnej klauzule. Ak však súbor zavrie chyba, nedá sa s tým urobiť veľa:

Zoznam 2

public void loadFile (String fileName) hodí IOException {InputStream in = null; skúste {in = new FileInputStream (názov súboru); readSomeData (in); } nakoniec {if (in! = null) {try {in.close (); } catch (IOException ioe) {// Ignorované}}}} 

Poznač si to loadFile () stále hlási Výnimka IO na metódu volania, ak skutočné načítanie údajov zlyhá z dôvodu problému I / O (vstup / výstup). Upozorňujeme tiež, že aj keď výnimka z Zavrieť() je ignorovaný, kód uvádza, že výslovne v komentári, aby bolo jasné pre každého, kto pracuje na kóde. Rovnaký postup môžete použiť na vyčistenie všetkých vstupno-výstupných prúdov, zatváranie zásuviek a pripojení JDBC atď.

Dôležité pri ignorovaní výnimiek je zabezpečenie toho, že do bloku ignorovania try / catch bude zabalená iba jedna metóda (takže sa stále volajú iné metódy v priloženom bloku) a že sa zachytí konkrétna výnimka. Táto zvláštna okolnosť sa zreteľne líši od chytenia generika Výnimka. Vo všetkých ostatných prípadoch by mala byť výnimka (prinajmenšom) prihlásená, najlepšie so sledovaním zásobníka.

Nezachyťte všeobecné výnimky

V zložitom softvéri daný blok kódu často vykonáva metódy, ktoré spôsobujú rôzne výnimky. Dynamické načítanie triedy a vytvorenie inštancie objektu môže spôsobiť niekoľko rôznych výnimiek, vrátane ClassNotFoundException, InstantiationException, IllegalAccessExceptiona ClassCastException.

Namiesto pridania štyroch rôznych blokov catch do bloku try môže zaneprázdnený programátor jednoducho zabaliť volania metód do bloku try / catch, ktorý zachytáva všeobecné Výnimkas (pozri zoznam 3 nižšie). Aj keď sa to zdá byť neškodné, mohli by sa vyskytnúť nežiaduce vedľajšie účinky. Napríklad ak className () je neplatné, Class.forName () bude hádzať a NullPointerException, ktoré sa pri metóde zachytia.

V takom prípade blok chytí výnimky, ktoré nikdy nechcel chytiť, pretože a NullPointerException je podtrieda RuntimeException, čo je zase podtrieda Výnimka. Takže všeobecné úlovok (výnimka e) zachytáva všetky podtriedy RuntimeException, počítajúc do toho NullPointerException, IndexOutOfBoundsExceptiona ArrayStoreException. Programátor zvyčajne nemá v úmysle chytiť tieto výnimky.

V zozname 3 je null className má za následok a NullPointerException, čo naznačuje volajúcej metóde, že názov triedy je neplatný:

Zoznam 3

public SomeInterface buildInstance (String className) {SomeInterface impl = null; try {Class clazz = Class.forName (className); impl = (SomeInterface) clazz.newInstance (); } catch (Výnimka e) {log.error ("Chyba pri vytváraní triedy:" + className); } návrat impl; } 

Ďalším dôsledkom generickej doložky o úlovku je to, že protokolovanie je obmedzené, pretože chytiť nevie zachytena konkretna vynimka. Niektorí programátori, keď sa stretávajú s týmto problémom, sa uchýlia k pridaniu kontroly, aby videli typ výnimky (pozri Zoznam 4), čo je v rozpore s účelom použitia blokov úlovku:

Výpis 4

catch (Výnimka e) {if (e instanceof ClassNotFoundException) {log.error ("Neplatný názov triedy:" + className + "," + e.toString ()); } else {log.error ("Nemôžem vytvoriť triedu:" + className + "," + e.toString ()); }} 

Zoznam 5 poskytuje kompletný príklad zachytávania konkrétnych výnimiek, ktoré by programátora mohli zaujímať inštancia operátor sa nevyžaduje, pretože sú zachytené konkrétne výnimky. Každá z kontrolovaných výnimiek (ClassNotFoundException, InstantiationException, IllegalAccessException) je chytený a riešený. Špeciálny prípad, ktorý by priniesol a ClassCastException (trieda sa načíta správne, ale neimplementuje Niektoré rozhranie rozhranie) sa tiež overuje kontrolou príslušnej výnimky:

Zoznam 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ázov triedy:" + className + "," + e.toString ()); } catch (InstantiationException e) {log.error ("Cannot create class:" + className + "," + e.toString ()); } catch (IllegalAccessException e) {log.error ("Nemôžem vytvoriť triedu:" + className + "," + e.toString ()); } catch (ClassCastException e) {log.error ("Neplatný typ triedy," + className + "neimplementuje" + SomeInterface.class.getName ()); } návrat impl; } 

V niektorých prípadoch je lepšie známu výnimku prerobiť (alebo možno vytvoriť novú výnimku), ako sa s ňou pokúsiť vysporiadať v metóde. To umožňuje volajúcej metóde spracovať chybový stav uvedením výnimky do známeho kontextu.

Zoznam 6 uvedený nižšie poskytuje alternatívnu verziu servera buildInterface () metóda, ktorá hodí a ClassNotFoundException ak nastane problém pri načítaní a inštancii triedy. V tomto príklade je zabezpečené, že metóda volania prijíma buď správne inštancovaný objekt, alebo výnimku. Metóda volania teda nemusí kontrolovať, či je vrátený objekt nulový.

Upozorňujeme, že tento príklad používa metódu Java 1.4 na vytvorenie novej výnimky zabalenej okolo inej výnimky, aby sa zachovali pôvodné informácie o sledovaní zásobníka. V opačnom prípade by sledovanie zásobníka označilo metódu buildInstance () ako metóda, pri ktorej výnimka vznikla, namiesto základnej výnimky vyvolanej pomocou newInstance ():

Výpis 6

public SomeInterface buildInstance (String className) throws ClassNotFoundException {try {Class clazz = Class.forName (className); návrat (SomeInterface) clazz.newInstance (); } catch (ClassNotFoundException e) {log.error ("Neplatný názov triedy:" + 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) {hod nový ClassNotFoundException (className + "neimplementuje" + SomeInterface.class.getName (), e); }} 

V niektorých prípadoch môže byť kód schopný zotaviť sa z určitých chybových stavov. V týchto prípadoch je dôležité chytiť konkrétne výnimky, aby kód mohol zistiť, či je stav obnoviteľný. Pozrite sa na tento príklad inštancie triedy v zozname 6.

V zozname 7 vráti kód predvolený objekt pre neplatné className, ale vyvoláva výnimku pre nelegálne operácie, napríklad neplatné obsadenie alebo porušenie bezpečnosti.

Poznámka:IllegalClassException je trieda výnimiek domény spomenutá tu na demonštračné účely.

Výpis 7

public SomeInterface buildInstance (String className) throws IllegalClassException {SomeInterface impl = null; try {Class clazz = Class.forName (className); návrat (SomeInterface) clazz.newInstance (); } catch (ClassNotFoundException e) {log.warn ("Neplatný názov triedy:" + className + ", predvolené nastavenie)); } catch (InstantiationException e) {log.warn ("Neplatný názov triedy:" + className + ", predvolené nastavenie)); } 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 (); } návrat impl; } 

Kedy by sa mali zachytiť všeobecné výnimky

Určité prípady odôvodňujú, keď je užitočné a potrebné chytiť všeobecné Výnimkas. Tieto prípady sú veľmi špecifické, ale dôležité pre veľké systémy odolné voči poruchám. V zozname 8 sa požiadavky čítajú z frontu požiadaviek a spracovávajú sa v poradí. Ak sa však počas spracovania žiadosti vyskytnú nejaké výnimky (buď a BadRequestException alebo akýkoľvek podtrieda RuntimeException, počítajúc do toho NullPointerException), potom sa táto výnimka zachytí vonku slučka while. Akákoľvek chyba teda spôsobí zastavenie spracovateľskej slučky a všetky zostávajúce požiadavky nebude byť spracované. To predstavuje zlý spôsob spracovania chyby počas spracovania žiadosti:

Výpis 8

public void processAllRequests () {Request req = null; try {while (true) {req = getNextRequest (); if (req! = null) {processRequest (req); // hodí BadRequestException} else {// Fronta požiadaviek je prázdna, treba urobiť break; }}} catch (BadRequestException e) {log.error ("Neplatná požiadavka:" + req, e); }} 
$config[zx-auto] not found$config[zx-overlay] not found