Что такое трассировка стека и как я могу использовать ее для устранения ошибок приложения?


643

Иногда, когда я запускаю свое приложение, оно выдает мне ошибку, которая выглядит следующим образом:

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)

Люди называют это «следом стека». Что такое трассировка стека? Что он может сказать мне об ошибке, которая происходит в моей программе?


Об этом вопросе - довольно часто я вижу вопрос, когда начинающий программист «получает ошибку», и они просто вставляют свою трассировку стека и некоторый случайный блок кода, не понимая, что такое трассировка стека или как они могут ее использовать. Этот вопрос предназначен для начинающих программистов, которым может понадобиться помощь в понимании значения трассировки стека.


25
Кроме того, если строка трассировки стека не содержит имя файла и номер строки, класс для этой строки не был скомпилирован с отладочной информацией.
Торбьерн Равн Андерсен

Ответы:


590

Проще говоря, трассировка стека - это список вызовов методов, которые приложение находилось в середине, когда было сгенерировано исключение.

Простой пример

С помощью примера, приведенного в вопросе, мы можем точно определить, где было выброшено исключение в приложении. Давайте посмотрим на трассировку стека:

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)

Это очень простая трассировка стека. Если мы начнем с начала списка «at ...», мы можем сказать, где произошла наша ошибка. Мы ищем самый верхний вызов метода, который является частью нашего приложения. В данном случае это:

at com.example.myproject.Book.getTitle(Book.java:16)

Чтобы отладить это, мы можем открыть Book.javaи посмотреть на строку 16, которая:

15   public String getTitle() {
16      System.out.println(title.toString());
17      return title;
18   }

Это будет означать, что что-то (вероятно title) находится nullв приведенном выше коде.

Пример с цепочкой исключений

Иногда приложения перехватывают исключение и повторно генерируют его как причину другого исключения. Это обычно выглядит так:

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   }

Это может дать вам трассировку стека, которая выглядит следующим образом:

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

То, что отличается от этого, это «Причины». Иногда исключения будут иметь несколько разделов «Причины». Для них обычно требуется найти «основную причину», которая будет одной из самых низких «причинных» секций в трассировке стека. В нашем случае это:

Caused by: java.lang.NullPointerException <-- root cause
        at com.example.myproject.Book.getId(Book.java:22) <-- important line

Опять же , с этим исключением мы хотели бы посмотреть на линию 22из , Book.javaчтобы увидеть , что может привести NullPointerExceptionздесь.

Более сложный пример с библиотечным кодом

Обычно трассировки стека намного сложнее, чем два приведенных выше примера. Вот пример (он длинный, но демонстрирует несколько уровней связанных исключений):

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

В этом примере намного больше. Что нас больше всего беспокоит, так это поиск методов, взятых из нашего кода , которые будут в com.example.myprojectпакете. Из второго примера (выше), мы сначала хотели бы найти первопричину, а именно:

Caused by: java.sql.SQLException

Однако все вызовы методов в соответствии с этим являются библиотечным кодом. Итак, мы перейдем к «Причины» над ним и поищем первый вызов метода, происходящий из нашего кода, а именно:

at com.example.myproject.MyEntityService.save(MyEntityService.java:59)

Как и в предыдущих примерах, мы должны смотреть MyEntityService.javaонлайн 59, потому что именно здесь возникла эта ошибка (эта ошибка немного очевидна, что пошло не так, поскольку SQLException сообщает об ошибке, но мы ищем процедуру отладки).


3
@RobHruska - Очень хорошо объяснил. +1. Знаете ли вы какие-либо парсеры, которые принимают трассировку исключений в виде строки и предоставляют полезные методы для анализа трассировки стека? - как getLastCausedBy () или getCausedByForMyAppCode ("com.example.myproject")
Энди Дюфрен

1
@AndyDufresne - я не сталкивался ни с кем, но опять же, я действительно не смотрел, либо.
Роб Хруска,

1
Предлагаемое улучшение: объясните первую строку трассировки стека, которая начинается с Exception in thread "main"вашего первого примера. Я думаю, что было бы особенно полезно объяснить, что эта строка часто сопровождается сообщением, таким как значение переменной, которое может помочь диагностировать проблему. Я попытался внести изменения самостоятельно, но я изо всех сил стараюсь вписать эти идеи в существующую структуру вашего ответа.
Код-ученик

5
Также в Java 1.7 добавлено «Подавлено:» - в котором перечислены трассировки стеков исключенных исключений перед отображением «Причины:» для этого исключения. Он автоматически используется конструкцией try-with-resource: docs.oracle.com/javase/specs/jls/se8/html/… и содержит исключения, если они были сгенерированы во время закрытия ресурса (-ов).
dhblah

Существует JEP openjdk.java.net/jeps/8220715 , целью которого является дальнейшее улучшение понятности особенно NPE путем предоставления таких деталей, как «Невозможно записать поле« nullInstanceField », потому что this.nullInstanceField является нулевым».
Mahatma_Fatal_Error

80

Я публикую этот ответ, поэтому самый верхний ответ (при сортировке по активности) не является просто ошибочным.

Что такое Stacktrace?

Stacktrace - очень полезный инструмент отладки. Он показывает стек вызовов (т. Е. Стек функций, которые были вызваны до этой точки) в момент возникновения неперехваченного исключения (или в момент, когда трассировка стека была сгенерирована вручную). Это очень полезно, поскольку показывает не только, где произошла ошибка, но и то, как программа оказалась в этом месте кода. Это приводит к следующему вопросу:

Что такое исключение?

Исключением является то, что среда выполнения использует, чтобы сообщить вам, что произошла ошибка. Популярные примеры: NullPointerException, IndexOutOfBoundsException или ArithmeticException. Каждый из них вызывается, когда вы пытаетесь сделать что-то, что невозможно. Например, исключение NullPointerException будет выдано при попытке разыменования объекта 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.

Как я должен иметь дело со стеками / исключениями?

Сначала выясните, что вызывает исключение. Попробуйте поискать название исключения, чтобы выяснить причину этого исключения. В большинстве случаев это будет вызвано неправильным кодом. В приведенных выше примерах все исключения вызваны неправильным кодом. Так что для примера NullPointerException вы можете убедиться, что aон никогда не будет нулевым в то время. Вы можете, например, инициализировать aили включить проверку, подобную этой:

if (a!=null) {
    a.toString();
}

Таким образом, оскорбительная строка не выполняется, если a==null. То же самое касается других примеров.

Иногда вы не можете быть уверены, что не получите исключения. Например, если вы используете сетевое соединение в своей программе, вы не можете остановить компьютер от потери его интернет-соединения (например, вы не можете запретить пользователю отключать сетевое соединение компьютера). В этом случае сетевая библиотека, вероятно, выдаст исключение. Теперь вы должны поймать исключение и обработать его. Это означает, что в примере с сетевым подключением вы должны попытаться повторно открыть подключение или уведомить пользователя или что-то в этом роде. Кроме того, всякий раз, когда вы используете catch, всегда перехватываете только исключение, которое вы хотите перехватить, не используйте такие широкие операторы catch, какcatch (Exception e)что бы поймать все исключения. Это очень важно, потому что в противном случае вы можете случайно поймать не то исключение и отреагировать неправильно.

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!")
}

Почему я не должен использовать catch (Exception e)?

Давайте используем небольшой пример, чтобы показать, почему вы не должны просто перехватывать все исключения:

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;
    }
}

Этот код пытается отловить ArithmeticExceptionпричину, вызванную возможным делением, на 0. Но он также отлавливает возможное NullPointerException, которое выбрасывается, если aили bесть null. Это означает, что вы можете получить, NullPointerExceptionно вы будете относиться к нему как к ArithmeticException и, вероятно, будете поступать неправильно. В лучшем случае вы все еще скучаете по тому, что было исключение NullPointerException. Такие вещи делают отладку намного сложнее, так что не делайте этого.

TLDR

  1. Выясните причину исключения и устраните его, чтобы исключение вообще не генерировалось.
  2. Если 1. невозможно, перехватите конкретное исключение и обработайте его.

    • Никогда не добавляйте try / catch, а затем просто игнорируйте исключение! Не делай этого!
    • Никогда не используйте catch (Exception e), всегда ловите определенные исключения. Это избавит вас от многих головных болей.

1
хорошее объяснение того, почему мы должны избегать маскировки ошибок
Судип Бхандари

2
Я публикую этот ответ, поэтому самый верхний ответ (при сортировке по активности) - это не просто неправильный ответ. Я понятия не имею, о каком из них вы говорите, поскольку это, вероятно, уже изменилось. Но принятый ответ определенно более интересен;)
AxelH

1
Насколько я знаю, тот, который я имел в виду, уже удален. В основном говорится: «просто вставьте try {} catch (Exception e) {} и игнорируйте все ошибки»). Принятый ответ намного старше моего ответа, поэтому я стремился дать немного иной взгляд на этот вопрос. Я не думаю, что это помогает кому-то просто копировать чужой ответ или освещать то, что уже хорошо освещали другие люди.
Даккарон

Вводить в заблуждение фразу «Не ловить исключение» - это только один вариант использования. Ваш пример великолепен, но как насчет того, где вы находитесь в верхней части вашего цикла потока (внутри прогона)? Вы должны ВСЕГДА ловить исключение (или, может быть, Throwable) там и регистрировать его, чтобы оно не исчезло незаметно (исключения, генерируемые при запуске, обычно не регистрируются правильно, если вы не настроили для этого поток / регистратор).
Билл К

1
Я не включил этот особый случай, поскольку он имеет значение только для многопоточности. В единственном потоке пропущенное исключение убивает программу и регистрируется явно. Если кто-то не знает, как правильно обрабатывать исключения, он обычно тоже не знает, как использовать многопоточность.
Даккарон

21

Чтобы добавить к тому, что Роб упомянул. Установка точек останова в вашем приложении позволяет выполнять пошаговую обработку стека. Это позволяет разработчику использовать отладчик, чтобы увидеть, в какой момент метод делает что-то непредвиденное.

Поскольку Роб использовал NullPointerException(NPE) для иллюстрации чего-то общего, мы можем помочь устранить эту проблему следующим образом:

если у нас есть метод, который принимает параметры, такие как: void (String firstName)

В нашем коде мы хотели бы оценить, что firstNameсодержит значение, мы сделали бы это так:if(firstName == null || firstName.equals("")) return;

Вышесказанное мешает нам использовать firstNameв качестве небезопасного параметра. Поэтому, выполняя нулевые проверки перед обработкой, мы можем помочь гарантировать, что наш код будет работать правильно. Чтобы расширить пример, который использует объект с методами, мы можем посмотреть здесь:

if(dog == null || dog.firstName == null) return;

Выше приведен правильный порядок проверки на нулевые значения, мы начинаем с базового объекта, в данном случае с собакой, а затем начинаем идти по дереву возможностей, чтобы убедиться, что все верно перед обработкой. Если бы порядок был отменен, NPE потенциально мог бы быть брошен, и наша программа потерпела бы крах.


Согласовано. Этот подход может быть использован, чтобы выяснить, какая ссылка в утверждении, например, nullкогда NullPointerExceptionпроверяется.
Роб Хруска

16
При работе со строкой, если вы хотите использовать метод equals, я думаю, что лучше использовать константу в левой части сравнения, например: вместо: if (firstName == null || firstName.equals ("" )) возвращение; Я всегда использую: if ((""). Equals (firstName)) Это предотвращает исключение Nullpointer
Torres

15

Существует еще одна функция трассировки стека, предлагаемая семейством Throwable - возможность манипулировать информацией трассировки стека.

Стандартное поведение:

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();
    }
}

Трассировки стека:

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)

Трассировка манипулируемого стека:

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();
    }
}

Трассировки стека:

Exception in thread "main" java.lang.RuntimeException
    at OtherClass.methodX(String.java:99)
    at OtherClass.methodY(String.java:55)

2
Я не знаю, что я чувствую по этому поводу ... учитывая природу потока, я бы посоветовал новым разработчикам не определять собственную трассировку стека.
PeonProgrammer

15

Чтобы понять имя : трассировка стека - это список Исключений (или вы можете сказать список «Причины»), от самого поверхностного Исключения (например, Исключение сервисного уровня) до самого глубокого (например, Исключение базы данных). Точно так же, как причина, по которой мы называем это «стеком», заключается в том, что стек является первым в последнем выходе (FILO), самое глубокое исключение произошло в самом начале, затем была создана цепочка исключений с серией последствий, исключение на поверхности было последним одно произошло вовремя, но мы видим это в первую очередь.

Ключ 1 : хитрая и важная вещь, которую необходимо здесь понять: самая глубокая причина не может быть «первопричиной», потому что если вы пишете какой-то «плохой код», это может вызвать некое исключение под ним, которое глубже, чем его слой. Например, неверный SQL-запрос может привести к сбросу соединения SQLServerException в нижней части окна, а не к ошибке sinax, которая может быть только в середине стека.

-> Найти основную причину в середине ваша работа. введите описание изображения здесь

Ключ 2 : Еще одна хитрая, но важная вещь находится внутри каждого блока «Причинить», первая строка была самым глубоким слоем и была первой для этого блока. Например,

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 был вызван Auther.java:25, который был вызван Bootstrap.java:14, Book.java:16 был основной причиной. Здесь прикрепите диаграмму сортировки стека трасс в хронологическом порядке. введите описание изображения здесь


8

Просто чтобы добавить к другим примерам, есть внутренние (вложенные) классы, которые появляются со $знаком. Например:

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();
    }
}

Результатом будет следующая трассировка стека:

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)

5

В других статьях описывается, что такое трассировка стека, но с ней все еще сложно работать.

Если вы получаете трассировку стека и хотите отследить причину исключения, хорошей отправной точкой для понимания этого является использование консоли Java Stack Trace Console в Eclipse . Если вы используете другую IDE, может быть похожая функция, но этот ответ касается Eclipse.

Во-первых, убедитесь, что у вас есть все источники Java, доступные в проекте Eclipse.

Затем в перспективе Java нажмите на вкладку Консоль (обычно внизу). Если вид консоли не отображается, перейдите к пункту меню « Окно» -> «Показать вид» и выберите « Консоль» .

Затем в окне консоли нажмите на следующую кнопку (справа)

Кнопка консоли

а затем выберите консоль Java Stack Trace из раскрывающегося списка.

Вставьте трассировку стека в консоль. Затем он предоставит список ссылок на ваш исходный код и любой другой доступный исходный код.

Вот что вы можете увидеть (изображение из документации по Eclipse):

Диаграмма из документации по Eclipse

Самый последний вызов метода будет вершиной стека, которая является верхней строкой (исключая текст сообщения). Спуск по стеку уходит в прошлое. Вторая строка - это метод, который вызывает первую строку и т. Д.

Если вы используете программное обеспечение с открытым исходным кодом, вам может потребоваться загрузить и прикрепить к своему проекту источники, если вы хотите проверить. Загрузите jar- файлы с исходным кодом, в своем проекте откройте папку Referenced Libraries, чтобы найти jar-файл для вашего модуля с открытым исходным кодом (тот, который содержит файлы классов), затем щелкните правой кнопкой мыши, выберите Properties и прикрепите jar- файл с исходным кодом.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.