Programmering

Pass på farene ved generiske unntak

Mens jeg jobbet med et nylig prosjekt, fant jeg et stykke kode som utførte ressursopprydding. Fordi det hadde mange forskjellige samtaler, kan det potensielt kaste seks forskjellige unntak. Den opprinnelige programmereren, i et forsøk på å forenkle koden (eller bare lagre å skrive), erklærte at metoden kaster Unntak i stedet for de seks forskjellige unntakene som kan kastes. Dette tvang ringekoden til å bli pakket inn i en prøve / fangst-blokk som fanget Unntak. Programmereren bestemte at fordi koden var for opprydding, var feilsakene ikke viktige, så fangstblokken forble tom da systemet uansett slås av.

Dette er åpenbart ikke den beste programmeringspraksis, men ingenting ser ut til å være veldig galt ... bortsett fra et lite logikkproblem i den opprinnelige kodens tredje linje:

Oppføring 1. Opprinnelig oppryddingskode

privat tomrom oppryddingConnections () kaster ExceptionOne, ExceptionTwo {for (int i = 0; i <forbindelser.lengde; i ++) {tilkobling [i] .release (); // kaster ExceptionOne, ExceptionTwo forbindelse [i] = null; } forbindelser = null; } beskyttet abstrakt tomrom cleanupFiles () kaster ExceptionThree, ExceptionFour; beskyttet abstrakt ugyldig removeListeners () kaster ExceptionFive, ExceptionSix; public void cleanupEverything () kaster Unntak {cleanupConnections (); cleanupFiles (); removeListeners (); } offentlig ugyldig gjort () {prøv {doStuff (); cleanupEverything (); doMoreStuff (); } fangst (unntak e) {}} 

I en annen del av koden, er tilkoblinger array initialiseres ikke før den første tilkoblingen er opprettet. Men hvis en forbindelse aldri opprettes, er tilkoblingsmatrisen null. Så i noen tilfeller, kallet til tilkoblinger [i] .release () resulterer i en NullPointerException. Dette er et relativt enkelt problem å fikse. Bare legg til en sjekk for tilkoblinger! = null.

Unntaket rapporteres imidlertid aldri. Det blir kastet av cleanupConnections (), kastet igjen av oppryddingAlt (), og til slutt fanget inn ferdig (). De ferdig () metoden gjør ingenting med unntak, den logger den ikke engang. Og fordi oppryddingAlt () kalles bare gjennom ferdig (), unntaket blir aldri sett. Så koden blir aldri løst.

Dermed i svikt scenariet, cleanupFiles () og removeListeners () metoder kalles aldri (slik at ressursene deres aldri frigjøres), og doMoreStuff () blir aldri kalt, og dermed den endelige behandlingen i ferdig () fullfører aldri. For å gjøre vondt verre, ferdig () kalles ikke når systemet slås av; i stedet kalles det for å fullføre hver transaksjon. Så ressurser lekker i hver transaksjon.

Dette problemet er helt klart et stort problem: feil rapporteres ikke og ressurser lekker. Men selve koden virker ganske uskyldig, og fra måten koden ble skrevet på, viser dette seg at det er vanskelig å spore. Imidlertid, ved å bruke noen få enkle retningslinjer, kan problemet bli funnet og løst:

  • Ikke se bort fra unntak
  • Ikke ta generisk Unntaks
  • Ikke kast generisk Unntaks

Ikke se bort fra unntak

Det mest åpenbare problemet med Listing 1's kode er at en feil i programmet blir fullstendig ignorert. Et uventet unntak (unntak er i sin natur uventet) kastes, og koden er ikke forberedt på å håndtere det unntaket. Unntaket rapporteres ikke engang fordi koden antar at forventede unntak ikke vil ha noen konsekvenser.

I de fleste tilfeller skal et unntak i det minste logges. Flere loggpakker (se sidefeltet "Loggingsundtak") kan logge systemfeil og unntak uten å påvirke systemytelsen vesentlig. De fleste loggesystemer tillater også utskrift av stablespor, og gir dermed verdifull informasjon om hvor og hvorfor unntaket skjedde. Til slutt, fordi loggene vanligvis er skrevet til filer, kan en oversikt over unntak gjennomgås og analyseres. Se Oppføring 11 i sidefeltet for et eksempel på logging av stablespor.

Det er ikke viktig å logge unntak i noen få spesifikke situasjoner. En av disse er rengjøringsressurser i en endelig klausul.

Unntak i endelig

I liste 2 blir noen data lest fra en fil. Filen må lukkes uavhengig av om et unntak leser dataene, så Lukk() metoden er pakket inn i en endelig klausul. Men hvis en feil lukker filen, kan ikke mye gjøres med den:

Oppføring 2

public void loadFile (String fileName) kaster IOException {InputStream in = null; prøv {in = new FileInputStream (fileName); readSomeData (in); } til slutt {if (in! = null) {try {in.close (); } fangst (IOException ioe) {// ignorert}}}} 

Noter det loadFile () rapporterer fremdeles en IO Unntak til anropsmetoden hvis den faktiske datainnlasting mislykkes på grunn av et I / O-problem (input / output). Vær også oppmerksom på at selv om et unntak fra Lukk() blir ignorert, sier koden det eksplisitt i en kommentar for å gjøre det klart for alle som jobber med koden. Du kan bruke den samme prosedyren for å rydde opp i alle I / O-strømmer, lukke stikkontakter og JDBC-tilkoblinger, og så videre.

Det viktige med å ignorere unntak er å sikre at bare en enkelt metode er pakket i ignorering av prøve / fangst-blokken (slik at andre metoder i den vedlagte blokken fremdeles kalles) og at et spesifikt unntak blir fanget. Denne spesielle omstendigheten skiller seg tydelig fra å fange en generikk Unntak. I alle andre tilfeller skal unntaket være logget (i det minste), helst med stabelspor.

Ikke ta generiske unntak

Ofte i kompleks programvare utfører en gitt kodeblokk metoder som kaster en rekke unntak. Å laste en klasse dynamisk og sette i gang et objekt kan gi flere forskjellige unntak, inkludert ClassNotFoundException, InstantiationException, IllegalAccessException, og ClassCastException.

I stedet for å legge til de fire forskjellige fangstblokkene i prøveblokken, kan en opptatt programmerer ganske enkelt pakke inn metoden samtaler i en prøve / fangstblokk som fanger generisk Unntaks (se Listing 3 nedenfor). Selv om dette virker ufarlig, kan det føre til noen utilsiktede bivirkninger. For eksempel hvis klassenavn() er null, Class.forName () vil kaste en NullPointerException, som vil bli fanget i metoden.

I så fall fanger fangstblokken unntak den aldri hadde til hensikt å fange fordi a NullPointerException er en underklasse av RuntimeException, som igjen er en underklasse av Unntak. Så det generiske fangst (unntak e) fanger alle underklasser av RuntimeException, gjelder også NullPointerException, IndexOutOfBoundsException, og ArrayStoreException. Vanligvis har ikke en programmerer til hensikt å fange disse unntakene.

I Listing 3 er null className resulterer i en NullPointerException, som indikerer til anropsmetoden at klassenavnet er ugyldig:

Oppføring 3

offentlig SomeInterface buildInstance (String className) {SomeInterface impl = null; prøv {Class clazz = Class.forName (className); impl = (SomeInterface) clazz.newInstance (); } catch (Unntak e) {log.error ("Feil ved oppretting av klasse:" + className); } returner impl; } 

En annen konsekvens av den generiske fangstklausulen er at hogst er begrenset fordi å fange vet ikke det spesifikke unntaket som blir fanget. Noen programmerere, når de står overfor dette problemet, ty til å legge til en sjekk for å se unntakstypen (se Listing 4), som strider mot formålet med å bruke fangstblokker:

Oppføring 4

fange (Unntak e) {hvis (e forekomst av ClassNotFoundException) {log.error ("Ugyldig klassenavn:" + className + "," + e.toString ()); } annet {log.error ("Kan ikke opprette klasse:" + className + "," + e.toString ()); }} 

Oppføring 5 gir et komplett eksempel på å fange spesifikke unntak en programmerer kan være interessert i tilfelle av operatør er ikke nødvendig fordi de spesifikke unntakene er fanget. Hvert av de avmerkede unntakene (ClassNotFoundException, InstantiationException, IllegalAccessException) blir fanget og behandlet. Den spesielle saken som ville produsere en ClassCastException (klassen lastes ordentlig, men implementerer ikke Noen grensesnitt interface) er også bekreftet ved å se etter det unntaket:

Oppføring 5

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

I noen tilfeller er det å foretrekke å kaste et kjent unntak på nytt (eller kanskje opprette et nytt unntak) enn å prøve å håndtere det i metoden. Dette gjør at anropsmetoden kan håndtere feiltilstanden ved å sette unntaket i en kjent sammenheng.

Listing 6 nedenfor gir en alternativ versjon av buildInterface () metode, som kaster en ClassNotFoundException hvis det oppstår et problem mens du laster inn og starter klassen. I dette eksemplet er anropsmetoden sikret å motta enten et riktig instantiert objekt eller et unntak. Dermed trenger ikke anropsmetoden å sjekke om det returnerte objektet er null.

Merk at dette eksemplet bruker Java 1.4-metoden for å opprette et nytt unntak pakket rundt et annet unntak for å bevare den opprinnelige stabelsporinformasjonen. Ellers vil stakksporingen indikere metoden buildInstance () som metoden der unntaket oppsto, i stedet for det underliggende unntaket som ble kastet av newInstance ():

Oppføring 6

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

I noen tilfeller kan koden være i stand til å gjenopprette fra visse feilforhold. I disse tilfellene er det viktig å fange spesifikke unntak, slik at koden kan finne ut om en tilstand kan gjenopprettes. Se på eksempelet for klasseinstansiering i Listing 6 med dette i tankene.

I oppføring 7 returnerer koden et standardobjekt for en ugyldig klassenavn, men kaster et unntak for ulovlige operasjoner, som en ugyldig rollebesetning eller et sikkerhetsbrudd.

Merk:IllegalClassException er en domene unntaksklasse som er nevnt her for demonstrasjonsformål.

Oppføring 7

offentlig SomeInterface buildInstance (String className) kaster IllegalClassException {SomeInterface impl = null; prøv {Class clazz = Class.forName (className); return (SomeInterface) clazz.newInstance (); } catch (ClassNotFoundException e) {log.warn ("Ugyldig klassenavn:" + className + ", bruker standard"); } catch (InstantiationException e) {log.warn ("Ugyldig klassenavn:" + className + ", bruker standard"); } catch (IllegalAccessException e) {throw new IllegalClassException ("Kan ikke opprette klasse:" + className, e); } catch (ClassCastException e) {throw new IllegalClassException (className + "implementerer ikke" + SomeInterface.class.getName (), e); } if (impl == null) {impl = new DefaultImplemantation (); } returner impl; } 

Når generiske unntak skal fanges

Enkelte tilfeller rettferdiggjør når det er praktisk, og påkrevd, å fange generisk Unntaks. Disse sakene er veldig spesifikke, men viktige for store, sviktolerante systemer. I liste 8 blir forespørsler lest fra en forespørselskø og behandlet i rekkefølge. Men hvis det oppstår unntak mens forespørselen behandles (enten a BadRequestException eller noen underklasse av RuntimeException, gjelder også NullPointerException), så blir det unntaket fanget utenfor behandlingen mens loop. Så enhver feil får behandlingen til å stoppe og eventuelle gjenværende forespørsler vil ikke behandles. Det representerer en dårlig måte å håndtere en feil under behandlingen av forespørselen på:

Oppføring 8

public void processAllRequests () {Request req = null; prøv {while (true) {req = getNextRequest (); hvis (req! = null) {processRequest (req); // kaster BadRequestException} annet {// Forespørselskø er tom, må gjøres pause; }}} fangst (BadRequestException e) {log.error ("Ugyldig forespørsel:" + req, e); }} 
$config[zx-auto] not found$config[zx-overlay] not found