Пазете се от опасностите от общи изключения

Докато работех по скорошен проект, намерих парче код, което извършва почистване на ресурсите. Тъй като имаше много разнообразни обаждания, той може потенциално да създаде шест различни изключения. Оригиналният програмист, опитвайки се да опрости кода (или просто да запази въвеждането), декларира, че методът изхвърля Exceptionвместо шестте различни изключения, които могат да бъдат хвърлени. Това принуди кода за повикване да бъде обвит в блок за опит / улов, който се хвана Exception. Програмистът реши, че тъй като кодът е с цел почистване, случаите на откази не са важни, така че блокът catch остава празен, тъй като системата така или иначе се изключва.

Очевидно това не са най-добрите практики за програмиране, но изглежда нищо не е ужасно грешно ... с изключение на малък логически проблем в третия ред на оригиналния код:

Листинг 1. Оригинален код за почистване

private void cleanupConnections () хвърля ExceptionOne, ExceptionTwo {for (int i = 0; i <connections.length; i ++) {connection [i] .release (); // Изхвърля ExceptionOne, ExceptionTwo връзка [i] = null; } връзки = нула; } защитен абстракт void cleanupFiles () хвърля ExceptionThree, ExceptionFour; защитен абстракт void removeListeners () хвърля ExceptionFive, ExceptionSix; public void cleanupEverything () хвърля изключение {cleanupConnections (); cleanupFiles (); removeListeners (); } public void done () {try {doStuff (); cleanupEverything (); doMoreStuff (); } улов (Изключение д) {}}

В друга част от кода connectionsмасивът не се инициализира, докато не се създаде първата връзка. Но ако връзката никога не е създадена, тогава масивът връзки е нула. Така че в някои случаи призивът за connections[i].release()резултат води до a NullPointerException. Това е относително лесен проблем за отстраняване. Просто добавете чек за connections != null.

Изключението обаче никога не се съобщава. Той е хвърлен от cleanupConnections(), хвърлен отново от cleanupEverything()и накрая е хванат done(). В done()метода не прави нищо, с изключение, по нищо не личи, че влизате. И тъй като cleanupEverything()се извиква само чрез done(), изключението никога не се вижда. Така че кодът никога не се поправя.

По този начин, в сценария на неуспех, методите cleanupFiles()and removeListeners()никога не се извикват (така че техните ресурси никога не се освобождават) и doMoreStuff()никога не се извиква, като по този начин окончателната обработка в done()никога не завършва. За да се влошат нещата, done()не се извиква, когато системата се изключи; вместо това се призовава да завърши всяка транзакция. Така че ресурсите изтичат при всяка транзакция.

Този проблем очевидно е основен: грешките не се отчитат и ресурсите изтичат. Но самият код изглежда доста невинен и от начина, по който е написан кодът, този проблем се оказва труден за проследяване. Чрез прилагане на няколко прости насоки проблемът може да бъде намерен и отстранен:

  • Не пренебрегвайте изключенията
  • Не хващайте родови Exceptions
  • Не хвърляйте родови Exceptions

Не пренебрегвайте изключенията

Най-очевидният проблем с кода на Листинг 1 е, че грешка в програмата се игнорира напълно. Изхвърля се неочаквано изключение (изключенията по своята същност са неочаквани) и кодът не е готов да се справи с това изключение. Изключението дори не се отчита, защото кодът предполага, че очакваните изключения няма да имат последствия.

В повечето случаи трябва да се регистрира най-малкото изключение. Няколко пакета за регистриране (вижте страничната лента „Изключения при регистриране“) могат да регистрират системни грешки и изключения, без да влияят значително на производителността на системата. Повечето системи за регистриране също позволяват отпечатване на следи от стека, като по този начин предоставят ценна информация за това къде и защо е възникнало изключението. И накрая, тъй като дневниците обикновено се записват във файлове, запис на изключенията може да бъде прегледан и анализиран. Вижте Листинг 11 в страничната лента за пример за регистриране на следи от стекове.

Вписването на изключения не е критично в няколко конкретни ситуации. Едно от тях е почистване на ресурси в клауза окончателно.

Изключения в накрая

В листинг 2 някои данни се четат от файл. Файлът трябва да се затвори, независимо дали изключение чете данните, така че close()методът е обвит в клауза final. Но ако грешка затвори файла, не може да се направи много по въпроса:

Листинг 2

public void loadFile (String fileName) хвърля IOException {InputStream in = null; опитайте {in = new FileInputStream (fileName); readSomeData (в); } накрая {if (in! = null) {try {in.close (); } catch (IOException ioe) {// Игнориран}}}}

Имайте предвид, че loadFile()все още отчита метод IOExceptionза извикване, ако действителното зареждане на данни не успее поради проблем с I / O (вход / изход). Също така имайте предвид, че въпреки че изключение от close()се игнорира, кодът посочва това изрично в коментар, за да стане ясно на всеки, който работи по кода. Можете да приложите същата процедура за почистване на всички I / O потоци, затваряне на сокети и JDBC връзки и т.н.

Важното при игнорирането на изключения е да се гарантира, че само един метод е обвит в игнориращия блок try / catch (така че други методи в заграждащия блок все още се извикват) и че е уловено конкретно изключение. Това специално обстоятелство се различава отчетливо от улов на родово Exception. Във всички останали случаи изключението трябва да бъде (най-малкото) регистрирано, за предпочитане с проследяване на стека.

Не хващайте общи изключения

Често в сложен софтуер, даден блок код изпълнява методи, които създават различни изключения. Динамично зареждане на класа и на екземпляр на обект може да се хвърли няколко различни изключения, включително ClassNotFoundException, InstantiationException, IllegalAccessException, и ClassCastException.

Вместо да добавя четирите различни блока за хващане към блока try, зает програмист може просто да обгърне извикванията на метода в блок try / catch, който улавя общи Exceptions (вижте Листинг 3 по-долу). Въпреки че това изглежда безобидно, могат да се получат някои нежелани странични ефекти. Например, ако className()е null, Class.forName()ще хвърли a NullPointerException, което ще бъде уловено в метода.

В този случай блокът catch улавя изключения, които никога не е възнамерявал да улови, тъй като a NullPointerExceptionе подклас на RuntimeException, който от своя страна е подклас на Exception. Така че общите catch (Exception e)улови всички подкласове на RuntimeException, включително NullPointerException, IndexOutOfBoundsExceptionи ArrayStoreException. Обикновено програмист не възнамерява да улови тези изключения.

В листинг 3 null classNameрезултатите в a NullPointerException, което показва на извикващия метод, че името на класа е невалидно:

Листинг 3

публичен SomeInterface buildInstance (String className) {SomeInterface impl = null; опитайте {Class clazz = Class.forName (className); impl = (SomeInterface) clazz.newInstance (); } catch (Exception e) {log.error ("Грешка при създаване на клас:" + className); } return impl; }

Друго последствие от общата клауза за улов е, че регистрирането е ограничено, тъй като catchне знае конкретното изключение, което се улавя. Някои програмисти, когато се сблъскат с този проблем, прибягват до добавяне на отметка, за да видят типа изключение (виж Листинг 4), което противоречи на целта на използването на catch блокове:

Листинг 4

catch (Exception e) {if (e instanceof ClassNotFoundException) {log.error ("Невалидно име на клас:" + className + "," + e.toString ()); } else {log.error ("Не може да се създаде клас:" + className + "," + e.toString ()); }}

Листинг 5 предоставя пълен пример за улавяне на конкретни изключения, от които програмист може да се интересува. instanceofОператорът не се изисква, тъй като конкретните изключения са уловени. Всеки един от проверените изключения ( ClassNotFoundException, InstantiationException, IllegalAccessException) е хванат и обработени. Специалният случай, който би създал a ClassCastException(класът се зарежда правилно, но не изпълнява SomeInterfaceинтерфейса), също се проверява чрез проверка за това изключение:

Листинг 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; опитайте {while (true) {req = getNextRequest (); if (req! = null) {processRequest (req); // хвърля BadRequestException} else {// Опашката за заявка е празна, трябва да се направи break; }}} catch (BadRequestException e) {log.error ("Невалидна заявка:" + req, e); }}