Co to jest ślad stosu i jak mogę go użyć do debugowania błędów aplikacji?
Czasami, gdy uruchamiam moją aplikację, wyświetla mi się błąd, który wygląda następująco:
Exception in thread "main" java.lang.NullPointerException
at com.example.myproject.Book.getTitle(Book.java:16)
at com.example.myproject.Author.getBookTitles(Author.java:25)
at com.example.myproject.Bootstrap.main(Bootstrap.java:14)
Ludzie nazywają to „śladem stosu”. Co to jest ślad stosu? Co może mi powiedzieć o błędzie, który występuje w moim programie?
O tym pytaniu - dość często pojawia się pytanie, gdzie początkujący programista „otrzymuje błąd” i po prostu wklejają swój ślad stosu i jakiś losowy blok kodu bez zrozumienia, czym jest ślad stosu i jak mogą go używać. To pytanie jest przeznaczone jako odniesienie dla początkujących programistów, którzy mogą potrzebować pomocy w zrozumieniu wartości śladu stosu.
Odpowiedzi
Mówiąc prościej, ślad stosu to lista wywołań metod, w których aplikacja znajdowała się w trakcie zgłaszania wyjątku.
Prosty przykład
Na przykładzie podanym w pytaniu możemy dokładnie określić, gdzie w aplikacji został zgłoszony wyjątek. Rzućmy okiem na ślad stosu:
Exception in thread "main" java.lang.NullPointerException
at com.example.myproject.Book.getTitle(Book.java:16)
at com.example.myproject.Author.getBookTitles(Author.java:25)
at com.example.myproject.Bootstrap.main(Bootstrap.java:14)
To jest bardzo prosty ślad stosu. Jeśli zaczniemy od początku listy „o…”, możemy stwierdzić, gdzie wystąpił nasz błąd. To, czego szukamy, to najwyższe wywołanie metody, które jest częścią naszej aplikacji. W tym przypadku jest to:
at com.example.myproject.Book.getTitle(Book.java:16)
Aby to debugować, możemy otworzyć Book.java
i spojrzeć na linię 16
, która jest:
15 public String getTitle() {
16 System.out.println(title.toString());
17 return title;
18 }
Oznaczałoby to, że coś (prawdopodobnie title
) jest null
w powyższym kodzie.
Przykład z łańcuchem wyjątków
Czasami aplikacje przechwytują wyjątek i ponownie wyrzucają go jako przyczynę innego wyjątku. Zwykle wygląda to tak:
34 public void getBookIds(int id) {
35 try {
36 book.getId(id); // this method it throws a NullPointerException on line 22
37 } catch (NullPointerException e) {
38 throw new IllegalStateException("A book has a null property", e)
39 }
40 }
To może dać ci ślad stosu, który wygląda następująco:
Exception in thread "main" java.lang.IllegalStateException: A book has a null property
at com.example.myproject.Author.getBookIds(Author.java:38)
at com.example.myproject.Bootstrap.main(Bootstrap.java:14)
Caused by: java.lang.NullPointerException
at com.example.myproject.Book.getId(Book.java:22)
at com.example.myproject.Author.getBookIds(Author.java:36)
... 1 more
Tym, co różni się w tym, jest „Spowodowane przez”. Czasami wyjątki będą miały wiele sekcji „Spowodowane przez”. W tym celu zazwyczaj chcesz znaleźć „główną przyczynę”, która będzie jedną z najniższych sekcji „Spowodowane przez” w śladzie stosu. W naszym przypadku jest to:
Caused by: java.lang.NullPointerException <-- root cause
at com.example.myproject.Book.getId(Book.java:22) <-- important line
Ponownie, z tym wyjątkiem, chcielibyśmy spojrzeć na wiersz 22
of, Book.java
aby zobaczyć, co może spowodować NullPointerException
tutaj.
Bardziej zniechęcający przykład z kodem biblioteki
Zwykle ślady stosu są znacznie bardziej złożone niż w dwóch powyższych przykładach. Oto przykład (jest długi, ale pokazuje kilka poziomów połączonych wyjątków):
javax.servlet.ServletException: Something bad happened
at com.example.myproject.OpenSessionInViewFilter.doFilter(OpenSessionInViewFilter.java:60)
at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1157) at com.example.myproject.ExceptionHandlerFilter.doFilter(ExceptionHandlerFilter.java:28) at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1157)
at com.example.myproject.OutputBufferFilter.doFilter(OutputBufferFilter.java:33)
at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1157) at org.mortbay.jetty.servlet.ServletHandler.handle(ServletHandler.java:388) at org.mortbay.jetty.security.SecurityHandler.handle(SecurityHandler.java:216) at org.mortbay.jetty.servlet.SessionHandler.handle(SessionHandler.java:182) at org.mortbay.jetty.handler.ContextHandler.handle(ContextHandler.java:765) at org.mortbay.jetty.webapp.WebAppContext.handle(WebAppContext.java:418) at org.mortbay.jetty.handler.HandlerWrapper.handle(HandlerWrapper.java:152) at org.mortbay.jetty.Server.handle(Server.java:326) at org.mortbay.jetty.HttpConnection.handleRequest(HttpConnection.java:542) at org.mortbay.jetty.HttpConnection$RequestHandler.content(HttpConnection.java:943)
at org.mortbay.jetty.HttpParser.parseNext(HttpParser.java:756)
at org.mortbay.jetty.HttpParser.parseAvailable(HttpParser.java:218)
at org.mortbay.jetty.HttpConnection.handle(HttpConnection.java:404)
at org.mortbay.jetty.bio.SocketConnector$Connection.run(SocketConnector.java:228) at org.mortbay.thread.QueuedThreadPool$PoolThread.run(QueuedThreadPool.java:582)
Caused by: com.example.myproject.MyProjectServletException
at com.example.myproject.MyServlet.doPost(MyServlet.java:169)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:727)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:820)
at org.mortbay.jetty.servlet.ServletHolder.handle(ServletHolder.java:511)
at org.mortbay.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1166) at com.example.myproject.OpenSessionInViewFilter.doFilter(OpenSessionInViewFilter.java:30) ... 27 more Caused by: org.hibernate.exception.ConstraintViolationException: could not insert: [com.example.myproject.MyEntity] at org.hibernate.exception.SQLStateConverter.convert(SQLStateConverter.java:96) at org.hibernate.exception.JDBCExceptionHelper.convert(JDBCExceptionHelper.java:66) at org.hibernate.id.insert.AbstractSelectingDelegate.performInsert(AbstractSelectingDelegate.java:64) at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:2329) at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:2822) at org.hibernate.action.EntityIdentityInsertAction.execute(EntityIdentityInsertAction.java:71) at org.hibernate.engine.ActionQueue.execute(ActionQueue.java:268) at org.hibernate.event.def.AbstractSaveEventListener.performSaveOrReplicate(AbstractSaveEventListener.java:321) at org.hibernate.event.def.AbstractSaveEventListener.performSave(AbstractSaveEventListener.java:204) at org.hibernate.event.def.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.java:130) at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.saveWithGeneratedOrRequestedId(DefaultSaveOrUpdateEventListener.java:210) at org.hibernate.event.def.DefaultSaveEventListener.saveWithGeneratedOrRequestedId(DefaultSaveEventListener.java:56) at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.entityIsTransient(DefaultSaveOrUpdateEventListener.java:195) at org.hibernate.event.def.DefaultSaveEventListener.performSaveOrUpdate(DefaultSaveEventListener.java:50) at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.onSaveOrUpdate(DefaultSaveOrUpdateEventListener.java:93) at org.hibernate.impl.SessionImpl.fireSave(SessionImpl.java:705) at org.hibernate.impl.SessionImpl.save(SessionImpl.java:693) at org.hibernate.impl.SessionImpl.save(SessionImpl.java:689) at sun.reflect.GeneratedMethodAccessor5.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) at java.lang.reflect.Method.invoke(Method.java:597) at org.hibernate.context.ThreadLocalSessionContext$TransactionProtectionWrapper.invoke(ThreadLocalSessionContext.java:344)
at $Proxy19.save(Unknown Source)
at com.example.myproject.MyEntityService.save(MyEntityService.java:59) <-- relevant call (see notes below)
at com.example.myproject.MyServlet.doPost(MyServlet.java:164)
... 32 more
Caused by: java.sql.SQLException: Violation of unique constraint MY_ENTITY_UK_1: duplicate value(s) for column(s) MY_COLUMN in statement [...]
at org.hsqldb.jdbc.Util.throwError(Unknown Source)
at org.hsqldb.jdbc.jdbcPreparedStatement.executeUpdate(Unknown Source)
at com.mchange.v2.c3p0.impl.NewProxyPreparedStatement.executeUpdate(NewProxyPreparedStatement.java:105)
at org.hibernate.id.insert.AbstractSelectingDelegate.performInsert(AbstractSelectingDelegate.java:57)
... 54 more
W tym przykładzie jest o wiele więcej. To, co nas najbardziej martwi, to szukanie metod, które pochodzą z naszego kodu , który byłby wszystkim w com.example.myproject
pakiecie. W drugim przykładzie (powyżej) chcielibyśmy najpierw przyjrzeć się głównej przyczynie, którą jest:
Caused by: java.sql.SQLException
Jednak wszystkie wywołania metod w ramach tego kodu są kodem biblioteki. Więc przejdziemy do „Spowodowane przez” powyżej i poszukamy pierwszego wywołania metody pochodzącego z naszego kodu, którym jest:
at com.example.myproject.MyEntityService.save(MyEntityService.java:59)
Podobnie jak w poprzednich przykładach, powinniśmy spojrzeć na MyEntityService.java
on-line 59
, ponieważ to tam powstał ten błąd (ten jest nieco oczywisty, co poszło nie tak, ponieważ SQLException określa błąd, ale procedura debugowania jest tym, czego szukamy).
Publikuję tę odpowiedź, więc najwyższa odpowiedź (posortowana według czynności) nie jest po prostu błędną.
Co to jest Stacktrace?
Stacktrace jest bardzo pomocnym narzędziem do debugowania. Pokazuje stos wywołań (czyli stos funkcji, które zostały wywołane do tego momentu) w momencie wyrzucenia nieprzechwyconego wyjątku (lub w momencie, gdy ślad stosu został wygenerowany ręcznie). Jest to bardzo przydatne, ponieważ nie tylko pokazuje, gdzie wystąpił błąd, ale także w jaki sposób program znalazł się w tym miejscu kodu. To prowadzi do następnego pytania:
Co to jest wyjątek?
Wyjątek to sposób, w jaki środowisko wykonawcze informuje, że wystąpił błąd. Popularne przykłady to NullPointerException, IndexOutOfBoundsException lub ArithmeticException. Każdy z nich jest spowodowany próbą zrobienia czegoś, co nie jest możliwe. Na przykład wyjątek NullPointerException zostanie wyrzucony podczas próby wyłuskiwania obiektu o wartości Null:
Object a = null;
a.toString(); //this line throws a NullPointerException
Object[] b = new Object[5];
System.out.println(b[10]); //this line throws an IndexOutOfBoundsException,
//because b is only 5 elements long
int ia = 5;
int ib = 0;
ia = ia/ib; //this line throws an ArithmeticException with the
//message "/ by 0", because you are trying to
//divide by 0, which is not possible.
Jak mam radzić sobie ze Stacktraces / Exceptions?
Najpierw dowiedz się, co powoduje wyjątek. Spróbuj wyszukać w Google nazwę wyjątku, aby dowiedzieć się, jaka jest przyczyna tego wyjątku. W większości przypadków będzie to spowodowane nieprawidłowym kodem. W podanych powyżej przykładach wszystkie wyjątki są spowodowane nieprawidłowym kodem. Tak więc w przypadku przykładu NullPointerException można się upewnić, że a
w tym czasie nigdy nie jest null. Możesz na przykład zainicjować a
lub dołączyć czek podobny do tego:
if (a!=null) {
a.toString();
}
W ten sposób naruszająca linia nie jest wykonywana, jeśli a==null
. To samo dotyczy innych przykładów.
Czasami nie możesz się upewnić, że nie dostaniesz wyjątku. Na przykład, jeśli używasz połączenia sieciowego w swoim programie, nie możesz powstrzymać komputera przed utratą połączenia internetowego (np. Nie możesz powstrzymać użytkownika przed rozłączeniem połączenia sieciowego komputera). W takim przypadku biblioteka sieciowa prawdopodobnie zgłosi wyjątek. Teraz powinieneś złapać wyjątek i zająć się nim. Oznacza to, że w przykładzie z połączeniem sieciowym powinieneś spróbować ponownie otworzyć połączenie lub powiadomić użytkownika lub coś w tym rodzaju. Ponadto za każdym razem, gdy używasz catch, zawsze przechwytuj tylko wyjątek, który chcesz przechwycić, nie używaj instrukcji typu broad catchcatch (Exception e)
, które przechwytują wszystkie wyjątki. Jest to bardzo ważne, ponieważ w przeciwnym razie możesz przypadkowo złapać niewłaściwy wyjątek i zareagować w niewłaściwy sposób.
try {
Socket x = new Socket("1.1.1.1", 6789);
x.getInputStream().read()
} catch (IOException e) {
System.err.println("Connection could not be established, please try again later!")
}
Dlaczego nie powinienem używać catch (Exception e)
?
Skorzystajmy z małego przykładu, aby pokazać, dlaczego nie należy po prostu wyłapywać wszystkich wyjątków:
int mult(Integer a,Integer b) {
try {
int result = a/b
return result;
} catch (Exception e) {
System.err.println("Error: Division by zero!");
return 0;
}
}
To, co próbuje zrobić ten kod, to wychwycić ArithmeticException
spowodowane przez możliwe dzielenie przez 0. Ale wyłapuje również możliwe, NullPointerException
które jest wyrzucane, jeśli a
lub b
są null
. Oznacza to, że możesz otrzymać a, NullPointerException
ale potraktujesz go jako ArithmeticException i prawdopodobnie zrobisz niewłaściwą rzecz. W najlepszym przypadku nadal nie zauważysz, że wystąpił wyjątek NullPointerException. Takie rzeczy znacznie utrudniają debugowanie, więc nie rób tego.
TLDR
- Dowiedz się, co jest przyczyną wyjątku i napraw go, aby w ogóle nie zgłaszał wyjątku.
Jeśli 1. nie jest możliwe, przechwyć określony wyjątek i obsłuż go.
- Nigdy po prostu nie dodawaj try / catch, a potem po prostu zignoruj wyjątek! Nie rób tego!
- Nigdy nie używaj
catch (Exception e)
, zawsze wychwytuj określone wyjątki. To zaoszczędzi ci wielu bólów głowy.
Aby dodać do tego, o czym wspomniał Rob. Ustawienie punktów przerwania w aplikacji umożliwia stopniowe przetwarzanie stosu. Dzięki temu programiści mogą użyć debugera, aby zobaczyć, w którym dokładnie momencie metoda robi coś, co było nieoczekiwane.
Ponieważ Rob użył NullPointerException
(NPE) do zilustrowania czegoś powszechnego, możemy pomóc w usunięciu tego problemu w następujący sposób:
jeśli mamy metodę, która przyjmuje parametry takie jak: void (String firstName)
W naszym kodzie chcielibyśmy ocenić, który firstName
zawiera wartość, zrobilibyśmy to w następujący sposób:if(firstName == null || firstName.equals("")) return;
Powyższe uniemożliwia nam użycie firstName
jako niebezpiecznego parametru. Dlatego wykonując testy zerowe przed przetwarzaniem, możemy zapewnić, że nasz kod będzie działał poprawnie. Aby rozwinąć przykład, który wykorzystuje obiekt z metodami, możemy spojrzeć tutaj:
if(dog == null || dog.firstName == null) return;
Powyższe jest właściwą kolejnością sprawdzania null, zaczynamy od obiektu bazowego, w tym przypadku psa, a następnie zaczynamy chodzić po drzewie możliwości, aby upewnić się, że wszystko jest poprawne przed przetworzeniem. Gdyby kolejność została odwrócona, potencjalnie mógłby zostać wyrzucony NPE i nasz program się zawiesił.
Jest jeszcze jedna funkcja śledzenia stosu oferowana przez rodzinę Throwable - możliwość manipulowania informacjami o śledzeniu stosu.
Standardowe zachowanie:
package test.stack.trace;
public class SomeClass {
public void methodA() {
methodB();
}
public void methodB() {
methodC();
}
public void methodC() {
throw new RuntimeException();
}
public static void main(String[] args) {
new SomeClass().methodA();
}
}
Ślad stosu:
Exception in thread "main" java.lang.RuntimeException
at test.stack.trace.SomeClass.methodC(SomeClass.java:18)
at test.stack.trace.SomeClass.methodB(SomeClass.java:13)
at test.stack.trace.SomeClass.methodA(SomeClass.java:9)
at test.stack.trace.SomeClass.main(SomeClass.java:27)
Zmanipulowany ślad stosu:
package test.stack.trace;
public class SomeClass {
...
public void methodC() {
RuntimeException e = new RuntimeException();
e.setStackTrace(new StackTraceElement[]{
new StackTraceElement("OtherClass", "methodX", "String.java", 99),
new StackTraceElement("OtherClass", "methodY", "String.java", 55)
});
throw e;
}
public static void main(String[] args) {
new SomeClass().methodA();
}
}
Ślad stosu:
Exception in thread "main" java.lang.RuntimeException
at OtherClass.methodX(String.java:99)
at OtherClass.methodY(String.java:55)
Aby zrozumieć nazwę : Ślad stosu to lista wyjątków (lub można powiedzieć, że jest to lista „Przyczyna według”), od najbardziej powierzchownego wyjątku (np. Wyjątek warstwy usług) do najgłębszego (np. Wyjątek bazy danych). Tak jak powód, dla którego nazywamy go `` stosem '', jest taki, że stos jest pierwszy na końcu ostatni (FILO), najgłębszy wyjątek wystąpił na samym początku, a następnie został wygenerowany łańcuch wyjątków, seria konsekwencji, wyjątek powierzchniowy był ostatnim jedno zdarzyło się w czasie, ale widzimy to w pierwszej kolejności.
Klucz 1 : Podstępną i ważną rzeczą, którą należy tutaj zrozumieć, jest to, że najgłębsza przyczyna może nie być „przyczyną źródłową”, ponieważ jeśli napiszesz jakiś „zły kod”, może to spowodować jakiś wyjątek pod spodem, który jest głębszy niż jego warstwa. Na przykład zła kwerenda sql może spowodować zresetowanie połączenia SQLServerException w dnie zamiast błędu syndax, który może znajdować się w środku stosu.
-> Znajdź główną przyczynę pośrodku to twoja praca.
Klucz 2 : Kolejna trudna, ale ważna rzecz znajduje się wewnątrz każdego bloku „Przyczyna po”, pierwsza linia była najgłębszą warstwą i zdarzyła się na pierwszym miejscu dla tego bloku. Na przykład,
Exception in thread "main" java.lang.NullPointerException
at com.example.myproject.Book.getTitle(Book.java:16)
at com.example.myproject.Author.getBookTitles(Author.java:25)
at com.example.myproject.Bootstrap.main(Bootstrap.java:14)
Book.java:16 został wywołany przez Auther.java:25, który został wywołany przez Bootstrap.java:14, Book.java:16 był główną przyczyną. Tutaj załącz diagram, posortuj stos śledzenia w porządku chronologicznym.
Aby dodać do innych przykładów, istnieją klasy wewnętrzne (zagnieżdżone), które pojawiają się ze $
znakiem. Na przykład:
public class Test {
private static void privateMethod() {
throw new RuntimeException();
}
public static void main(String[] args) throws Exception {
Runnable runnable = new Runnable() {
@Override public void run() {
privateMethod();
}
};
runnable.run();
}
}
Spowoduje to następujący ślad stosu:
Exception in thread "main" java.lang.RuntimeException
at Test.privateMethod(Test.java:4)
at Test.access$000(Test.java:1)
at Test$1.run(Test.java:10)
at Test.main(Test.java:13)
Inne posty opisują, czym jest ślad stosu, ale nadal może być ciężki z nim pracować.
Jeśli uzyskasz ślad stosu i chcesz prześledzić przyczynę wyjątku, dobrym punktem wyjścia do zrozumienia tego jest użycie konsoli śledzenia stosu Java w środowisku Eclipse . Jeśli używasz innego IDE, może istnieć podobna funkcja, ale ta odpowiedź dotyczy Eclipse.
Po pierwsze, upewnij się, że wszystkie źródła Java są dostępne w projekcie Eclipse.
Następnie w perspektywie Java kliknij kartę Konsola (zwykle u dołu). Jeśli widok konsoli nie jest widoczny, przejdź do opcji menu Okno -> Pokaż widok i wybierz Konsola .
Następnie w oknie konsoli kliknij następujący przycisk (po prawej)
a następnie z listy rozwijanej wybierz pozycję Konsola śledzenia stosu Java .
Wklej swój ślad stosu do konsoli. Następnie dostarczy listę linków do twojego kodu źródłowego i każdego innego dostępnego kodu źródłowego.
Oto, co możesz zobaczyć (obraz z dokumentacji Eclipse):
Ostatnio wykonane wywołanie metody będzie szczytem stosu, czyli górną linią (z wyłączeniem tekstu wiadomości). Zejście ze stosu cofa się w czasie. Druga linia to metoda, która wywołuje pierwszą linię itd.
Jeśli korzystasz z oprogramowania typu open source, może być konieczne pobranie i dołączenie do projektu źródeł, jeśli chcesz je zbadać. Pobierz słoiki źródłowe, w swoim projekcie otwórz folder Biblioteki referencyjne, aby znaleźć swój jar dla modułu open source (ten z plikami klas), a następnie kliknij prawym przyciskiem myszy, wybierz Właściwości i dołącz słoik źródłowy.