Dlaczego wyjątek.printStackTrace () uważa się za złą praktykę?

126

Jest wiele z materiału out there co sugeruje, że druk ślad stosu wyjątku jest złą praktyką.

Np. Z sprawdzenia RegexpSingleline w Checkstyle:

Tego sprawdzenia można [...] użyć do znalezienia typowych złych praktyk, takich jak wywoływanie ex.printStacktrace ()

Jednak staram się znaleźć gdziekolwiek, co daje ważny powód, dla którego z pewnością ślad stosu jest bardzo przydatny w śledzeniu, co spowodowało wyjątek. Rzeczy, o których wiem:

  1. Ślad stosu nigdy nie powinien być widoczny dla użytkowników końcowych (ze względu na wygodę użytkownika i ze względów bezpieczeństwa)

  2. Generowanie śladu stosu jest stosunkowo kosztownym procesem (choć jest mało prawdopodobne, aby stanowił problem w większości „wyjątkowych” okoliczności)

  3. Wiele platform rejestrowania wydrukuje ślad stosu za Ciebie (nasz nie i nie, nie możemy go łatwo zmienić)

  4. Drukowanie śladu stosu nie stanowi obsługi błędów. Powinien być połączony z rejestrowaniem innych informacji i obsługą wyjątków.

Jakie są inne powody, dla których warto unikać drukowania śladu stosu w kodzie?

Chris Knight
źródło
12
Jako osoba, która musi regularnie rozwiązywać problemy, nigdy nie pominęłabym drukowania śladu stosu, gdy coś poszło nie tak. Tak, nie pokazuj tego użytkownikowi, ale tak, zrzuć go do dziennika błędów.
Paul Grime,
4
Czy naprawdę potrzebujesz innych powodów? Myślę, że całkiem dobrze odpowiedziałeś na swoje pytanie. Jednak stacktrace powinien być drukowany w wyjątkowych sytuacjach. :)
Neil

Odpowiedzi:

125

Throwable.printStackTrace()zapisuje ślad stosu do System.errPrintStream. System.errStrumień bazowy i norma „error” strumienia wyjściowego procesu JVM może być przekierowana przez

  • wywoływanie, System.setErr()które zmienia miejsce docelowe wskazywane przez System.err.
  • lub przez przekierowanie strumienia wyjściowego błędu procesu. Strumień wyjściowy błędu może zostać przekierowany do pliku / urządzenia
    • których treść może zostać zignorowana przez personel,
    • plik / urządzenie może nie mieć możliwości rotacji dziennika, co oznacza, że ​​ponowne uruchomienie procesu jest wymagane w celu zamknięcia otwartego uchwytu pliku / urządzenia przed zarchiwizowaniem istniejącej zawartości pliku / urządzenia.
    • lub plik / urządzenie faktycznie odrzuca wszystkie zapisane w nim dane, jak ma to miejsce w przypadku /dev/null.

Wnioskując z powyższego, wywołanie Throwable.printStackTrace()stanowi tylko prawidłowe (nie dobre / świetne) zachowanie obsługi wyjątków

  • jeśli nie System.errpodlegałeś zmianie przydziału przez cały czas trwania aplikacji,
  • a jeśli nie potrzebujesz rotacji logów podczas działania aplikacji,
  • i jeśli przyjęto / zaprojektowano, praktyką rejestrowania aplikacji jest zapis System.err(i standardowy strumień wyjściowy błędów maszyny JVM).

W większości przypadków powyższe warunki nie są spełnione. Można nie być świadomym istnienia innego kodu działającego w JVM i nie można przewidzieć rozmiaru pliku dziennika ani czasu trwania procesu w czasie wykonywania, a dobrze zaprojektowana praktyka rejestrowania polegałaby na pisaniu plików dziennika „przetwarzanych maszynowo” (a preferowana, ale opcjonalna funkcja w rejestratorze) w znanym miejscu docelowym, aby pomóc w obsłudze.

Na koniec należy pamiętać, że wynik programu na Throwable.printStackTrace()pewno zostałby przepleciony z innymi zapisanymi treściami System.err(i być może nawet System.outgdyby obie zostały przekierowane do tego samego pliku / urządzenia). Jest to irytacja (w przypadku aplikacji jednowątkowych), z którą należy sobie poradzić, ponieważ w takim przypadku dane wokół wyjątków nie są łatwe do przeanalizowania. Co gorsza, jest wysoce prawdopodobne, że aplikacja wielowątkowa będzie generować bardzo zagmatwane dzienniki, ponieważ Throwable.printStackTrace() nie jest bezpieczna dla wątków .

Nie ma mechanizmu synchronizacji do synchronizacji zapisu śladu stosu, System.errgdy wiele wątków jest wywoływanych Throwable.printStackTrace()w tym samym czasie. Rozwiązanie tego w rzeczywistości wymaga synchronizacji kodu na monitorze powiązanym z System.err(a także System.out, jeśli docelowy plik / urządzenie jest takie samo), a to jest dość wysoka cena za poprawność pliku dziennika. Na przykład, klasy ConsoleHandleri StreamHandlersą odpowiedzialne za dołączanie rekordów dziennika do konsoli, korzystając z narzędzia do rejestrowania udostępnianego przez java.util.logging; faktyczna operacja publikowania rekordów dziennika jest zsynchronizowana - każdy wątek, który próbuje opublikować rekord dziennika, musi również uzyskać blokadę na monitorze związanym zStreamHandlerinstancja. Jeśli chcesz mieć taką samą gwarancję posiadania rekordów dziennika bez przeplotu przy użyciu System.out/ System.err, musisz zapewnić to samo - komunikaty są publikowane w tych strumieniach w sposób umożliwiający serializację.

Biorąc pod uwagę wszystkie powyższe i bardzo ograniczone scenariusze, w których Throwable.printStackTrace()faktycznie się przydają, często okazuje się, że przywoływanie tego jest złą praktyką.


Rozszerzając argument w jednym z poprzednich akapitów, jest to również zły wybór Throwable.printStackTracew połączeniu z loggerem, który zapisuje w konsoli. Częściowo jest to spowodowane tym, że rejestrator synchronizuje się na innym monitorze, podczas gdy aplikacja (prawdopodobnie, jeśli nie chcesz, aby rekordy dziennika z przeplotem) synchronizowała się na innym monitorze. Argument ten ma również zastosowanie, gdy używasz dwóch różnych rejestratorów, które piszą do tego samego miejsca docelowego w aplikacji.

Vineet Reynolds
źródło
Świetna odpowiedź, dzięki. Jednak chociaż zgadzam się, że przez większość czasu jest to hałas lub nie jest to konieczne, są takie chwile, kiedy jest to absolutnie krytyczne.
Chris Knight,
1
@Chris Tak, są chwile, kiedy nie możesz tylko używać System.out.printlni Throwable.printStackTraceoczywiście wymagana jest ocena programisty. Trochę się martwiłem, że pominięto część dotyczącą bezpieczeństwa nici. Jeśli spojrzysz na większość implementacji rejestratorów, zauważysz, że synchronizują one część, w której zapisywane są rekordy dziennika (nawet do konsoli), chociaż nie pobierają monitorów na System.errlub System.out.
Vineet Reynolds
2
Czy pomyliłbym się, zauważając, że żaden z tych powodów nie ma zastosowania do przesłonięć printStackTrace, które przyjmują obiekt PrintStream lub PrintWriter jako parametr?
ESRogs
1
@ Geek, ten drugi jest deskryptorem pliku procesu o wartości 2. Ten pierwszy jest tylko abstrakcją.
Vineet Reynolds
1
W kodzie źródłowym JDK metody printStackTrace () używa zsynchronizowanego do blokowania obiektu PrintStream System.err, więc powinna być metodą bezpieczną wątkowo.
EyouGo
33

Dotykasz tutaj wielu problemów:

1) Ślad stosu nigdy nie powinien być widoczny dla użytkowników końcowych (ze względu na wygodę użytkownika i ze względów bezpieczeństwa)

Tak, powinien być dostępny do diagnozowania problemów użytkowników końcowych, ale użytkownik końcowy nie powinien ich widzieć z dwóch powodów:

  • Są bardzo niejasne i nieczytelne, aplikacja będzie wyglądać bardzo nieprzyjaźnie dla użytkownika.
  • Pokazywanie śladu stosu użytkownikowi końcowemu może spowodować potencjalne zagrożenie bezpieczeństwa. Popraw mnie, jeśli się mylę, PHP faktycznie wyświetla parametry funkcji w śladzie stosu - genialne, ale bardzo niebezpieczne - jeśli miałbyś otrzymać wyjątek podczas łączenia się z bazą danych, co prawdopodobnie zobaczysz w śledzeniu stosu?

2) Generowanie śladu stosu jest stosunkowo kosztownym procesem (chociaż jest mało prawdopodobne, aby stanowił problem w większości „wyjątkowych” okoliczności)

Generowanie śladu stosu ma miejsce, gdy wyjątek jest tworzony / rzucany (dlatego rzucenie wyjątku ma swoją cenę), drukowanie nie jest takie drogie. W rzeczywistości możesz zastąpićThrowable#fillInStackTrace() skutecznie wyjątek niestandardowy, dzięki czemu rzucanie wyjątku jest prawie tak tanie, jak proste polecenie GOTO.

3) Wiele platform logowania wydrukuje ślad stosu za Ciebie (nasz nie i nie, nie możemy go łatwo zmienić)

Bardzo dobra uwaga. Główny problem polega na tym, że jeśli framework rejestruje wyjątek za Ciebie, nie rób nic (ale upewnij się, że tak jest!) Jeśli chcesz sam zarejestrować wyjątek, użyj struktury rejestrowania, takiej jak Logback lub Log4J , aby nie umieszczać ich na surowej konsoli ponieważ bardzo trudno to kontrolować.

Dzięki strukturze logowania możesz łatwo przekierować ślady stosu do pliku, konsoli lub nawet wysłać je na określony adres e-mail. Z zakodowanym printStackTrace()na stałe musisz żyć z sysout.

4) Drukowanie śladu stosu nie stanowi obsługi błędów. Powinien być połączony z rejestrowaniem innych informacji i obsługą wyjątków.

Ponownie: zaloguj się SQLExceptionpoprawnie (z pełnym śladem stosu, używając struktury rejestrowania) i pokaż miły: „ Przepraszamy, obecnie nie możemy przetworzyć Twojego żądania ”. Czy naprawdę uważasz, że użytkownik jest zainteresowany powodami? Czy widziałeś ekran błędu StackOverflow? Jest bardzo zabawny, ale nie zdradza żadnych szczegółów. Jednak zapewnia użytkownikowi, że problem zostanie zbadany.

Ale to zrobi zadzwonić natychmiast i trzeba być w stanie zdiagnozować problem. Potrzebujesz więc obu: odpowiedniego rejestrowania wyjątków i przyjaznych dla użytkownika komunikatów.


Podsumowując: zawsze rejestruj wyjątki (najlepiej przy użyciu struktury rejestrowania ), ale nie ujawniaj ich użytkownikowi końcowemu. Zastanów się dokładnie i nad komunikatami o błędach w swoim GUI, pokazuj ślady stosu tylko w trybie programowania.

Tomasz Nurkiewicz
źródło
Twoja odpowiedź jest tą, którą otrzymałem.
Blasanka
„większość„ wyjątkowych ”okoliczności”. miły.
awwsmm
19

Pierwsza rzecz printStackTrace () nie jest kosztowna, jak twierdzisz, ponieważ ślad stosu jest wypełniany, gdy wyjątek jest tworzony sam.

Chodzi o to, aby wszystko, co trafia do dzienników, było przekazywane przez strukturę rejestratora, aby można było kontrolować rejestrowanie. Dlatego zamiast używać printStackTrace, po prostu użyj czegoś takiego jakLogger.log(msg, exception);

Suraj Chandran
źródło
11

Drukowanie śladu stosu wyjątku samo w sobie nie stanowi złej praktyki, a jedynie drukowanie śladu stosu, gdy wystąpi wyjątek, jest tutaj prawdopodobnie problemem - często samo wydrukowanie śladu stosu nie wystarcza.

Istnieje również tendencja do podejrzeń, że właściwa obsługa wyjątków nie jest wykonywana, jeśli wszystko, co jest wykonywane w catchbloku, to plik e.printStackTrace. Niewłaściwa obsługa może w najlepszym przypadku oznaczać, że problem jest ignorowany, aw najgorszym program kontynuuje wykonywanie w niezdefiniowanym lub nieoczekiwanym stanie.

Przykład

Rozważmy następujący przykład:

try {
  initializeState();

} catch (TheSkyIsFallingEndOfTheWorldException e) {
  e.printStackTrace();
}

continueProcessingAssumingThatTheStateIsCorrect();

Tutaj chcemy wykonać pewne przetwarzanie inicjalizacji, zanim przejdziemy do przetwarzania, które wymaga, aby inicjalizacja miała miejsce.

W powyższym kodzie wyjątek powinien zostać przechwycony i odpowiednio obsłużony, aby uniemożliwić programowi przejście do continueProcessingAssumingThatTheStateIsCorrect metody, która mogłaby spowodować problemy.

W wielu przypadkach e.printStackTrace()oznacza to, że jakiś wyjątek jest połykany i przetwarzanie może przebiegać tak, jakby nie wystąpił każdy problem.

Dlaczego stało się to problemem?

Prawdopodobnie jednym z największych powodów, dla których słaba obsługa wyjątków stała się bardziej powszechna, jest sposób, w jaki środowiska IDE, takie jak Eclipse, będą automatycznie generować kod, który wykona e.printStackTraceoperację obsługi wyjątków:

try {
  Thread.sleep(1000);
} catch (InterruptedException e) {
  // TODO Auto-generated catch block
  e.printStackTrace();
}

(Powyższe to rzeczywisty try-catchautomatycznie generowany przez Eclipse do obsługi InterruptedExceptionwyrzucenia przez Thread.sleep.)

W przypadku większości aplikacji samo wydrukowanie śladu stosu na błąd standardowy prawdopodobnie nie będzie wystarczające. Niewłaściwa obsługa wyjątków może w wielu przypadkach doprowadzić do uruchomienia aplikacji w stanie, który jest nieoczekiwany i może prowadzić do nieoczekiwanego i niezdefiniowanego zachowania.

coobird
źródło
1
To. Dość często po prostu nie wychwycenie wyjątku w ogóle, przynajmniej na poziomie metody, jest lepsze niż przechwycenie, wydrukowanie śladu stosu i kontynuowanie tak, jakby nie pojawił się żaden problem.
eis
10

Myślę, że twoja lista powodów jest dość obszerna.

Jeden szczególnie zły przykład, który napotkałem więcej niż raz, wygląda następująco:

    try {
      // do stuff
    } catch (Exception e) {
        e.printStackTrace(); // and swallow the exception
    }

Problem z powyższym kodem polega na tym, że obsługa składa się wyłącznie z printStackTracewywołania: wyjątek nie jest właściwie obsługiwany ani nie ma możliwości ucieczki.

Z drugiej strony, z reguły zawsze rejestruję ślad stosu, gdy w moim kodzie jest nieoczekiwany wyjątek. Przez lata ta polityka zaoszczędziła mi wiele czasu na debugowanie.

Wreszcie, w lżejszym tonie, doskonały wyjątek Boga .

NPE
źródło
„wyjątek nie może uciec” - czy chodziło Ci o to, że wyjątek jest propagowany na stosie? czym różni się rejestrowanie od printStackTrace ()?
MasterJoe
5

printStackTrace()drukuje na konsoli. W warunkach produkcyjnych nikt nigdy tego nie ogląda. Suraj ma rację, powinien przekazać te informacje rejestratorowi.

Oh Chin Boon
źródło
Ważna uwaga, chociaż uważnie obserwujemy wyjście z konsoli produkcyjnej.
Chris Knight,
Czy możesz wyjaśnić, co masz na myśli, mówiąc uważnie obserwując? Jak i kto? Na jakiej częstotliwości?
Oh Chin Boon
Uważnie obserwując, powinienem powiedzieć, kiedy tego wymaga. Jest to plik dziennika kroczącego, który jest jednym z kilku portów połączenia, jeśli coś pójdzie nie tak z naszą aplikacją.
Chris Knight,
3

W aplikacjach serwerowych stacktrace wysadza twój plik stdout / stderr. Może stawać się coraz większy i wypełniony bezużytecznymi danymi, ponieważ zwykle nie masz kontekstu, sygnatury czasowej i tak dalej.

np. catalina.out podczas używania tomcat jako kontenera

Hajo Thelen
źródło
Słuszna uwaga. Nieprawidłowe użycie printStackTrace () może wysadzić nasze pliki logowania, a przynajmniej wypełnić je stosami bezużytecznych informacji
Chris Knight,
@ChrisKnight - czy możesz zasugerować artykuł wyjaśniający, w jaki sposób pliki logowania mogą zostać wysadzone bezużytecznymi informacjami? dzięki.
MasterJoe
3

Nie jest to zła praktyka, ponieważ coś jest „nie tak” w funkcji PrintStackTrace (), ale ponieważ jest to „zapach kodu”. W większości przypadków wywołanie PrintStackTrace () występuje, ponieważ komuś nie udało się poprawnie obsłużyć wyjątku. Gdy we właściwy sposób poradzisz sobie z wyjątkiem, na ogół nie przejmujesz się już StackTrace.

Ponadto wyświetlanie śladu stosu na stderr jest ogólnie przydatne tylko podczas debugowania, a nie w środowisku produkcyjnym, ponieważ bardzo często stderr prowadzi donikąd. Rejestrowanie ma większy sens. Jednak samo zastąpienie funkcji PrintStackTrace () rejestrowaniem wyjątku nadal pozostawia aplikację, która uległa awarii, ale nadal działa, jakby nic się nie stało.

AVee
źródło
0

Jak niektórzy faceci już tutaj wspomnieli, problem polega na tym, że z wyjątkiem połknięcia na wypadek, gdybyś zadzwonił e.printStackTrace()docatch bloku. Nie zatrzyma wykonywania wątku i będzie kontynuowane po bloku try, jak w normalnych warunkach.

Zamiast tego musisz albo spróbować odzyskać z wyjątku (w przypadku, gdy jest możliwy do odzyskania), albo rzucić RuntimeException, albo przesłać do wywołującego wyjątek, aby uniknąć cichych awarii (na przykład z powodu niewłaściwej konfiguracji rejestratora).

gumkins
źródło
0

Aby uniknąć splątanego problemu ze strumieniem wyjściowym, o którym mówi @Vineet Reynolds

możesz wydrukować to na standardowe wyjście: e.printStackTrace(System.out);

Traeyee
źródło