Co to jest ślad stosu i jak mogę go użyć do debugowania błędów aplikacji?

Oct 21 2010

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

623 RobHruska Oct 21 2010 at 21:52

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.javai 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 nullw 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 22of, Book.javaaby zobaczyć, co może spowodować NullPointerExceptiontutaj.

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.myprojectpakiecie. 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.javaon-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).

82 Dakkaron Oct 14 2015 at 17:40

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 aw tym czasie nigdy nie jest null. Możesz na przykład zainicjować alub 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ć ArithmeticExceptionspowodowane przez możliwe dzielenie przez 0. Ale wyłapuje również możliwe, NullPointerExceptionktóre jest wyrzucane, jeśli alub bnull. Oznacza to, że możesz otrzymać a, NullPointerExceptionale 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

  1. Dowiedz się, co jest przyczyną wyjątku i napraw go, aby w ogóle nie zgłaszał wyjątku.
  2. 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.
21 Woot4Moo Oct 21 2010 at 22:05

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 firstNamezawiera wartość, zrobilibyśmy to w następujący sposób:if(firstName == null || firstName.equals("")) return;

Powyższe uniemożliwia nam użycie firstNamejako 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ł.

15 przemekhertel Sep 17 2014 at 00:34

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)
15 KevinLi Jul 26 2016 at 09:24

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.

8 EugeneS Apr 19 2016 at 10:43

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)
6 rghome Mar 12 2015 at 16:34

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.