Що таке слід стека, і як я можу використовувати його для налагодження помилок у програмі?


644

Іноді, коли я запускаю свою програму, це дає мені помилку, яка виглядає так:

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
Крім того, якщо рядок стеження не містить імені файлу та номера рядка, клас для цього рядка не був складений з інформацією про налагодження.
Thorbjørn Ravn Andersen

Відповіді:


589

Простіше кажучи, трасування стека - це список викликів методу, що додаток було посеред, коли було викинуто виняток.

Простий приклад

На прикладі, наведеному у запитанні, ми можемо точно визначити, де виключення було кинуто в додатку. Давайте подивимося на слід стека:

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 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 додав "Suppression:" - який перераховує придушені сліди стека перед тим, як відображати "Викликано:" для цього винятку. Він автоматично використовується конструктор-спробу-з-ресурсом: docs.oracle.com/javase/specs/jls/se8/html/… і містить винятки, якщо такі були кинуті під час закриття ресурсів.
dhblah

Існує JEP openjdk.java.net/jeps/8220715, який має на меті покращити зрозумілість особливо NPE, надаючи деталі, такі як "Неможливо записати поле" nullInstanceField ", оскільки" this.nullInstanceField "недійсний".
Mahatma_Fatal_Error

80

Я публікую цю відповідь, тому найвища відповідь (при сортуванні за діяльністю) - це не та, яка просто неправильна.

Що таке стек-трек?

Склад - дуже корисний інструмент налагодження. Він показує стек викликів (тобто, стек функцій, які були викликані до цього моменту) у момент, коли було викинуто невиконане виняток (або час, коли стек-трек генерувався вручну). Це дуже корисно, оскільки воно не тільки показує, де сталася помилка, а й те, як програма опинилася в тому місці коду. Це призводить до наступного питання:

Що таке виняток?

Виняток - це те, що середовище виконання використовується для того, щоб повідомити про помилку. Популярні приклади - 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.

Як я маю поводитися зі стек-трасами / винятками?

Спочатку з’ясуйте, що викликає Виняток. Спробуйте googleing ім'я виключення, щоб дізнатися, що є причиною цього винятку. Більшу частину часу це буде викликано неправильним кодом. У наведених вище прикладах усі винятки спричинені неправильним кодом. Отже, для прикладу NullPointerException ви можете переконатися, що aвін ніколи не є нульовим на той час. Ви можете, наприклад, ініціалізувати aабо включити чек, як цей:

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

Таким чином, лінія порушника не виконується, якщо a==null. Те саме стосується інших прикладів.

Іноді ви не можете переконатися, що не отримаєте винятку. Наприклад, якщо ви використовуєте мережеве з'єднання у своїй програмі, ви не можете перешкодити комп’ютеру втратити з'єднання з Інтернетом (наприклад, ви не можете зупинити користувача від відключення мережевого з'єднання комп'ютера). У цьому випадку мережева бібліотека, ймовірно, кине виняток. Тепер ви повинні зловити виняток і впоратися з ним. Це означає, що у прикладі з мережевим підключенням слід спробувати відновити з'єднання або сповістити користувача або щось подібне. Крім того, щоразу, коли ви використовуєте улов, завжди ловіть лише той виняток, який ви хочете зловити, не використовуйте широкі висловлювання про улов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. це неможливо, виберіть конкретний виняток і обробіть його.

    • Ніколи не додайте спробу / лову, а потім просто ігноруйте виняток! Не робіть цього!
    • Ніколи не використовуйте catch (Exception e), завжди ловіть конкретні винятки. Це позбавить вас від багатьох головних болів.

1
приємне пояснення того, чому нам слід уникати маскування помилок
Судіп Бхандарі

2
Я публікую цю відповідь, тож найвища відповідь (коли відсортовано за діяльністю) - це не та, яка просто невірна. Я не маю уявлення про те, про який ви говорите, оскільки це, мабуть, зараз змінилося. Але прийнята відповідь, безумовно, більше
заважає

1
Те, що я мав на увазі, було видалено на сьогодні, наскільки я знаю. В основному сказано, "просто поставте спробу {} catch (Виняток 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
Що стосується String, якщо ви хочете використовувати метод 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 у боттем замість помилки syndax, яка може бути просто посередині стека.

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

Ключ 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 в Eclipse . Якщо ви використовуєте інший IDE, можливо, є подібна функція, але ця відповідь стосується Eclipse.

По-перше, переконайтеся, що у вашому проекті Eclipse доступні всі джерела Java.

Потім в перспективі Java натисніть на вкладку Консоль (зазвичай внизу). Якщо подання консолі не видно, перейдіть до пункту меню Вікно -> Показати перегляд та виберіть Консоль .

Потім у вікні консолі натисніть на наступну кнопку (справа)

Кнопка консолей

а потім виберіть зі спадного списку консоль слідування Java Stack Trace .

Вставте слід стека в консоль. Потім він надасть список посилань на ваш вихідний код та будь-який інший наявний вихідний код.

Це те, що ви можете бачити (зображення з документації Eclipse):

Діаграма з документації Eclipse

Останнім викликом методу буде верхня частина стека, яка є верхньою лінією (виключаючи текст повідомлення). Спускання стека йде назад у часі. Другий рядок - це метод, який викликає перший рядок тощо.

Якщо ви використовуєте програмне забезпечення з відкритим кодом, можливо, вам доведеться завантажити та долучити до свого проекту джерела, якщо ви хочете вивчити. Завантажте джерельні банки в своєму проекті, відкрийте папку " Посилання з бібліотеками", щоб знайти ваш jar для вашого модуля з відкритим кодом (той, який має файли класу), потім клацніть правою кнопкою миші, виберіть " Властивості" та приєднайте джерельну банку.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.