Programmering

Pas på farerne ved generiske undtagelser

Mens jeg arbejdede på et nyligt projekt, fandt jeg et stykke kode, der udførte ressourceoprydning. Fordi det havde mange forskellige opkald, kunne det potentielt kaste seks forskellige undtagelser. Den oprindelige programmør, i et forsøg på at forenkle koden (eller bare gemme at skrive), erklærede, at metoden kaster Undtagelse snarere end de seks forskellige undtagelser, der kunne kastes. Dette tvang opkaldskoden til at blive pakket ind i en prøve / fangst-blok, der fangede Undtagelse. Programmøren besluttede, at fordi koden var til oprydningsformål, var fejlsagerne ikke vigtige, så fangstblokken forblev tom, da systemet alligevel lukkede ned.

Det er åbenbart, at dette ikke er den bedste programmeringspraksis, men intet ser ud til at være frygteligt forkert ... bortset fra et lille logikproblem i den oprindelige kodes tredje linje:

Notering 1. Oprindelig oprydningskode

privat tomrumsoprydningConnections () kaster ExceptionOne, ExceptionTwo {for (int i = 0; i <forbindelser.længde; i ++) {forbindelse [i]. frigiv (); // kaster ExceptionOne, ExceptionTwo forbindelse [i] = null; } forbindelser = null; } beskyttet abstrakt ugyldigt cleanupFiles () kaster ExceptionThree, ExceptionFour; beskyttet abstrakt ugyldigt removeListeners () kaster ExceptionFive, ExceptionSix; offentlig tomrumsoprydningEverything () kaster undtagelse {cleanupConnections (); cleanupFiles (); removeListeners (); } offentligt ugyldigt gjort () {prøv {doStuff (); cleanupEverything (); doMoreStuff (); } fangst (undtagelse e) {}} 

I en anden del af koden er forbindelser array initialiseres ikke, før den første forbindelse oprettes. Men hvis der aldrig oprettes en forbindelse, er forbindelsesarrayet nul. Så i nogle tilfælde kaldes til forbindelser [i] .release () resulterer i en NullPointerException. Dette er et relativt let problem at rette. Du skal blot tilføje en check for forbindelser! = null.

Undtagelsen rapporteres dog aldrig. Det kastes af cleanupConnections (), kastet igen af oprydningAlt ()og endelig fanget i Færdig(). Det Færdig() metode gør ikke noget med undtagelsen, det logger det ikke engang. Og fordi oprydningAlt () kaldes kun igennem Færdig()undtagelsen ses aldrig. Så koden bliver aldrig løst.

I fiaskoscenariet er således cleanupFiles () og removeListeners () metoder kaldes aldrig (så deres ressourcer frigives aldrig), og doMoreStuff () kaldes aldrig, således den endelige behandling i Færdig() fuldender aldrig. At gøre tingene værre, Færdig() kaldes ikke, når systemet lukker ned; i stedet kaldes det til at gennemføre hver transaktion. Så ressourcer lækker i hver transaktion.

Dette problem er helt klart et stort problem: fejl rapporteres ikke, og ressourcerne lækker. Men selve koden virker ret uskyldig, og fra den måde, koden blev skrevet på, viser det sig, at dette problem er vanskeligt at spore. Men ved at anvende et par enkle retningslinjer kan problemet findes og løses:

  • Ignorer ikke undtagelser
  • Fang ikke generisk Undtagelses
  • Kast ikke generisk Undtagelses

Ignorer ikke undtagelser

Det mest oplagte problem med Listing 1's kode er, at en fejl i programmet ignoreres fuldstændigt. En uventet undtagelse (undtagelser er i sagens natur uventede) kastes, og koden er ikke parat til at håndtere denne undtagelse. Undtagelsen rapporteres ikke engang, fordi koden antager, at de forventede undtagelser ikke har nogen konsekvenser.

I de fleste tilfælde skal en undtagelse i det mindste være logget. Flere logningspakker (se sidebjælken "Logningsundtagelser") kan logge systemfejl og undtagelser uden at påvirke systemets ydeevne væsentligt. De fleste logningssystemer tillader også udskrivning af stakspor, hvilket giver værdifuld information om, hvor og hvorfor undtagelsen opstod. Endelig, fordi logfilerne typisk skrives til filer, kan en oversigt over undtagelser gennemgås og analyseres. Se Liste 11 i sidepanelet for et eksempel på logning af stakspor.

Det er ikke afgørende at logge undtagelser i nogle få specifikke situationer. En af disse er rengøringsressourcer i en endelig klausul.

Undtagelser i endelig

I liste 2 læses nogle data fra en fil. Filen skal lukke, uanset om en undtagelse læser dataene, så tæt() metode er pakket i en endelig klausul. Men hvis en fejl lukker filen, kan der ikke gøres meget ved det:

Liste 2

public void loadFile (String fileName) kaster IOException {InputStream in = null; prøv {in = ny FileInputStream (filnavn); readSomeData (in); } endelig {if (in! = null) {prøv {in.close (); } fange (IOException ioe) {// ignoreret}}}} 

Noter det loadFile () rapporterer stadig en IOUndtagelse til opkaldsmetoden, hvis den faktiske dataindlæsning mislykkes på grund af et I / O (input / output) problem. Bemærk også, at selvom en undtagelse fra tæt() ignoreres, angiver koden det eksplicit i en kommentar for at gøre det klart for alle, der arbejder på koden. Du kan anvende den samme procedure til at rydde op i alle I / O-streams, lukning af stikkontakter og JDBC-forbindelser osv.

Det vigtige ved at ignorere undtagelser er at sikre, at kun en enkelt metode pakkes ind i ignorering af forsøg / fangst-blokken (så andre metoder i den vedlagte blok stadig kaldes), og at en bestemt undtagelse fanges. Denne særlige omstændighed adskiller sig tydeligt fra at fange en generik Undtagelse. I alle andre tilfælde skal undtagelsen være logget (i det mindste), helst med et stakspor.

Fang ikke generiske undtagelser

Ofte i kompleks software udfører en given blok kode metoder, der kaster en række undtagelser. Dynamisk indlæsning af en klasse og instantering af et objekt kan give flere forskellige undtagelser, herunder ClassNotFoundException, InstantiationException, Ulovlig adgangsundtagelseog ClassCastException.

I stedet for at tilføje de fire forskellige fangstblokke til prøveblokken, kan en travl programmør simpelthen indpakke metodekaldene i en prøve / fangstblok, der fanger generisk Undtagelses (se liste 3 nedenfor). Selvom dette virker harmløst, kan der forekomme utilsigtede bivirkninger. For eksempel hvis className () er nul, Class.forName () vil kaste en NullPointerException, som bliver fanget i metoden.

I så fald fanger fangstblokken undtagelser, som den aldrig havde til hensigt at fange, fordi a NullPointerException er en underklasse af RuntimeException, som igen er en underklasse af Undtagelse. Så det generiske fangst (undtagelse e) fanger alle underklasser af RuntimeException, inklusive NullPointerException, IndexOutOfBoundsExceptionog ArrayStoreException. En programmør har typisk ikke til hensigt at fange disse undtagelser.

I Listing 3 er null className resulterer i en NullPointerException, som angiver over for opkaldsmetoden, at klassens navn er ugyldigt:

Liste 3

offentlig SomeInterface buildInstance (String className) {SomeInterface impl = null; prøv {Class clazz = Class.forName (className); impl = (SomeInterface) clazz.newInstance (); } fange (Undtagelse e) {log.error ("Fejl ved oprettelse af klasse:" + className); } returnere impl; } 

En anden konsekvens af den generiske fangstklausul er, at logning er begrænset, fordi fangst kender ikke den specifikke undtagelse, der fanges. Nogle programmerere, når de står over for dette problem, ty til at tilføje en afkrydsning for at se undtagelsestypen (se Liste 4), som modsiger formålet med at bruge fangstblokke:

Liste 4

catch (Exception e) {if (e instanceof ClassNotFoundException) {log.error ("Ugyldigt klassenavn:" + className + "," + e.toString ()); } andet {log.error ("Kan ikke oprette klasse:" + className + "," + e.toString ()); }} 

Liste 5 giver et komplet eksempel på at fange specifikke undtagelser, som en programmør måske er interesseret i forekomst af operatør er ikke påkrævet, fordi de specifikke undtagelser er fanget. Hver af de afkrydsede undtagelser (ClassNotFoundException, InstantiationException, Ulovlig adgangsundtagelse) bliver fanget og behandlet. Den særlige sag, der ville producere en ClassCastException (klassen indlæses korrekt, men implementerer ikke SomeInterface interface) verificeres også ved at kontrollere denne undtagelse:

Liste 5

offentlig SomeInterface buildInstance (String className) {SomeInterface impl = null; prøv {Class clazz = Class.forName (className); impl = (SomeInterface) clazz.newInstance (); } fange (ClassNotFoundException e) {log.error ("Ugyldigt klassenavn:" + className + "," + e.toString ()); } fange (InstantiationException e) {log.error ("Kan ikke oprette klasse:" + className + "," + e.toString ()); } fange (IllegalAccessException e) {log.error ("Kan ikke oprette klasse:" + className + "," + e.toString ()); } catch (ClassCastException e) {log.error ("Ugyldig klassetype," + className + "implementerer ikke" + SomeInterface.class.getName ()); } returnere impl; } 

I nogle tilfælde er det at foretrække at omlægge en kendt undtagelse (eller måske oprette en ny undtagelse) end at prøve at håndtere den i metoden. Dette gør det muligt for opkaldsmetoden at håndtere fejltilstanden ved at sætte undtagelsen i en kendt sammenhæng.

Liste 6 nedenfor giver en alternativ version af buildInterface () metode, som kaster en ClassNotFoundException hvis der opstår et problem under indlæsning og start af klassen. I dette eksempel er opkaldsmetoden sikret at modtage enten et korrekt instantieret objekt eller en undtagelse. Således behøver opkaldsmetoden ikke kontrollere, om det returnerede objekt er nul.

Bemærk, at dette eksempel bruger Java 1.4-metoden til at oprette en ny undtagelse, der er viklet rundt om en anden undtagelse for at bevare den originale stack-sporingsinformation. Ellers angiver stakksporingen metoden buildInstance () som metoden, hvor undtagelsen stammer fra, i stedet for den underliggende undtagelse kastet af newInstance ():

Liste 6

offentlig SomeInterface buildInstance (String className) kaster ClassNotFoundException {prøv {Class clazz = Class.forName (className); returnere (SomeInterface) clazz.newInstance (); } fange (ClassNotFoundException e) {log.error ("Ugyldigt klassenavn:" + className + "," + e.toString ()); smide e; } catch (InstantiationException e) {throw new ClassNotFoundException ("Kan ikke oprette klasse:" + className, e); } catch (IllegalAccessException e) {throw new ClassNotFoundException ("Kan ikke oprette klasse:" + className, e); } catch (ClassCastException e) {throw new ClassNotFoundException (className + "implementerer ikke" + SomeInterface.class.getName (), e); }} 

I nogle tilfælde kan koden muligvis gendanne sig efter visse fejltilstande. I disse tilfælde er det vigtigt at fange specifikke undtagelser, så koden kan finde ud af, om en tilstand kan genoprettes. Se på eksemplet med klasse-instantiering i Listing 6 med dette i tankerne.

I liste 7 returnerer koden et standardobjekt for et ugyldigt className, men kaster en undtagelse for ulovlige handlinger, som f.eks. et ugyldigt rollebesætning eller en sikkerhedsbrud

Bemærk:IllegalClassException er en domæneundtagelsesklasse, der er nævnt her til demonstrationsformål.

Liste 7

offentlig SomeInterface buildInstance (String className) kaster IllegalClassException {SomeInterface impl = null; prøv {Class clazz = Class.forName (className); returnere (SomeInterface) clazz.newInstance (); } catch (ClassNotFoundException e) {log.warn ("Ugyldigt klassenavn:" + className + ", ved hjælp af standard"); } catch (InstantiationException e) {log.warn ("Ugyldigt klassenavn:" + className + ", ved hjælp af standard"); } catch (IllegalAccessException e) {throw new IllegalClassException ("Kan ikke oprette klasse:" + className, e); } catch (ClassCastException e) {throw new IllegalClassException (className + "implementerer ikke" + SomeInterface.class.getName (), e); } hvis (impl == null) {impl = ny DefaultImplemantation (); } returnere impl; } 

Hvornår generiske undtagelser skal fanges

Visse tilfælde retfærdiggør, når det er praktisk og påkrævet at fange generisk Undtagelses. Disse sager er meget specifikke, men vigtige for store, fejltolerante systemer. I liste 8 læses anmodninger fra en kø med anmodninger og behandles i rækkefølge. Men hvis der opstår undtagelser, mens anmodningen behandles (enten a BadRequestException eller nogen underklasse af RuntimeException, inklusive NullPointerException), så vil denne undtagelse blive fanget uden for behandlingen under loop. Så enhver fejl får behandlingssløjfen til at stoppe og eventuelle resterende anmodninger vil ikke behandles. Det repræsenterer en dårlig måde at håndtere en fejl under behandling af anmodning på:

Liste 8

public void processAllRequests () {Request req = null; prøv {while (true) {req = getNextRequest (); hvis (req! = null) {processRequest (req); // kaster BadRequestException} ellers {// Anmodningskøen er tom, skal gøres pause; }}} fangst (BadRequestException e) {log.error ("Ugyldig anmodning:" + req, e); }}