Съдържание на курса
Урок 6 — Проект с Maven
От Dorian Chávez · основател на Hábil и архитект на интеграции ·
Време: 90 мин (точно колкото трябва: включва инсталирането на Maven, а при първото изграждане се свалят части от интернет; не бързай)
Какво изграждаш: revisor престава да бъде папка с разхвърляни файлове и се превръща в проект на Maven, с пакети, който се компилира, пакетира се в .jar и се изпълнява винаги със същите две команди
Какво научаваш: кога един отделен файл вече не стига; пакети и видимост; завръщането на public static void main(String[] args); инсталиране на Maven 3.10.0 със SDKMAN; стандартната структура на Maven и pom.xml; цикълът compile, test, package, verify; какво има вътре в един .jar и защо още не се изпълнява с java -jar
Откъде идваш. Носиш проекта от Урок 5: папка с няколко файла .java (моделът, класификацията, форматът, отчетът, четенето на servicios.txt със собствените му изключения), която се изпълнява с java Revisor.java и отнема около 2.5 секунди, защото симулацията изчаква толкова, колкото казва всяка услуга. Работи и го стартираш с една-единствена команда. Но тази команда има граници, които ще проличат веднага щом програмата поиска нещо, което не си написал ти: следващият урок изисква библиотека за тестове, а по-следващият — библиотека за четене на JSON. Стартерът на файлове с изходен код (source-file launcher) не умее да ползва чужди библиотеки, не запазва нищо от това, което компилира, и няма разпознаваема структура. Днес превръщаш revisor в истински проект: със структурата, която разпознава всеки Java програмист, и с инструмент, който го компилира, пакетира и — от следващия урок — го тества. Работиш в папката си ~/curso-java/revisor/.
Какво не прави този урок. Още не пише тестове: това е Урок 7, който се опира на това, което изграждаш днес (pom.xml, папката за тестове, която днес остава празна, и жизнения цикъл на Maven). Не използва никаква мрежа: Simulador продължава да играе ролята на услугите и това не се променя до Урок 8. Не добавя и библиотеки към програмата (първата зависимост на програмата идва в 8) и не проверява услугите едновременно (това е Урок 9). Тук се решава как се организира, компилира и пакетира Java програма, която вече има повече от десет файла.
За стартиранията. Всичко в този урок е изпълнено с Temurin 25 и Maven 3.10.0, под macOS, с терминал в UTF-8; инсталирането на Maven със SDKMAN и неговото mvn --version са измерени допълнително и на Linux Mint 22.3, на 13 октомври 2026 г. Изходите на Maven са дълги и се менят при всяко стартиране (сваляния, времена, пътища), затова се показват съкратено: запазва се това, което анализира всеки раздел, а трите точки (...) отбелязват какво е изрязано; дългите пътища се показват от ~/curso-java нататък. Съобщенията на стартера на Java (онези, които започват с Error:) излизат на езика на системата: тук се показват на английски, а ако твоят Linux Mint е на испански, първият ред ще го видиш преведен; останалото не се променя. Програмите са в programas/06-proyecto-maven/, в хранилището на курса.
След урока ще можеш да
- Обясниш кои три неща не прави стартерът на файлове с изходен код и какво добавя към програмата ти един проект на Maven.
- Декларираш пакет, поставиш файла в папката, която му съответства, и кажеш какви класове вижда всеки клас според видимостта им.
- Инсталираш Maven 3.10.0 със SDKMAN и разпознаеш в
mvn --version, че ползва твоя Temurin 25. - Прочетеш
pom.xmlи кажеш за какво служи всеки елемент, включително защо се фиксира версията на всеки плъгин (plugin). - Кажеш кои фази изпълнява
mvn -B verify, какво оставя всяка от тях вtarget/и какво има вътре в.jar. - Преместиш
revisorв структурата на Maven и го изпълниш отtarget/classesи от неговия.jar.
Защо, преди как
До миналия урок инструментът ти за изграждане беше самият java: подаваш му един файл .java, той го компилира в паметта заедно с останалите от папката и го изпълнява. За учене е идеален. За програма, която ще расте, има три ограничения и трите се решават със същия инструмент:
- Не може да ползва чужди библиотеки. Стартерът от Урок 1 (
java Revisor.java) компилира твоите файлове и нищо повече. Следващият урок изисква библиотека за тестове, а Урок 8 — библиотека за четене на JSON; без начин да се декларират зависимости няма път напред. - Не оставя нищо. Компилира при всяко изпълнение и не запазва резултата: не съществува „компилирана програма“, която да предадеш, нито начин да кажеш „компилирай, но още не изпълнявай“. Няма и къде да се поставят тестове, нито кой да ги стартира.
- Няма форма. Когато програмата има десет файла, трима души и две години, „папка с файлове“ престава да бъде структура. Всички Java проекти по света споделят една конвенция за папките и именно тя позволява на един инструмент или на нов човек да ги разбере, без никой да му ги обяснява.
Инструментът е Maven, който компилира, тества и пакетира Java проекти по файл, който ги описва. Инсталира се веднъж, а оттам всеки Java проект, който ползва Maven, се изгражда със същата команда. За да го ползваш, трябват две предварителни стъпки, които урокът обяснява по ред: да подредиш кода в пакети (начинът на Java да подрежда класовете си по име и по папка) и да научиш какви класове вижда всеки от останалите (видимостта).
Урокът има четири части: пакети и видимост (6.1); Maven: инсталиране, структура, pom.xml, жизнен цикъл и преместването на revisor (6.2); пълният цикъл и .jar (6.3); и стъпката на проекта (6.4). С тази цел пред очи, ето какво ще видиш накрая, когато изградиш целия проект:
$ mvn -B verify
...
[INFO] --- compiler:3.16.0:compile (default-compile) @ revisor ---
[INFO] Compiling 13 source files with javac [debug release 25] to target/classes
...
[INFO] --- surefire:3.6.0:test (default-test) @ revisor ---
[INFO] No tests to run.
[INFO]
[INFO] --- jar:3.5.1:jar (default-jar) @ revisor ---
[INFO] Building jar: ~/curso-java/revisor/target/revisor-0.6.0.jar
[INFO] BUILD SUCCESS
Тринайсет компилирани файла, един изграден .jar и един ред, който минава незабелязано: No tests to run., „няма тестове за изпълнение“. Накрая ще знаеш откъде идва всеки ред, какво остана вътре в .jar и защо този BUILD SUCCESS с нула тестове е точно въпросът, с който започва Урок 7.
Понятията
6.1 Пакети и видимост
6.1.1 Какво не прави стартерът
Стартерът на файлове с изходен код (java Revisor.java) е удобство, което Java добави, за да не изисква ученето инструмент за изграждане. Компилира в паметта, при всяко изпълнение, файловете, от които програмата се нуждае, изпълнява резултата и не оставя нищо на диска (JEP 458, окончателен от Java 22). Идеален е за всичко, което направи досега, и има три ограничения, които вече тежат. Първото може да се провокира с два реда. Напиши файл, който ползва JUnit, библиотеката за тестове, която ще използваш в Урок 7:
// fig06_01.java
import org.junit.jupiter.api.Test;
void main() {
IO.println("revisor: todavía no revisa nada");
}
Това е обичайната програма с един ред повече: import на клас от JUnit. Един import (познаваш го от файловете, които ползваха java.util) казва на компилатора от кой пакет идва едно име. Изпълни го:
$ java fig06_01.java
fig06_01.java:2: error: package org.junit.jupiter.api does not exist
import org.junit.jupiter.api.Test;
^
1 error
error: compilation failed
Компилаторът търси пакета org.junit.jupiter.api и не го намира, защото стартерът не знае откъде да го вземе: няма никакво място, където да декларираш, че програмата ти зависи от библиотека. Когато видиш package ... does not exist с име, което не е твое, почти винаги означава това: липсва зависимост, не е правописна грешка.
Второто ограничение не дава съобщение, а навик: тъй като стартерът компилира всичко при всяко изпълнение и не запазва нищо, „компилираното“ не съществува като продукт. Няма .class, който да предадеш, нито начин да кажеш „компилирай, но още не изпълнявай“. При тринайсет файла цената във време е част от секундата (измерено: около 0.3 секунди на тази машина), но цената в ред расте с проекта. Третото ограничение е това на тестовете: стартерът не знае какво е тест, нито къде живее, нито как се стартират всички заедно.
Преди Maven има още едно нещо, което Java изисква и което проектът ти още няма: пакетите.
6.1.2 Пакети: име и папка
Представи си, че двама души пишат, всеки за себе си, клас на име Servicio. Ако двата попаднат в една и съща програма, компилаторът не може да разбере кой кой е. Проблемът е стар и решението на Java също: всеки клас живее в пакет, който е име с точки — mx.habil.revisor — и е част от пълното име на класа. Твоят Servicio става mx.habil.revisor.Servicio, а този на другия човек — com.otraempresa.inventario.Servicio. В JDK е същото: съществува java.util.List и съществува java.awt.List, две различни неща със същото късо име.
Конвенцията за избор на име е да се ползва домейнът на този, който пише кода, наобратно (habil.mx става mx.habil), следван от името на проекта (revisor). Домейнът е уникален в света, така че и името е. Правилата на езика са три и трите са в спецификацията, глава 7:
- Файлът декларира пакета си на първия ред (без да се броят коментарите):
package mx.habil.revisor;. - Папката има същото име като пакета, с една подпапка за всяка точка: файлът живее в
mx/habil/revisor/. - За да се ползва клас от друг пакет, той се импортира:
import mx.habil.revisor.Estado;. Класовете от същия пакет се виждат един друг безimport, а тези отjava.lang(катоString) — винаги.
Да го видим с най-малката програма, която може да се напише в пакет. Това е клас Hola с main, който отпечатва пълното име на собствения си клас:
// fig06_02/src/mx/habil/revisor/Hola.java
package mx.habil.revisor;
/** Una clase mínima dentro de un paquete. */
public final class Hola {
private Hola() {}
/** Imprime el nombre completo de su propia clase. */
public static void main(String[] args) {
System.out.println("hola desde " + Hola.class.getName());
}
}
Три неща са нови. Първото е package. Второто е, че този клас вече не е компактен файл: има своя class и пълен main, public static void main(String[] args), който раздел 6.1.4 обяснява. Третото, Hola.class.getName(), пита Java за името на класа така, както го знае; тук служи да видим, че пакетът е част от него. (Коментарите между /** и */ са документация; обясняват се в 6.1.3.)
Компилирането и изпълнението на файл с пакет вече не е java Hola.java. Става на две стъпки и в двете има по една подробност:
$ cd fig06_02
$ javac -d out src/mx/habil/revisor/Hola.java
$ java -cp out mx.habil.revisor.Hola
hola desde mx.habil.revisor.Hola
javac -d out казва „остави .class файловете в папка out“, а javac прави нещо, което не се вижда: създава подпапките на пакета (out/mx/habil/revisor/Hola.class). .class файловете остават в папка със същата форма като тази на кода. При изпълнението -cp out е съкращение от classpath, „път към класовете“: списъкът от места, където JVM търси .class файловете. С това JVM знае откъде да започне да търси, а mx.habil.revisor.Hola е пълното име на класа, с неговия пакет: JVM го превръща в пътя mx/habil/revisor/Hola.class и го търси вътре в out.
Сега двете грешки, които всички правят първия път. Първата е да напишеш само късото име:
$ java -cp out Hola
Error: Could not find or load main class Hola
Caused by: java.lang.ClassNotFoundException: Hola
JVM търси Hola.class директно в out, не го намира (то е две нива по-надолу) и го съобщава с изключение с дълго име, ClassNotFoundException: „няма клас на име Hola“. Решението е да напишеш пълното име. Втората грешка е да „влезеш в папката на класа“ и да изпълниш оттам, мислейки си, че щом стоиш до файла, ще го намери:
$ cd out/mx/habil/revisor
$ java Hola
Error: Could not find or load main class Hola
Caused by: java.lang.NoClassDefFoundError: Hola (wrong name: mx/habil/revisor/Hola)
Тук вече намира Hola.class, но истинското му име е mx/habil/revisor/Hola, а не Hola, и JVM го отхвърля с (wrong name: mx/habil/revisor/Hola): „файлът казва, че класът се казва другояче“. Това е същото правило, видяно от другата страна. Името на класа и пътят на файла му трябва да съвпадат и JVM го проверява. Затова програмите с пакети се изпълняват винаги от корена на класовете (out, или target/classes в Maven), с пълното име.
Остава един въпрос, който компилаторът има право да ти зададе: какво става, ако файлът декларира един пакет, а е в друга папка? Отговорът е по-малко драматичен, отколкото изглежда, и е добре да се знае такъв, какъвто е. Clasificador.java беше преместен една папка по-нагоре от мястото му, като редът му package mx.habil.revisor; остана непокътнат, и на javac му беше поискано да компилира Revision.java, като търси останалите класове по папки:
$ javac -sourcepath src/main/java -d out src/main/java/mx/habil/revisor/Revision.java
src/main/java/mx/habil/revisor/Revision.java:14: error: cannot find symbol
new Resultado(servicio, Clasificador.clasificar(respuesta), respuesta);
^
symbol: variable Clasificador
location: class Revision
1 error
Компилаторът излиза да търси mx/habil/revisor/Clasificador.java, защото Revision го споменава, не го намира там, където името на пакета казва, че трябва да е, и заключава, че не съществува. Изненадващото е обратното: когато му се подадат изрично всички файлове, javac не се оплаква, а Maven го прави точно така (измерено: проектът се компилира и .class файловете остават там, където трябва, въпреки че .java е в грешната папка). Папката, която лъже, не чупи компилирането на Maven; чупи всичко, което търси по папки: редакторите, търсенията, инкременталното компилиране и човека, който дойде след теб. Това е тиха грешка, от най-лошите: правилото е папката винаги да казва истината, дори компилаторът днес да те остави да лъжеш.
Има една последна грешка от това семейство — на файла, който не се казва като публичния си клас. Файлът Revisor.java в този пример декларира клас с друго име:
package mx.habil.revisor;
public final class Principal {}
$ javac -d out mx/habil/revisor/Revisor.java
mx/habil/revisor/Revisor.java:3: error: class Principal is public, should be declared in a file named Principal.java
public final class Principal {}
^
1 error
Един public клас трябва да живее във файл със същото име. Това е още едно съвпадение, което компилаторът наистина изисква. В компактните файлове от първите уроци не се забелязваше, защото имплицитният клас взема името на файла (fig02_04.java дефинира клас fig02_04).
6.1.3 Видимост: какво вижда всеки клас от останалите
С пакетите се появява въпрос, който преди нямаше смисъл: може ли клас от mx.habil.otro да ползва клас от mx.habil.revisor? Отговорът дава видимостта, която се пише с една дума в началото на декларацията. Java има четири нива, а курсът ползва три:
| Пише се | Кой я вижда | За какво се ползва |
|---|---|---|
private |
само класът, в който е декларирана | вътрешните подробности: поле, конструктор, който никой не бива да вика |
| (нищо) | всеки клас от същия пакет | почти целият проект: това е видимостта „на ниво пакет“ |
public |
всеки клас от всеки пакет | това, което се предлага на света: входната точка |
(Четвъртото, protected, смесва видимостта на ниво пакет с наследяването между класове; курсът не ползва наследяване и то не се появява.)
Правилото е: най-тясната видимост, която върши работа. Не е въпрос на стил. Всяко нещо public е обещание към този, който го ползва: ако утре го промениш или го махнеш, чупиш неговата работа. Клас на ниво пакет го обещаваш само на собствения си пакет и можеш да го промениш, без да предупреждаваш никого. Досега всичко беше на ниво пакет, без да го забелязваш: класовете ти нямаха никаква дума отпред. Сега, когато в света има повече от един пакет, компилаторът го налага. Два примерни файла: Estado на ниво пакет в mx.habil.revisor и клас от друг пакет, mx.habil.otro, който иска да го ползва.
package mx.habil.revisor;
enum Estado {
ARRIBA,
CAIDO
}
package mx.habil.otro;
import mx.habil.revisor.Estado;
class Intruso {
Estado estado = Estado.ARRIBA;
}
Когато ги компилираш заедно:
$ javac -d out src/mx/habil/revisor/Estado.java src/mx/habil/otro/Intruso.java
src/mx/habil/otro/Intruso.java:3: error: Estado is not public in mx.habil.revisor; cannot be accessed from outside package
import mx.habil.revisor.Estado;
^
src/mx/habil/otro/Intruso.java:6: error: Estado is not public in mx.habil.revisor; cannot be accessed from outside package
Estado estado = Estado.ARRIBA;
^
src/mx/habil/otro/Intruso.java:6: error: Estado is not public in mx.habil.revisor; cannot be accessed from outside package
Estado estado = Estado.ARRIBA;
^
3 errors
Това се случва, когато клас от mx.habil.otro се опита да ползва Estado с видимост на ниво пакет: Estado is not public in mx.habil.revisor; cannot be accessed from outside package. Съобщението е ясно и решението почти никога не е „да му сложиш public“: а да се запиташ дали онзи друг клас изобщо трябва да ползва Estado.
В revisor всичко е с видимост на ниво пакет, освен един клас: Revisor, входната точка, която е единственото, от което външен човек се нуждае. Така изглежда например Estado от твоя проект в новия му дом:
// fig06_03/src/main/java/mx/habil/revisor/Estado.java
package mx.habil.revisor;
enum Estado {
ARRIBA,
LENTO,
CAIDO
}
Това е същият enum от Урок 3, само с един ред повече: package най-горе. Няма никаква дума за видимост отпред, така че е на ниво пакет и го виждат всички класове на mx.habil.revisor и нито един отвън. На единственото място, където public наистина се появява (Revisor, в 6.2.6), е необходим и коментар за документация, написан между /** и */, който се нарича Javadoc: коментарът, който инструментите на Java (и редакторът ти) показват, когато някой попита „какво е това?“. Правилото на проекта от този урок нататък е, че всеки public клас и всеки public метод носи Javadoc от поне едно изречение, завършващо с точка; в Урок 10 един инструмент ще го проверява. Тъй като почти всичко е на ниво пакет, този задължителен Javadoc остава на минимум.
6.1.4 public static void main(String[] args): завръщането на дългата форма
В уроци 1 до 5 пишеше void main() и нищо повече. Сега Revisor се връща към формата, която си виждал във всеки чужд Java код: клас и вътре в него метод main с три думи повече:
public: JVM го вика отвън, така че трябва да е видим за всички.static: принадлежи на класа, не на обект. Може да се вика, без да е създаден никакъв.String[] args: списъкът с аргументи, които човекът е написал след името на програмата в командния ред (java ... Revisor uno dos). Днес не се ползва; Уроци 9 и 10 го ползват.
Справедливо е да се попиташ защо да се връщаме към по-дълга форма, щом Java 25 допуска void main(). Честният отговор има две части. Първата е, че късата форма продължава да работи в пакет: може да се провери и програмата върви. Втората е, че има условие, което Revisor не изпълнява, и грешката е поучителна. Клас, който само пази един main, няма причина да позволява някой да го инстанцира, затова му се слага private конструктор, което прави private Revisor() {}. Но main на екземпляр изисква JVM да може да създаде обекта. Този клас, в пакет, има късия main и частния конструктор:
package mx.x;
public final class Prueba {
private Prueba() {}
void main() {
IO.println("hola");
}
}
$ javac -d out mx/x/Prueba.java
$ java -cp out mx.x.Prueba
Error: no non-private zero argument constructor found in class mx.x.Prueba
remove private from existing constructor or define as:
public mx.x.Prueba()
no non-private zero argument constructor found: JVM търси конструктор без параметри, който не е частен, и няма такъв. Класическата форма не зависи от това, работи с всяка версия на JDK от първата и е тази, която разпознават всички инструменти и целият чужд код, който ще четеш. За програма, която ще живее години, това е добрата причина: да не зависиш от ново удобство, ако старата форма не струва нищо. IO.println също продължава да работи във всеки клас, но проектът ползва System.out, което вече познаваш от урок 5, защото това ще срещаш навсякъде.
6.2 Maven: инсталиране, структура и pom.xml
6.2.1 Какво прави Maven, в четири задачи
Maven е инструмент за изграждане: получава Java проект и прави с него, винаги в същия ред, това, което ти правеше на ръка. Задачите му са четири. Да компилира кода (вика javac вместо теб, с правилните опции). Да изпълнява тестовете и да спре, ако някой пропадне. Да пакетира резултата в архив .jar (компресиран архив от класове, подобен на .zip). И да осигурява зависимостите: когато проектът ти казва „нуждая се от библиотека X версия Y“, Maven я сваля от публично хранилище, Maven Central, запазва я на твоя компютър и я поставя в обсега на компилатора.
Онова, което прави Maven особен, е една идея с име: конвенция вместо конфигурация (convention over configuration). Вместо всеки проект да разказва в конфигурационен файл къде е кодът му, къде са тестовете му и къде оставя резултатите, Maven фиксира структура от папки, която ползват всички, а файлът на проекта казва само това, което излиза от обичайното. Следствието е онова, което има значение: който познава един проект на Maven, познава всички, а инструментите (редакторът, сървърът за непрекъсната интеграция, анализаторът на код) знаят къде да гледат, без да им се казва. Официалното въведение разказва същото за пет минути четене.
Maven не върши работата сам. Той е оркестратор: всяка задача се изпълнява от специализирана част, наречена плъгин (plugin) (компилаторът е един, онзи, който стартира тестовете, е друг, онзи, който сглобява .jar, е трети), а Maven решава кога кого да извика. Това разделение има практично следствие, което ще видиш в pom.xml: всеки плъгин има собствена версия.
6.2.2 Инсталиране на Maven 3.10.0 със SDKMAN
Maven се инсталира със същия инструмент, с който инсталира Java в Урок 1: SDKMAN. Както при Java, работи в личната ти папка, без sudo и без да пипа системните програми, и ти позволява да избереш точната версия. Курсът фиксира Maven 3.10.0, която страницата за сваляне на Maven отбелязва като текущата и препоръчана за всички версия (изисква JDK 8 или по-нов, за да се изпълнява; с твоя Temurin 25 е повече от достатъчно). Фиксирането на версията има значение по причина, която се вижда на екрана: измерено с Maven 3.9.11 върху Java 25, при стартиране отпечатва четири предупреждения WARNING: A terminally deprecated method in sun.misc.Unsafe has been called (идват от вътрешна библиотека на Maven, не от твоя код); 3.10.0 не ги отпечатва. Целта е твоят изход да е същият като този в урока.
Първо провери, че SDKMAN я предлага. sdk list maven показва наличните версии; на 13 октомври 2026 г., на Linux Mint 22.3, списъкът съдържаше 3.10.0 (и също версии 4.0.0-rc-…, кандидати за следващата главна версия, която още не е окончателна и която курсът не ползва). После я инсталирай с пълния ѝ номер:
$ sdk install maven 3.10.0
...
Done installing!
Setting maven 3.10.0 as default.
За разлика от Java, тук номера го пишеш ти точно така: 3.10.0 е версията на курса и не се променя, дори да излязат други. Последният ред казва, че я оставя по подразбиране. Ако вместо това SDKMAN те попита дали искаш тя да е по подразбиране, отговори „да“, с Enter или Y; а ако си отговорил „не“, поправяш го с sdk default maven 3.10.0. Сега, както в Урок 1, отвори нов терминал, за да вземе промените на SDKMAN, и провери:
$ mvn --version
Apache Maven 3.10.0 (c43a36b8d67be7e0805a411bc0898af1a51f5472)
Maven home: ~/.sdkman/candidates/maven/current
Java version: 25.0.3, vendor: Eclipse Adoptium, runtime: ~/.sdkman/candidates/java/25.0.3.0+9-tem
...
OS name: "linux", ... arch: "amd64"
Това е изходът, измерен на 13 октомври 2026 г. на Mint 22.3 (съкратен: трите точки отбелязват изрязаното, а пътищата са скъсени с ~; Maven ги отпечатва пълни, като /home/<твоят потребител>/.sdkman/candidates/maven/current). В твоя изход трябва да разпознаеш три неща. Apache Maven 3.10.0 на първия ред: версията на курса. Java version: 25.x, с кръпката, която имаш, и vendor: Eclipse Adoptium: Maven ползва твоя Temurin 25 (Adoptium е проектът, който публикува Temurin). И двата пътя, Maven home и runtime, вътре в ~/.sdkman/candidates/: двата инструмента идват от SDKMAN. Ако Java version казва нещо различно от 25, Maven ползва друг JDK: или терминалът е стар, или има променлива JAVA_HOME, която сочи другаде. SDKMAN поддържа тази променлива актуална, когато сменяш версията със sdk default или sdk use (карето от Урок 1, 1.1.7); провери я с echo $JAVA_HOME.
6.2.3 Стандартната структура от папки
С инсталиран Maven остава да му дадеш проект с формата, който очаква. Ето я, и е еднаква във всички проекти на Maven по света:
revisor/
pom.xml ← la descripción del proyecto
servicios.txt ← la configuración del revisor (sigue en la raíz)
src/
main/java/mx/habil/revisor/ ← el código del programa
test/java/mx/habil/revisor/ ← el código de las pruebas
target/ ← lo que Maven genera; se puede borrar entero
Всяка папка има една-единствена роля. src/main/java е кодът на програмата, с една подпапка за всяка точка от името на пакета, както в out преди малко. src/test/java е кодът на тестовете, със същата структура от пакети като програмата: тестът на Clasificador, който ще напишеш в Урок 7, ще бъде mx.habil.revisor.ClasificadorTest. Че тестовете живеят в същия пакет, не е случайност, нито украшение: това им позволява да виждат класовете с видимост на ниво пакет, които програмата държи затворени за света (6.1.3). Тест, който трябва да иска public за всичко, би задължил да отваряш програмата само за да я тестваш.
target/ е това, което Maven произвежда: .class файловете на програмата в target/classes, тези на тестовете в target/test-classes, отчетите от тестовете и .jar. Това е папка за изхвърляне: може да се изтрие цялата във всеки момент и се възстановява с една команда, защото всичко в нея идва от това, което е в src. От тук две правила: никога не се редактира нищо вътре в target и никога не се качва в хранилище (ако някой ден запазиш този проект с Git, добави target/ към файла му .gitignore). servicios.txt остава в корена, защото програмата го търси по относителен път, от папката, в която се изпълнява, а ти ще изпълняваш винаги от корена на проекта.
6.2.4 pom.xml, ред по ред
Файлът, който описва проекта, се казва pom.xml (POM е съкращение от „модел на обектите на проекта“ (Project Object Model); .xml е форматът). XML е текстов формат от вложени тагове: всеки елемент се отваря с <име> и се затваря с </име>, а това, което е вътре, е неговата стойност или неговите деца. По-шумен е от JSON в Урок 8, но е този, който Maven е избрал, и няма да го пишеш от нулата много често; ще го четеш и затова си струва веднъж да го разбереш цялостно. Ето този на проекта, пълен:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>mx.habil</groupId>
<artifactId>revisor</artifactId>
<version>0.6.0</version>
<properties>
<maven.compiler.release>25</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.16.0</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.6.0</version>
</plugin>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>3.5.1</version>
</plugin>
</plugins>
</build>
</project>
Данните на проекта. <modelVersion>4.0.0</modelVersion> е версията на формата на самия pom.xml; винаги е 4.0.0 и се пише без размисъл. Трите, които следват, groupId, artifactId и version, са координатите на проекта, адресът, с който всеки проект на Maven по света може да се позове на този. groupId е този, който го публикува (mx.habil, както името на пакета); artifactId е името на проекта (revisor); а version казва в коя точка се намира. Тук е 0.6.0, защото това е състоянието на курса в Урок 6; всеки урок ще я вдига, а 10 ще я остави на 1.0.0. Координатите изглеждат като формалност, докато в Урок 7 не декларираш първата си зависимост: зависимостта е groupId, artifactId и version. И решават нещо, което ще видиш в 6.3: името на .jar.
Свойствата. <properties> групира именувани стойности, които други части на файла (или на плъгините) четат. Две са задължителни. maven.compiler.release със стойност 25 казва на компилатора „компилирай като Java 25“, което включва гаранция, която старата двойка source/target не даваше: с release компилаторът не само генерира формата на поисканата версия, но и отхвърля всеки клас на JDK, който не е съществувал в тази версия. И служи като ранно предупреждение; ето какво се случва, ако някой се опита да изгради проекта с инсталиран JDK 21:
$ mvn -B compile
[ERROR] BUILD FAILURE
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.16.0:compile (default-compile) on project revisor: Fatal error compiling: error: release version 25 not supported -> [Help 1]
release version 25 not supported: JDK, който ползва Maven, не знае да компилира за Java 25. Това е грешката, която вижда този, който има инсталиран default-jdk на Mint (Java 21, Урок 1), и този, който следва урок с друга версия: вместо да се провали наполовина със странна грешка, проектът отказва още от първата секунда и казва защо. Другото свойство, project.build.sourceEncoding, със стойност UTF-8, декларира кодирането на твоите файлове .java; пише се изрично, за да не зависи резултатът от компютъра, на който се компилира, и за да са винаги едни и същи ударенията в "sin conexión" (без връзка).
Плъгините. Вътре в <build>, <plugins> декларира трите, от които проектът има нужда днес: компилаторът (maven-compiler-plugin), този, който изпълнява тестовете (maven-surefire-plugin, който днес няма тестове за изпълнение и който Урок 7 ще пусне в работа), и този, който сглобява .jar (maven-jar-plugin). Всеки носи изписана своя <version> и това е решение, не пропуск. Maven може да работи и без нея, защото носи версия по подразбиране на всеки основен плъгин; ето какво става, ако махнеш версията на един от тях:
$ mvn -B verify
[WARNING]
[WARNING] Some problems were encountered while building the effective model for mx.habil:revisor:jar:0.6.0
[WARNING] 'build.plugins.plugin.version' for org.apache.maven.plugins:maven-surefire-plugin is missing.
Maven предупреждава, че версията липсва и че „бъдещи версии на Maven може да спрат да приемат такива проекти“. Има и по-практична причина: ако не фиксираш версията, изграждането ти се променя, когато се промени Maven, а не когато ти решиш. Същият проект, същата команда и различен резултат в деня, в който някой обнови инструмента. Фиксирането на версията на всяка част е това, което прави изграждането възпроизводимо: ръководството на Maven за възпроизводими изграждания разглежда този проблем и започва с искането плъгините да са в конкретни и актуални версии. Същото ще важи и за зависимостите.
Още една подробност, преди да го ползваш. pom.xml отгоре не носи декларацията <?xml ... ?>, която се появява в почти всички примери из интернет. Тя е по желание и премахването ѝ избягва една класическа грешка: тази декларация трябва да е първото нещо във файла, без коментар или интервал преди нея, и който я напише и после добави коментар отгоре, получава невалиден файл със съобщение, което не помага.
6.2.5 Жизненият цикъл
Maven не изпълнява отделни плъгини, изпълнява фази, а всяка фаза вика плъгините, които ѝ съответстват. Фазите образуват верига във фиксиран ред, жизнения цикъл (официалната документация), и има просто правило, което го управлява: когато поискаш една фаза, Maven изпълнява първо всички предишни. Тези, които ще ползваш в курса, са четири:
| Фаза | Какво прави | Какво оставя |
|---|---|---|
compile |
компилира кода от src/main/java |
target/classes |
test |
компилира тестовете и ги изпълнява (до Урок 7 няма нито един) | target/test-classes и target/surefire-reports |
package |
сглобява .jar с компилираното |
target/revisor-0.6.0.jar |
verify |
финални проверки върху пакета (засега няма повече) | — |
По горното правило mvn verify компилира, тества и пакетира; mvn test компилира и тества; а mvn compile само компилира. В курса се ползва verify като „пълната команда“: в Урок 10 ще окачиш на нея проверките за качество, а командата, която вече знаеш, няма да се промени. И винаги с -B (batch, „на партиди“): казва на Maven да не ползва цветове, нито ленти за напредък и да не задава въпроси, което е начинът да поискаш чист изход, който може да се копира, да се постави в съобщение или да се чете на сървър без екран.
6.2.6 Преместването на revisor в проект
Време е да направиш това, което показва дървото от папки. Три стъпки и едно незадължително подобрение, и нито едно не променя това, което програмата прави. От ~/curso-java/revisor/:
$ mkdir -p src/main/java/mx/habil/revisor src/test/java/mx/habil/revisor
$ mv *.java src/main/java/mx/habil/revisor/
mkdir -p създава папка и всички, които липсват по пътя ѝ. mv мести файловете; внимавай да го изпълниш от revisor/ и servicios.txt да не се премести (*.java не го засяга). Сега всеки файл се нуждае от своя ред package. Да го направиш на ръка в тринайсет файла е скучна работа, затова този цикъл в терминала го прави вместо теб: за всеки файл записва новия ред и след това оригиналното съдържание във временен файл и го преименува върху оригинала:
$ for f in src/main/java/mx/habil/revisor/*.java; do { printf 'package mx.habil.revisor;\n\n'; cat "$f"; } > "$f.nuevo" && mv "$f.nuevo" "$f"; done
Провери го, като отвориш който и да е: първият ред трябва да е package mx.habil.revisor;, следван от празен ред. Остават две неща, които терминалът не може да направи вместо теб, защото изискват да разбираш кода.
Първото е Revisor. Твоят Revisor.java от урок 5 беше компактен файл: main без клас около него. Сега е клас, единственият публичен в проекта, с частния си конструктор (6.1.3) и main в дългата форма (6.1.4). Кратък е, затова е показан целият:
// fig06_03/src/main/java/mx/habil/revisor/Revisor.java
package mx.habil.revisor;
import java.nio.file.Path;
import java.util.List;
/** El punto de entrada: lee servicios.txt, revisa cada servicio e imprime el reporte. */
public final class Revisor {
private Revisor() {}
/** Arranca el revisor; los errores de configuración salen por la salida de error. */
public static void main(String[] args) throws InterruptedException {
try {
List<Servicio> servicios = Configuracion.leer(Path.of("servicios.txt"));
Reporte reporte = Reporte.de(Revision.revisarTodos(servicios));
System.out.println(Formato.tabla(reporte));
} catch (ErrorDeConfiguracion e) {
System.err.println("error de configuración: " + e.getMessage());
}
}
}
Забележи какво не се промени: тялото на try е това от урок 5, ред по ред. Чете конфигурацията, проверява услугите в реда на списъка, сглобява отчета, отпечатва таблицата; а ако конфигурацията е лоша, catch отпечатва съобщението на испански през изхода за грешки. Единственото ново е обвивката и три неща, които сменят значението си: компактният файл виждаше целия модул java.base, без да го иска, а един нормален клас се нуждае от свои import (затова се появяват Path и List); main е сега този, който JVM вика (и затова трябва да е public static); а класовете, които преди виждаше „защото бяха в същата папка“, сега ги вижда, защото са в същия пакет.
Второто е една възможност, която пакетът отваря, и е по желание. В уроци 3 до 5, без пакет, всеки път когато трябваше Respondio, се налагаше да се пише Respuesta.Respondio, защото от пакет без име не може да се импортира нищо (ако опиташ, компилаторът казва package R does not exist). С пакет import mx.habil.revisor.Respuesta.Respondio; вече работи и кодът може да казва само Respondio. Тестовете, които ще напишеш в Урок 7, го ползват; кодът на програмата, такъв какъвто го носиш от урок 5, продължава да работи, без да го пипаш, и можеш да го оставиш така.
6.2.7 Първото изграждане
С преместения проект ето първото изграждане. От корена (~/curso-java/revisor/, където е pom.xml) и с pom.xml отгоре, който още няма тестове:
$ mvn -B compile
[INFO] Scanning for projects...
[INFO]
[INFO] --------------------------< mx.habil:revisor >--------------------------
[INFO] Building revisor 0.6.0
[INFO] from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- resources:3.5.0:resources (default-resources) @ revisor ---
[INFO] skip non existing resourceDirectory ~/curso-java/revisor/src/main/resources
[INFO]
[INFO] --- compiler:3.16.0:compile (default-compile) @ revisor ---
[INFO] Recompiling the module because of changed source code.
[INFO] Compiling 13 source files with javac [debug release 25] to target/classes
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
Чети го отгоре надолу, защото всеки ред --- plugin:версия:цел --- ти казва коя част е работила. resources:resources копира файлове, които не са код (тук няма: „skip non existing resourceDirectory“). compiler:compile върши работата: Compiling 13 source files with javac [debug release 25] to target/classes казва колко файла е компилирал, с какви опции (debug означава, че запазва имената на променливите, което обяснява следващия абзац) и къде е оставил .class файловете. А BUILD SUCCESS е известието, че никоя фаза не е пропаднала. При първото стартиране на Maven ще видиш и много редове Downloading, които тук са изрязани: Maven сваля собствените си плъгини от Maven Central и ги пази в ~/.m2/repository, скрита папка в дома ти, която служи като локален кеш. Това обяснява две неща: първото стартиране е бавно и изисква интернет, а следващите са бързи и работят без връзка.
С класовете в target/classes програмата се изпълнява със същия -cp отпреди малко, сега с корена на класовете на Maven:
$ java -cp target/classes mx.habil.revisor.Revisor
SERVICIO ESTADO TIEMPO DETALLE
catalogo ARRIBA 142ms 200
inventario LENTO 2.3s 200
pagos ARRIBA 87ms 200
reportes CAIDO — sin conexión
4 servicios · 2 arriba · 1 lento · 1 caído · promedio 843ms
Това е обичайната таблица, със същото резюме: 4 servicios · 2 arriba · 1 lento · 1 caído · promedio 843ms (4 услуги · 2 работят · 1 бавна · 1 паднала · средно 843 ms). Това число програмата го изчислява всеки път, а тестовете от Урок 7 ще го следят.
debug от реда Compiling има скрита награда, която се връзва с Урок 2: съобщението за NullPointerException, което с java Revisor.java казваше "<local1>" (стартерът компилира без тази информация), сега казва името на променливата. С тестов клас, компилиран от Maven, който прави detalle.isBlank() върху null:
$ java -cp target/classes mx.habil.revisor.Nulo
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.isBlank()" because "detalle" is null
at mx.habil.revisor.Nulo.main(Nulo.java:6)
because "detalle" is null: името на променливата, която е причинила грешката. Maven компилира с информация за дебъгване по подразбиране и затова съобщенията на програмите в проекта се четат по-лесно от тези на отделните файлове.
6.2.8 Три проверки, преди да продължиш
Ако си стигнал дотук, проектът вече се компилира и се изпълнява. Провери три неща, в този ред:
mvn -B compileзавършва сBUILD SUCCESSи казваCompiling 13 source files.java -cp target/classes mx.habil.revisor.Revisorотпечатва таблицата с четирите услуги с резюме от 843 ms.- Изпълнена от друга папка, същата команда не намира
servicios.txt: това е правилното поведение и ще го видиш в „Грешката, която ще видиш“.
Ако някоя пропадне, грешката почти винаги е от същите три семейства: файл без своя ред package (компилаторът казва, че не намира клас), файл в грешната папка (6.1.2) или команда, изпълнена извън корена на проекта, където няма pom.xml:
$ cd ~/curso-java
$ mvn -B verify
[ERROR] BUILD FAILURE
[ERROR] The goal you specified requires a project to execute but there is no POM in this directory (~/curso-java). Please verify you invoked Maven from the correct directory. -> [Help 1]
Maven го казва с всички букви: в папката, в която си, няма POM. Липсва cd до корена на проекта.
6.3 Пълният цикъл и .jar
6.3.1 Какво прави mvn -B verify днес
Дотук ползваше compile, първата фаза. Сега поискай последната, онази, която ще пишеш през остатъка от курса. От корена на проекта:
$ mvn -B verify
[INFO] Building revisor 0.6.0
...
[INFO] --- compiler:3.16.0:compile (default-compile) @ revisor ---
[INFO] Recompiling the module because of changed source code.
[INFO] Compiling 13 source files with javac [debug release 25] to target/classes
[INFO]
[INFO] --- resources:3.5.0:testResources (default-testResources) @ revisor ---
[INFO] skip non existing resourceDirectory ~/curso-java/revisor/src/test/resources
[INFO]
[INFO] --- compiler:3.16.0:testCompile (default-testCompile) @ revisor ---
[INFO] No sources to compile
[INFO]
[INFO] --- surefire:3.6.0:test (default-test) @ revisor ---
[INFO] No tests to run.
[INFO]
[INFO] --- jar:3.5.1:jar (default-jar) @ revisor ---
[INFO] Building jar: ~/curso-java/revisor/target/revisor-0.6.0.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
Чети го с таблицата от 6.2.5 до себе си, защото всеки блок --- plugin:версия:цел --- е една фаза в работа, в реда на веригата. Първо фазата compile, като в 6.2.7: тринайсет файла към target/classes. После идват три реда, които са от фазата test и още нямат работа. testResources търси помощни файлове за тестовете в src/test/resources и няма. testCompile е същият компилатор, но приложен към src/test/java, и казва No sources to compile: папката за тестове, която създаде в 6.2.6, съществува и е празна. А surefire:test, плъгинът, който изпълнява тестовете, казва No tests to run., защото няма нито един. Накрая фазата package вика jar:jar, който сглобява файла target/revisor-0.6.0.jar. Фазата verify още не добавя нищо (Урок 10 ще окачи на нея проверките за качество), а Maven приключва с BUILD SUCCESS.
Две подробности заслужават втори поглед. Първата е името на .jar: revisor-0.6.0.jar не си го писал никъде, Maven го сглобява от artifactId и version на pom.xml. Когато версията се вдигне, името на файла ще се вдигне с нея; затова координатите не са формалност. Втората е редът на Surefire. Забележи какво се случи: Maven не изпълни нито един тест и завърши в зелено. Днес това е истината, защото не си писал тестове, а отчетът го казва с всички букви. В Урок 7 ще видиш, че зелено без тестове може да се появи и когато си писал тестове, без това предупреждение, и защо броят на тестовете се чете винаги от отчета, а никога от BUILD SUCCESS. Запази този образ: No tests to run. е това, което казва Maven, когато знае, че няма тестове.
6.3.2 Какво има вътре в .jar
Файлът .jar (от Java ARchive) е компресиран архив със същия формат като .zip, който пази компилирани класове и малко информация за тях. JDK носи инструмент за работа с него, който се казва по същия начин, jar, и неговата опция tf изброява съдържанието (t е „изброй“, f е „на файла, който ти казвам“):
$ jar tf target/revisor-0.6.0.jar
META-INF/
META-INF/MANIFEST.MF
mx/
mx/habil/
mx/habil/revisor/
META-INF/maven/
META-INF/maven/mx.habil/
META-INF/maven/mx.habil/revisor/
mx/habil/revisor/Clasificador.class
mx/habil/revisor/Configuracion.class
mx/habil/revisor/ErrorDeConfiguracion.class
mx/habil/revisor/Estado.class
mx/habil/revisor/Formato.class
mx/habil/revisor/Reporte.class
mx/habil/revisor/Respuesta$Respondio.class
mx/habil/revisor/Respuesta$SinRespuesta.class
mx/habil/revisor/Respuesta.class
mx/habil/revisor/Resultado.class
mx/habil/revisor/Resumen.class
mx/habil/revisor/Revision.class
mx/habil/revisor/Revisor.class
mx/habil/revisor/Servicio.class
mx/habil/revisor/Simulador.class
META-INF/maven/mx.habil/revisor/pom.xml
META-INF/maven/mx.habil/revisor/pom.properties
Има три групи. Най-голямата са твоите класове и те са с папките на своя пакет: mx/habil/revisor/Clasificador.class, точно формата, който javac -d out произведе в 6.1.2 и който JVM изисква, за да намери клас по пълното му име. По същество един .jar е папка target/classes, вмъкната в един файл. Преброй класовете: петнайсет са, от тринайсет изходни файла. Двата повече са Respuesta$Respondio.class и Respuesta$SinRespuesta.class: двата record-а, които живеят вътре в интерфейса Respuesta (Урок 3). Всеки вложен клас или record се компилира в свой собствен .class, с името на класа, който го съдържа, един $ и неговото име.
Втората група е META-INF/MANIFEST.MF, манифестът: текстов файл с данни за .jar, който чете JVM. Третата, META-INF/maven/..., е копие на pom.xml и файл с координатите; добавя ги Maven, за да знае този, който намери .jar сам, от кой проект идва и в коя версия. Онова, което не е там, също има значение: нито един .java (.jar носи компилираното, не изходния код), нито servicios.txt (той е в корена на проекта, не в src/main/resources, така че не е част от програмата), нито нещо от JUnit или от тестове.
6.3.3 Изпълнение от .jar и какво още липсва
Тъй като .jar съдържа същото като target/classes, служи по същия начин като classpath:
$ java -cp target/revisor-0.6.0.jar mx.habil.revisor.Revisor
SERVICIO ESTADO TIEMPO DETALLE
catalogo ARRIBA 142ms 200
inventario LENTO 2.3s 200
pagos ARRIBA 87ms 200
reportes CAIDO — sin conexión
4 servicios · 2 arriba · 1 lento · 1 caído · promedio 843ms
Същата таблица, сега от един-единствен файл, който можеш да копираш на друг компютър с Java 25. Съществува по-кратка форма, java -jar archivo.jar, която ползват Java програмите, предавани на други хора. Опитай я:
$ java -jar target/revisor-0.6.0.jar
no main manifest attribute, in target/revisor-0.6.0.jar
„Няма главен атрибут в манифеста“. С -jar JVM не получава името на класа, който трябва да стартира: търси го в манифеста, на ред, наречен Main-Class. А манифестът, който Maven е сглобил, не го носи. Можеш да го видиш, защото unzip -p отпечатва файл отвътре на .jar, без да го извлича:
$ unzip -p target/revisor-0.6.0.jar META-INF/MANIFEST.MF
Manifest-Version: 1.0
Created-By: Maven JAR Plugin 3.5.1
Java-Version: 25
Build-Jdk-Spec: 25
Четири реда: версията на формата, кой го е създал и с каква Java. Нищо не казва кой е входният клас. (Ако твоята JVM е на испански, съобщението за грешка излиза преведено; измерено с JVM на мексикански испански: no hay ningún atributo de manifiesto principal en target/revisor-0.6.0.jar. Кодът на изход (exit code) е 1 и в двата езика.) Би могло да се добави този ред с конфигурацията на плъгина jar, но това не би решило основния проблем, който се появява в Урок 8: когато програмата ползва библиотека, .jar ще има твоите класове, а не тези на библиотеката, и JVM няма да ги намери. Урок 10 поправя и двете неща наведнъж с .jar, който носи всичко вътре и знае откъде да започне. Дотогава командата на курса е тази с -cp, която работи с target/classes и с .jar по равно.
6.3.4 target/ може да се изхвърли
Вече имаш пред очи всичко, което живее в target/: classes с .class файловете, .jar и няколко работни папки на Maven (maven-status, maven-archiver, generated-sources), които пазят данни за следващото стартиране. Всичко това идва от src/ и pom.xml и затова може да се изтрие цялото във всеки момент: следващото mvn -B verify ще го създаде отново същото. Това е доказателството, че един проект е добре сглобен. Ако някога изтриеш target/ и проектът вече не се изгражда, значи е имало нещо важно там, където не му е мястото.
От тук идват двете правила, които вече видя в 6.2.3 и които си струва да се повторят, защото са онези, които най-често се нарушават. Никога не се редактира нищо вътре в target/: промяна там се губи при следващото изграждане, а най-лошото е, че междувременно изглежда, че е сработила. И никога не се пази target/ в хранилище: би запазил резултата, а не изходния код, и всеки човек, който изгражда проекта, би виждал промени във файлове, които не е пипал. Пази се src/, pom.xml и каквото програмата има нужда да работи, като servicios.txt.
6.4 Стъпката на проекта: revisor като проект на Maven
Имаш всички части. Ето целия проект, такъв какъвто остава в края на урока. По-голямата част вече я познаваш: от програмата само Revisor (6.2.6) се промени наистина спрямо Урок 5, а останалите класове само получиха своя ред package. Включени са всички, за да може проектът да се сглоби, без да се гледа друг урок:
revisor/
pom.xml versión 0.6.0 · release 25 · tres plugins con versión fija
servicios.txt sin cambios
src/main/java/mx/habil/revisor/
Revisor.java NUEVO como clase pública: el único punto de entrada
Estado.java Respuesta.java Servicio.java Resultado.java Clasificador.java
Formato.java Simulador.java Revision.java Resumen.java Reporte.java
Configuracion.java ErrorDeConfiguracion.java (solo ganan su línea package)
src/test/java/mx/habil/revisor/ (vacía: la llena la Lección 7)
Вече видя цели pom.xml (6.2.4), Revisor.java (6.2.6) и Estado.java (6.1.3). Твоят servicios.txt е обичайният:
# nombre url [timeout_ms]
catalogo https://catalogo.example
inventario https://inventario.example
pagos https://pagos.example
reportes https://reportes.example
А това са останалите единайсет файла. Ако си следвал уроци 3 до 5, вече ги имаш и единствената разлика е казаното в 6.2.6; включвам ги, за да може проектът на този урок да се изгради, без да зависи от нищо друго.
<details> <summary>Единайсетте файла на програмата, които се променят само в първия си ред</summary>// fig06_03/src/main/java/mx/habil/revisor/Respuesta.java
package mx.habil.revisor;
sealed interface Respuesta permits Respuesta.Respondio, Respuesta.SinRespuesta {
record Respondio(int codigo, long tiempoMs) implements Respuesta {}
record SinRespuesta(String motivo) implements Respuesta {}
}
// fig06_03/src/main/java/mx/habil/revisor/Servicio.java
package mx.habil.revisor;
import java.net.URI;
record Servicio(String nombre, URI url, int timeoutMs) {
static final int TIMEOUT_POR_OMISION_MS = 5000;
Servicio(String nombre, URI url) {
this(nombre, url, TIMEOUT_POR_OMISION_MS);
}
}
// fig06_03/src/main/java/mx/habil/revisor/Resultado.java
package mx.habil.revisor;
import java.util.OptionalLong;
record Resultado(Servicio servicio, Estado estado, Respuesta respuesta) {
String detalle() {
return switch (respuesta) {
case Respuesta.Respondio(int codigo, long _) -> String.valueOf(codigo);
case Respuesta.SinRespuesta(String motivo) -> motivo;
};
}
OptionalLong tiempoMs() {
return switch (respuesta) {
case Respuesta.Respondio(int _, long tiempoMs) -> OptionalLong.of(tiempoMs);
case Respuesta.SinRespuesta _ -> OptionalLong.empty();
};
}
}
// fig06_03/src/main/java/mx/habil/revisor/Clasificador.java
package mx.habil.revisor;
final class Clasificador {
static final long UMBRAL_LENTO_MS = 1000;
static Estado clasificar(Respuesta respuesta) {
return switch (respuesta) {
case Respuesta.Respondio(int codigo, long _) when codigo >= 400 -> Estado.CAIDO;
case Respuesta.Respondio(int _, long tiempoMs) when tiempoMs >= UMBRAL_LENTO_MS ->
Estado.LENTO;
case Respuesta.Respondio _ -> Estado.ARRIBA;
case Respuesta.SinRespuesta _ -> Estado.CAIDO;
};
}
}
// fig06_03/src/main/java/mx/habil/revisor/Simulador.java
package mx.habil.revisor;
import java.util.Map;
import java.util.Optional;
final class Simulador {
static final Map<String, Respuesta> RESPUESTAS =
Map.of(
"catalogo", new Respuesta.Respondio(200, 142),
"inventario", new Respuesta.Respondio(200, 2300),
"pagos", new Respuesta.Respondio(200, 87),
"reportes", new Respuesta.SinRespuesta("sin conexión"));
static Optional<Respuesta> buscar(String nombre) {
return Optional.ofNullable(RESPUESTAS.get(nombre));
}
}
// fig06_03/src/main/java/mx/habil/revisor/Revision.java
package mx.habil.revisor;
import java.util.ArrayList;
import java.util.List;
import java.util.OptionalLong;
final class Revision {
static Resultado revisar(Servicio servicio) throws InterruptedException {
Respuesta respuesta =
Simulador.buscar(servicio.nombre()).orElse(new Respuesta.SinRespuesta("sin conexión"));
Resultado resultado =
new Resultado(servicio, Clasificador.clasificar(respuesta), respuesta);
OptionalLong tiempo = resultado.tiempoMs();
if (tiempo.isPresent()) {
Thread.sleep(tiempo.getAsLong());
}
return resultado;
}
static List<Resultado> revisarTodos(List<Servicio> servicios) throws InterruptedException {
List<Resultado> resultados = new ArrayList<>();
for (Servicio servicio : servicios) {
resultados.add(revisar(servicio));
}
return resultados;
}
}
// fig06_03/src/main/java/mx/habil/revisor/Resumen.java
package mx.habil.revisor;
import java.util.List;
import java.util.Map;
import java.util.OptionalDouble;
import java.util.OptionalLong;
import java.util.stream.Collectors;
record Resumen(int total, int arriba, int lento, int caido, OptionalLong promedioMs) {
static Resumen de(List<Resultado> resultados) {
Map<Estado, Long> porEstado =
resultados.stream()
.collect(Collectors.groupingBy(Resultado::estado, Collectors.counting()));
OptionalDouble promedio =
resultados.stream()
.map(Resultado::tiempoMs)
.filter(OptionalLong::isPresent)
.mapToLong(OptionalLong::getAsLong)
.average();
OptionalLong promedioMs =
promedio.isPresent()
? OptionalLong.of(Math.round(promedio.getAsDouble()))
: OptionalLong.empty();
return new Resumen(
resultados.size(),
porEstado.getOrDefault(Estado.ARRIBA, 0L).intValue(),
porEstado.getOrDefault(Estado.LENTO, 0L).intValue(),
porEstado.getOrDefault(Estado.CAIDO, 0L).intValue(),
promedioMs);
}
}
// fig06_03/src/main/java/mx/habil/revisor/Reporte.java
package mx.habil.revisor;
import java.util.List;
record Reporte(List<Resultado> resultados, Resumen resumen) {
Reporte {
resultados = List.copyOf(resultados);
}
static Reporte de(List<Resultado> resultados) {
return new Reporte(resultados, Resumen.de(resultados));
}
}
// fig06_03/src/main/java/mx/habil/revisor/Formato.java
package mx.habil.revisor;
import java.util.Locale;
import java.util.stream.Collectors;
final class Formato {
static final String PATRON_LINEA = "%-12s%-10s%6s %s";
static String formatearTiempo(long tiempoMs) {
if (tiempoMs < 1000) {
return tiempoMs + "ms";
}
return String.format(Locale.ROOT, "%.1fs", tiempoMs / 1000.0);
}
static String tiempo(Respuesta respuesta) {
return switch (respuesta) {
case Respuesta.Respondio(int _, long tiempoMs) -> formatearTiempo(tiempoMs);
case Respuesta.SinRespuesta _ -> "—";
};
}
static String encabezado() {
return String.format(Locale.ROOT, PATRON_LINEA, "SERVICIO", "ESTADO", "TIEMPO", "DETALLE");
}
static String linea(Resultado resultado) {
return String.format(
Locale.ROOT,
PATRON_LINEA,
resultado.servicio().nombre(),
resultado.estado(),
tiempo(resultado.respuesta()),
resultado.detalle());
}
static String contar(long cantidad, String palabra) {
return cantidad + " " + palabra + (cantidad == 1 ? "" : "s");
}
static String resumen(Resumen resumen) {
String promedio =
resumen.promedioMs().isPresent() ? formatearTiempo(resumen.promedioMs().getAsLong()) : "—";
return contar(resumen.total(), "servicio")
+ " · "
+ resumen.arriba()
+ " arriba · "
+ contar(resumen.lento(), "lento")
+ " · "
+ contar(resumen.caido(), "caído")
+ " · promedio "
+ promedio;
}
static String tabla(Reporte reporte) {
String renglones =
reporte.resultados().stream().map(Formato::linea).collect(Collectors.joining("\n"));
return encabezado() + "\n" + renglones + "\n\n" + resumen(reporte.resumen());
}
}
// fig06_03/src/main/java/mx/habil/revisor/Configuracion.java
package mx.habil.revisor;
import java.io.BufferedReader;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
final class Configuracion {
static List<Servicio> leer(Path ruta) throws ErrorDeConfiguracion {
String archivo = ruta.getFileName().toString();
List<Servicio> servicios = new ArrayList<>();
try (BufferedReader lector = Files.newBufferedReader(ruta)) {
String renglon;
int numero = 0;
while ((renglon = lector.readLine()) != null) {
numero++;
String limpio = renglon.strip();
if (limpio.isEmpty() || limpio.startsWith("#")) {
continue;
}
servicios.add(leerRenglon(limpio, numero));
}
} catch (NoSuchFileException e) {
throw new ErrorDeConfiguracion("no existe el archivo " + archivo, e);
} catch (IOException e) {
throw new ErrorDeConfiguracion("no se pudo leer el archivo " + archivo, e);
}
verificarRepetidos(servicios);
if (servicios.isEmpty()) {
throw new ErrorDeConfiguracion("el archivo no tiene servicios");
}
return servicios;
}
static Servicio leerRenglon(String renglon, int numero) throws ErrorDeConfiguracion {
String[] campos = renglon.split("\\s+");
if (campos.length < 2 || campos.length > 3) {
throw new ErrorDeConfiguracion(
"renglón " + numero + ": se esperaban 2 o 3 campos y hay " + campos.length);
}
URI url = leerUrl(campos[1], numero);
if (campos.length == 2) {
return new Servicio(campos[0], url);
}
return new Servicio(campos[0], url, leerTimeout(campos[2], numero));
}
static URI leerUrl(String texto, int numero) throws ErrorDeConfiguracion {
if (!texto.startsWith("http://") && !texto.startsWith("https://")) {
throw new ErrorDeConfiguracion(
"renglón " + numero + ": la url debe empezar con http:// o https://");
}
try {
return new URI(texto);
} catch (URISyntaxException e) {
throw new ErrorDeConfiguracion("renglón " + numero + ": la url no es válida", e);
}
}
static int leerTimeout(String texto, int numero) throws ErrorDeConfiguracion {
String mensaje = "renglón " + numero + ": timeout_ms debe ser un entero mayor que 0";
try {
int timeoutMs = Integer.parseInt(texto);
if (timeoutMs > 0) {
return timeoutMs;
}
} catch (NumberFormatException e) {
throw new ErrorDeConfiguracion(mensaje, e);
}
throw new ErrorDeConfiguracion(mensaje);
}
static void verificarRepetidos(List<Servicio> servicios) throws ErrorDeConfiguracion {
Set<String> vistos = new HashSet<>();
Set<String> repetidos = new LinkedHashSet<>();
for (Servicio servicio : servicios) {
if (!vistos.add(servicio.nombre())) {
repetidos.add(servicio.nombre());
}
}
if (!repetidos.isEmpty()) {
throw new ErrorDeConfiguracion("nombres repetidos: " + String.join(", ", repetidos));
}
}
}
// fig06_03/src/main/java/mx/habil/revisor/ErrorDeConfiguracion.java
package mx.habil.revisor;
final class ErrorDeConfiguracion extends Exception {
ErrorDeConfiguracion(String mensaje) {
super(mensaje);
}
ErrorDeConfiguracion(String mensaje, Throwable causa) {
super(mensaje, causa);
}
}
</details>За да завършиш урока: mvn -B verify трябва да каже Compiling 13 source files, No tests to run. и BUILD SUCCESS и да остави target/revisor-0.6.0.jar; java -cp target/classes mx.habil.revisor.Revisor и java -cp target/revisor-0.6.0.jar mx.habil.revisor.Revisor трябва и двете да отпечатат таблицата с резюмето от Урок 4 (4.5) от 843 ms. Има статия на Hábil за това как се изгражда непрекъснатата интеграция на една софтуерна къща, мястото, където mvn -B verify се изпълнява само при всяка промяна; струва си да я прочетеш сега, когато командата вече е твоя.
Грешката, която ще видиш
Урокът има една основна грешка, тази на класа, който JVM не намира, и няколко спомагателни съобщения, които е добре да се четат с един поглед.
Основната грешка. Тя е от 6.1.2 и ще я видиш веднага щом изпълниш програма с пакет по грешния начин:
$ java -cp out Hola
Error: Could not find or load main class Hola
Caused by: java.lang.ClassNotFoundException: Hola
Чете се на три стъпки, винаги едни и същи. Какво търсеше: името след class, тук само Hola. Къде го търси: в classpath, който си му дал с -cp, тук out. Какво намери: нищо (ClassNotFoundException) или файл с друго истинско име (NoClassDefFoundError ... wrong name: mx/habil/revisor/Hola). С тези три отговора поправката е винаги една от две: да напишеш пълното име на класа, с неговия пакет, или да изпълниш от корена на класовете (out, target/classes или .jar). В revisor правилната форма е само една и не се променя до края на урока: java -cp target/classes mx.habil.revisor.Revisor, от корена на проекта.
Спомагателните съобщения, за да ги разпознаваш с един поглед:
package org.junit.jupiter.api does not exist(fig06_01.java): липсва зависимост. Стартерът не знае за зависимости; в Maven те се декларират вpom.xml, а Урок 7 декларира именно тази.cannot find symbolсъсsymbol: variable Clasificador, при компилиране с търсене по папки: файл е в папка, която не съответства на неговия пакет. Папката трябва да казва истината (6.1.2).Estado is not public in mx.habil.revisor; cannot be accessed from outside package: поискал си клас с видимост на ниво пакет от друг пакет. Преди да му сложишpublic, запитай се дали този клас трябва да се ползва оттам.no non-private zero argument constructor found:mainна екземпляр в клас с частен конструктор. Дългата формаpublic static void main(String[] args)няма този проблем (6.1.4).The goal you specified requires a project to execute but there is no POM in this directory: изпълнил сиmvnизвън корена на проекта. Влез в папката, която съдържаpom.xml.release version 25 not supported: Maven ползва JDK по-стар от 25. Провери редаJava versionнаmvn --version, променливатаJAVA_HOMEи, ако има няколко JDK, карето от Урок 1 заsdk default.no main manifest attribute: опитал сиjava -jarс.jar, който не казва кой е входният му клас. Ползвайjava -cpс името на класа; Урок 10 го решава.error de configuración: no existe el archivo servicios.txt(грешка в конфигурацията: файлът servicios.txt не съществува) при изпълнение от друга папка, което е последното и онова, което се повтаря най-често:
$ cd ..
$ java -cp revisor/target/classes mx.habil.revisor.Revisor
error de configuración: no existe el archivo servicios.txt
Програмата търси servicios.txt по относителен път, Path.of("servicios.txt"), а относителен път означава „файлът от папката, в която стоя“, не „този, който е до програмата“. От папката по-нагоре не съществува. Забележи какво е направила добре програмата: не показа изключение от трийсет реда, нито NoSuchFileException, а съобщението на испански от Урок 5. За да разбереш в коя папка си, командата pwd ти го казва. В Урок 9 първият аргумент на командния ред ще ти позволи да посочиш файла, без да зависиш от това къде си.
Какво се прави погрешно
Инсталиране на Maven по път, различен от този на Java. Ако Java идва от SDKMAN, а Maven от другаде (системен пакет, архив, разархивиран на ръка), всеки носи собствени представи кой JDK да ползва, а в деня, в който не съвпаднат, грешката не казва защо. С двата инструмента в SDKMAN mvn --version и java --version говорят за един и същ Temurin 25.
Слагане на public на всичко „за да се компилира“. Всяко публично нещо е обещание към този, който го ползва, а всяко обещание е цена, когато поискаш да го промениш. Грешката is not public почти винаги означава, че нещо е на грешното място, а не че липсва дума.
Папка, която не съвпада с пакета, „защото се компилира“. Maven го компилира (6.1.2), но чупи всичко, което търси по папки: редактора, търсенията, инкременталното компилиране и следващия човек. Това е грешка, която не предупреждава, и затова се избягва по навик, а не по съобщение.
Зависимости и плъгини без версия или с най-новата. Кара изграждането ти да се промени в деня, в който някой публикува нова версия или обнови Maven, без да си пипал нищо. Всяка част носи изписана своя версия и се вдига нарочно.
Редактиране или запазване на target/. Това е, което Maven генерира и може да се изтрие и да се възстанови във всеки момент (6.3.4). Промяна, направена там, се губи при следващото изграждане, а ако я качиш в хранилище, запазваш резултата, а не изходния код.
Изпълнение от коя да е папка и обвиняване на програмата. Maven трябва да е в папката на pom.xml, а revisor трябва да е в папката на servicios.txt: днес те са една и съща, коренът на проекта. Преди да прочетеш грешка „не съществува“, прочети pwd.
Упражнения
Упражнение 1 — Папката, която лъже
Премести Clasificador.java една папка по-нагоре от тази, която му е определена, в src/main/java/mx/habil/, без да пипаш реда му package mx.habil.revisor;. Преди да изпълниш каквото и да било, предскажи какво прави всяка от тези две команди: javac -sourcepath src/main/java -d out src/main/java/mx/habil/revisor/Revision.java (която компилира Revision, като търси останалите класове по папки) и mvn -B compile. Изпълни ги и сравни. Къде остана Clasificador.class вътре в target/classes? Върни файла в неговата папка и изтрий папката out, която javac създаде.
Упражнение 2 — Какво решава Maven вместо теб
Махни от pom.xml реда <version>3.5.1</version> на плъгина maven-jar-plugin и изпълни mvn -B verify. Прочети редовете WARNING в началото: какво казват, че липсва, и къде? После потърси реда --- jar:... в изхода: коя версия на плъгина е ползвал Maven? С този отговор обясни в дневника си защо pom.xml фиксира версията, макар днес да дава същото. Възстанови реда в края.
Упражнение 3 — Грешният JDK
Ако имаш инсталиран JDK 21 със SDKMAN освен 25 (JDK-тата на SDKMAN живеят в ~/.sdkman/candidates/java/, една папка за всеки идентификатор; ls ~/.sdkman/candidates/java/ ти ги показва), насочи Maven към него само за една команда, като сложиш променливата отпред: JAVA_HOME=~/.sdkman/candidates/java/<идентификаторът на 21> mvn -B compile. Преди това предскажи в коя фаза пропада и с какво съобщение. Ако нямаш 21, не го инсталирай за това: прочети решението. И в двата случая отговори: кое свойство на pom.xml предизвиква грешката и защо е по-добре да пропадне там, а не по-късно?
Упражнение 4 — Какво има в твоя .jar
Изпълни jar tf target/revisor-0.6.0.jar и преброй файловете, които завършват на .class. Преди да броиш, предскажи колко трябва да са, знаейки, че проектът има тринайсет файла .java. Ако числото не съвпада с предсказанието ти, намери излишните и обясни откъде идват. После опитай java -jar target/revisor-0.6.0.jar и обясни съобщението с това, което видя в манифеста.
Решения
Решение 1
С Clasificador.java една папка по-нагоре двете команди се държат различно:
$ javac -sourcepath src/main/java -d out src/main/java/mx/habil/revisor/Revision.java
src/main/java/mx/habil/revisor/Revision.java:14: error: cannot find symbol
new Resultado(servicio, Clasificador.clasificar(respuesta), respuesta);
^
symbol: variable Clasificador
location: class Revision
1 error
$ mvn -B compile
...
[INFO] Compiling 13 source files with javac [debug release 25] to target/classes
[INFO] BUILD SUCCESS
javac с -sourcepath търси всеки клас, от който се нуждае, по папки: за mx.habil.revisor.Clasificador гледа в src/main/java/mx/habil/revisor/Clasificador.java, не го намира и заключава, че не съществува. Maven, напротив, подава на javac пълния списък на файловете от src/main/java, намира тринайсетте и компилира без да се оплаква. А Clasificador.class остава в target/classes/mx/habil/revisor/, където казва неговият пакет, а не там, където беше .java: компилаторът подрежда изхода според реда package. Затова грешката не се вижда в Maven, а се вижда във всеки инструмент, който търси по папки. Програмата върви същата; проектът обаче вече лъже.
Решение 2
Без версията Maven изгражда, но предупреждава в началото:
$ mvn -B verify
[INFO] Scanning for projects...
[WARNING]
[WARNING] Some problems were encountered while building the effective model for mx.habil:revisor:jar:0.6.0
[WARNING] 'build.plugins.plugin.version' for org.apache.maven.plugins:maven-jar-plugin is missing. @ line 25, column 15
[WARNING]
[WARNING] It is highly recommended to fix these problems because they threaten the stability of your build.
[WARNING]
[WARNING] For this reason, future Maven versions might no longer support building such malformed projects.
...
[INFO] --- jar:3.5.1:jar (default-jar) @ revisor ---
...
[INFO] BUILD SUCCESS
Казва какво липсва (версията на плъгина maven-jar-plugin) и къде (@ line 25, column 15, редът на pom.xml, където започва този плъгин; ако твоят файл има друга форма, числото се променя). А отговорът на втория въпрос е онзи, който прави упражнението интересно: Maven ползва същата 3.5.1, която фиксираше твоят pom.xml. Днес, с Maven 3.10.0, версията по подразбиране съвпада с тази на курса. Но това е съвпадение на тази версия на Maven, не свойство на твоя проект: версията по подразбиране я решава този, който публикува Maven, и се променя, когато се промени Maven. Фиксирането ѝ е разликата между „днес дава същото“ и „винаги дава същото“. Изречението за дневника: версията се фиксира, за да не зависи резултатът от това кой Maven има инсталиран всеки.
Решение 3
С JDK 21 Maven стартира, стига до фазата compile и спира там:
$ JAVA_HOME=<la carpeta del JDK 21> mvn -B compile
...
[INFO] --- compiler:3.16.0:compile (default-compile) @ revisor ---
[INFO] Recompiling the module because of changed source code.
[INFO] Compiling 13 source files with javac [debug release 25] to target/classes
[INFO] ------------------------------------------------------------------------
[ERROR] BUILD FAILURE
...
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.16.0:compile (default-compile) on project revisor: Fatal error compiling: error: release version 25 not supported -> [Help 1]
(Измерено с JDK 21 от друга дистрибуция; съобщението е на компилатора и не зависи от нея.) Отговорното свойство е maven.compiler.release със стойност 25: компилаторът на JDK 21 не умее да произвежда класове за Java 25 и го казва, преди да компилира нито един файл. По-добре е да пропадне там, защото съобщението назовава причината (версията), вместо симптом (метод или синтаксис, който „не съществува“), и защото грешката се появява в първата фаза, преди някой да е губил време с тестове или полуготови пакети. Забележи и това, което упражнението показва: Maven не ползва непременно java от твоя терминал, а този от JAVA_HOME, ако е дефинирана. Затова, когато Maven и java --version не съвпадат, първо се гледа тази променлива.
Решение 4
Има петнайсет файла .class, не тринайсет:
$ jar tf target/revisor-0.6.0.jar
...
mx/habil/revisor/Respuesta$Respondio.class
mx/habil/revisor/Respuesta$SinRespuesta.class
mx/habil/revisor/Respuesta.class
...
Двата повече са record-ите, вложени в интерфейса Respuesta: всеки клас, record или enum се компилира в свой собствен .class и ако живее вътре в друг, името му носи отпред това на онзи, който го съдържа, и един $. Един файл .java може да произведе няколко .class, а връзката „един файл, един клас“ е задължителна само за public класовете (6.1.2). А java -jar пропада с no main manifest attribute, защото манифестът, който видя в 6.3.3, няма реда Main-Class: JVM, с -jar, не знае от кой клас да започне. С java -cp target/revisor-0.6.0.jar mx.habil.revisor.Revisor ти му го казваш и работи.
Как разбирам, че съм успял
-
mvn --versionказваApache Maven 3.10.0и версия на Java, която започва с 25. -
mvn -B verify, от корена на~/curso-java/revisor/, казваCompiling 13 source files,No tests to run.иBUILD SUCCESSи оставяtarget/revisor-0.6.0.jar. -
java -cp target/classes mx.habil.revisor.Revisorотпечатва таблицата с четирите услуги с резюмето4 servicios · 2 arriba · 1 lento · 1 caído · promedio 843msи същото с.jarвместоtarget/classes. - От папката по-нагоре, същата команда с
revisor/target/classesотпечатваerror de configuración: no existe el archivo servicios.txtи можеш да обясниш защо. - Можеш да обясниш какво става, ако файл декларира
package mx.habil.revisor;и е в друга папка, и защо Maven ти позволява да го направиш, но не е добра идея. - Можеш да обясниш защо има петнайсет
.classв.jarи защоjava -jarоще не работи. - Записа в дневника си кое съобщение от този урок ти беше най-трудно за четене.
Резюме
Отговори, без да гледаш урока:
- Кои три неща не може да прави стартерът на файлове с изходен код, а прави проектът на Maven?
- Каква връзка трябва да има между името на един пакет, реда
packageи папката на файла? Кой го проверява и кой не? - Какво вижда клас с видимост на ниво пакет и какво не? Кой клас на
revisorеpublicи защо само той? - Защо
Revisorползваpublic static void main(String[] args), а неvoid main()? - С какво инсталираш Maven и кои три неща разпознаваш в
mvn --version, за да знаеш, че е останал добре? - Какво са координатите на един проект на Maven? За какво служи фиксирането на версията на всеки плъгин?
- Кои фази изпълнява
mvn verifyи в какъв ред? Какво оставя всяка вtarget/? - Какво има вътре в един
.jar, какво няма и защоjava -jarоще не го изпълнява?
За допълнително четене
- Maven, „Introduction to the Build Lifecycle“ и „Introduction to the POM“: фазите, кой плъгин върви във всяка от тях и всичко, което може да носи един
pom.xml. На английски, като почти цялата официална документация. Посетено на 7 октомври 2026 г. - Спецификацията на езика, глава 7: пакети и модули и ръководството на Oracle за пакетите: точните правила за имена, папки и импортирания. Посетено на 7 октомври 2026 г.
- Oracle, „JAR File Specification“ (JDK 25) и наръчникът на командата
jar: какво носи един.jar, какво е манифестът и за какво служиMain-Class. Посетено на 13 октомври 2026 г. - SDKMAN, употреба:
list,install,useиdefault, същите команди за Java и за Maven. Посетено на 13 октомври 2026 г.
Предпочитате имейл? Пишете ни на hola@habil.mx