$ sudo teach IT
МОДУЛЬ 12 · УРОК 4

Инструмент сборки: Maven/Gradle

Познакомимся с профессиональными инструментами сборки Java-проектов. Разберём Maven и Gradle — два самых популярных сборщика, которые автоматизируют управление зависимостями, компиляцию, тестирование и упаковку приложений

~30 мин Для продвинутых Java 21+

Зачем нужны инструменты сборки?

Представьте, что вы только что начали изучать Java. Вы создаёте файл Main.java и компилируете его одной командой javac Main.java. Затем запускаете java Main. Всё работает. Но что произойдёт, когда ваш проект вырастет?

Допустим, вы разрабатываете веб-приложение. вам понадобится библиотека для работы с базой данных (Hibernate), библиотека для HTTP-запросов (OkHttp), библиотека для логирования (Log4j), библиотека для работы с JSON (Jackson) и ещё десяток других. Каждая из этих библиотек состоит из JAR-файлов, каждый JAR-файл может зависеть от других JAR-файлов. В результате получается сложная сеть зависимостей, которую нужно отслеживать вручную.

Без инструмента сборки вам пришлось бы:

  • Вручную скачивать каждый JAR-файл с официального сайта
  • Вручную отслеживать, какие версии библиотек совместимы друг с другом
  • Помнить, что нужно скомпилировать 50+ Java-файлов в правильном порядке
  • Вручную управлять классификацией классов (classpath)
  • Самостоятельно упаковывать приложение в JAR или WAR
  • Каждый раз запускать длинную команду компиляции из десятков аргументов

Именно здесь на помощь приходят инструменты сборки — программы, которые автоматизируют весь этот процесс. Они берут на себя управление зависимостями, компиляцию, тестирование, упаковку и деплой. Вы просто описываете, что хотите получить, а инструмент сборки делает всё остальное.

Аналогия: Инструмент сборки — это как шеф-повар на кухне ресторана. Вы даёте ему рецепт (конфигурационный файл), а он сам закупает продукты (библиотеки), обрабатывает их (компилирует), готовит блюдо (тесты) и подаёт на стол (упаковывает в JAR). Вам не нужно бегать по магазинам и самостоятельно нарезать овощи.

Проблема ручного управления зависимостями

Давайте рассмотрим типичную проблему, с которой сталкивается каждый разработчик. Вы хотите использовать библиотеку Gson для работы с JSON. Скачиваете JAR-файл, кладёте в папку lib, добавляете в classpath. Всё работает.

Но вот выясняется, что Gson зависит от Guava, а Guava зависит от ещё трёх библиотек. Вы скачиваете их все. Затем обнаруживаете, что Gson версии 2.10 несовместим с Guava версии 31.0. Придётся искать совместимые версии.

Ещё хуже, когда в проекте используются две библиотеки, которые зависят от разных версий одной и той же библиотеки. Это называется конфликт зависимостей. Например, ваш проект использует Spring версии 5.3 и Hibernate версии 5.6, и обе они зависят от Jackson, но от разных версий. Какую версию выбрать?

Инструменты сборки решают эти проблемы автоматически:

  • Автоматическая загрузка — инструмент сам скачивает нужные библиотеки из репозиториев
  • Разрешение конфликтов — выбирает совместимые версии зависимостей
  • Транзитивные зависимости — автоматически загружает зависимости зависимостей
  • Кэширование — сохраняет скачанные библиотеки локально, не качая заново
  • Изоляция — каждый проект может использовать свои версии библиотек

Почему javac и java недостаточно?

Компилятор javac и виртуальная машина java — это замечательные инструменты, но они решают только узкий круг задач. Компилятор знает, как превратить .java файлы в .class файлы, а JVM знает, как их выполнить. Но они не знают:

  • Как скачать библиотеку из интернета
  • Какую версию библиотеки использовать
  • Как собрать все class-файлы в один JAR
  • Как запустить тесты перед сборкой
  • Как создать документацию к проекту
  • Как автоматизировать процесс сборки в CI/CD

Попробуйте собрать大型 проект вручную с помощью только javac. Вам придётся написать скрипт, который компилирует все файлы в правильном порядке, передаёт все библиотеки через classpath, упаковывает результат в JAR и генерирует манифест. Это утомительно и подвержено ошибкам.

Apache Maven — философия «соглашение вместо конфигурации»

Maven появился в 2004 году как проект Apache Software Foundation. Его главная философия — «convention over configuration» (соглашение вместо конфигурации). Это означает, что Maven ожидает от вас определённую структуру проекта, и если вы следуете этой структуре, вам не нужно ничего настраивать.

Например, Maven ожидает, что исходный код Java будет лежать в папке src/main/java, ресурсы — в src/main/resources, тесты — в src/test/java, а ресурсы тестов — в src/test/resources. Если вы соблюдаете эту структуру, Maven автоматически знает, что нужно компилировать, где искать ресурсы, как запускать тесты.

Стандартная структура Maven-проекта:

my-project/
├── pom.xml
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com/
│   │   │       └── example/
│   │   │           └── App.java
│   │   └── resources/
│   │       └── application.properties
│   └── test/
│       ├── java/
│       │   └── com/
│       │       └── example/
│       │           └── AppTest.java
│       └── resources/
│           └── test-data.properties

Эта структура не случайна. Каждая папка имеет своё назначение. src/main/java — основной исходный код. src/main/resources — файлы, которые должны попасть в финальный артефакт (конфигурации, изображения, XML-схемы). src/test/java — код тестов, который не попадёт в продакшн-сборку.

Почему convention over configuration? Представьте, что вы заходите в кафе. В каждом кафе кофе подают в чашке, а не в тарелке. Вы не просите «пожалуйста, подайте кофе в чашке» — это уже подразумевается. Точно так же Maven подразумевает стандартную структуру. Это упрощает жизнь разработчиков, потому что любой Java-программист, открыв Maven-проект, сразу понимает, где что лежит.

pom.xml — сердце Maven-проекта

Файл pom.xml (Project Object Model) — это конфигурационный файл Maven. Он описывает проект: его имя, версию, зависимости, плагины, профили и многое другое. Это XML-файл, который Maven читает при каждом запуске.

Каждый Maven-проект однозначно идентифицируется тремя полями:

  • groupId — обратный домен организации (например, com.example). Обычно совпадает с корневым пакетом Java.
  • artifactId — имя проекта (например, my-app). Это имя JAR-файла без расширения.
  • version — версия проекта (например, 1.0.0). Используется для отслеживания изменений.

Пример минимального pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<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
         http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>my-application</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <name>Моё приложение</name>
    <description>Пример Maven-проекта</description>

</project>

Поле packaging определяет тип собираемого артефакта. Возможные значения: jar (по умолчанию), war, ear, pom. Для большинства Java-приложений используется jar.

Уникальный идентификатор проекта формируется как groupId:artifactId:version. Например: com.example:my-application:1.0.0. Этот triple (GAV) используется Maven для поиска и скачивания библиотек.

Maven-репозитории — откуда берутся библиотеки?

Maven хранит все библиотеки в репозиториях — специальных серверах с JAR-файлами. Когда вы указываете зависимость в pom.xml, Maven автоматически скачивает её из репозитория.

Существует три типа репозиториев:

  • Локальный репозиторий — папка на вашем компьютере (обычно ~/.m2/repository), где Maven хранит скачанные библиотеки. Если библиотека уже скачана, Maven берёт её оттуда, не обращаясь к сети.
  • Центральный репозиторий — официальный сервер Maven (repo.maven.apache.org), содержащий более 400 000 библиотек. Это основной источник для большинства зависимостей.
  • Удалённый (корпоративный) репозиторий — сервер внутри вашей организации (например, Nexus или Artifactory), где хранятся внутренние библиотеки и кэшируются внешние.

Порядок поиска: Maven сначала ищет в локальном репозитории. Если не находит — обращается к удалённым репозиториям (сначала к корпоративным, затем к центральному). Скачанная библиотека сохраняется в локальный репозиторий для повторного использования.

Интересный факт: Центральный репозиторий Maven хранит более 400 000 артефактов от десятков тысяч разработчиков. Каждый день через него проходят миллионы скачиваний. Это делает Maven Central одним из крупнейших репозиториев программного обеспечения в мире.

Жизненный цикл Maven

Maven организует процесс сборки в последовательность этапов, называемых жизненным циклом (lifecycle). Каждый этап (phase) выполняет определённую группу задач. Вот основные фазы:

Фаза Описание
clean Удаляет папку target с предыдущей сборкой
validate Проверяет корректность структуры проекта
compile Компилирует исходный код в target/classes
test Запускает单元 тесты (требует предварительной компиляции)
package Упаковывает классы в JAR/WAR/EAR
verify Запускает интеграционные тесты и проверки
install Устанавливает JAR в локальный репозиторий
deploy Отправляет JAR в удалённый репозиторий

Когда вы запускаете mvn package, Maven выполняет все предыдущие фазы последовательно: validate → compile → test → package. Это называется «привязка» — каждая фаза автоматически запускает все предыдущие.

Самые частые команды Maven:

mvn clean compile
mvn clean test
mvn clean package
mvn clean install
mvn clean deploy

Обычно перед любой командой добавляют clean, чтобы удалить артефакты предыдущей сборки и избежать проблем с устаревшими файлами.

Maven-зависимости — scope и управление версиями

Зависимости в Maven указываются в секции <dependencies>. Каждая зависимость определяется тройкой GAV (groupId, artifactId, version). Но помимо этого, у зависимости есть параметр scope — область видимости.

Scope определяет, когда и где зависимость доступна:

Scope Компиляция Тесты Рантайм Упаковка
compile ✓ ✓ ✓ ✓
provided ✓ ✓ ✗ ✗
runtime ✗ ✓ ✓ ✓
test ✗ ✓ ✗ ✗
system ✓ ✓ ✗ ✗

compile — используется по умолчанию. Зависимость доступна везде и попадает в финальный артефакт. Пример: библиотека Gson, которая нужна вашему коду.

provided — зависимость доступна при компиляции, но не попадает в артефакт, потому что предполагается, что среда выполнения уже её предоставляет. Пример: API сервлетов в веб-приложении — контейнер Tomcat уже содержит эти классы.

runtime — зависимость не нужна при компиляции, но нужна при выполнении. Пример: JDBC-драйвер — ваш код компилируется к интерфейсу java.sql.Connection, а конкретная реализация подключается только при запуске.

test — зависимость доступна только в тестах. Пример: JUnit — он не нужен в продакшне.

Пример подключения зависимостей

<dependencies>
    <dependency>
        <groupId>com.google.code.gson</groupId>
        <artifactId>gson</artifactId>
        <version>2.10.1</version>
        <scope>compile</scope>
    </dependency>

    <dependency>
        <groupId>jakarta.servlet</groupId>
        <artifactId>jakarta.servlet-api</artifactId>
        <version>6.0.0</version>
        <scope>provided</scope>
    </dependency>

    <dependency>
        <groupId>org.postgresql</groupId>
        <artifactId>postgresql</artifactId>
        <version>42.7.1</version>
        <scope>runtime</scope>
    </dependency>

    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <version>5.10.1</version>
        <scope>test</scope>
    </dependency>
</dependencies>

Maven-плагины — расширение возможностей

Maven сам по себе — это лишь каркас. Вся реальная работа выполняется плагинами. Каждая фаза жизненного цикла привязана к одному или нескольким плагинам. Без плагинов Maven не умеет ничего — он даже не может скомпилировать код.

Два самых важных плагина:

  • maven-compiler-plugin — компилирует Java-исходники. По умолчанию использует версию Java 1.6, что для современных проектов слишком старо. Рекомендуется явно указывать версию.
  • maven-surefire-plugin — запускает单元 тесты. Работает с JUnit 4 и JUnit 5. Автоматически ищет классы тестов по именованным паттернам.
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.12.1</version>
            <configuration>
                <source>21</source>
                <target>21</target>
                <release>21</release>
            </configuration>
        </plugin>

        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.2.3</version>
        </plugin>

        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-jar-plugin</artifactId>
            <version>3.3.0</version>
            <configuration>
                <archive>
                    <manifest>
                        <mainClass>com.example.App</mainClass>
                    </manifest>
                </archive>
            </configuration>
        </plugin>
    </plugins>
</build>

В этом примере мы настроили компилятор на Java 21, указали плагин для запуска тестов и настроили JAR-плагин для создания исполняемого JAR-файла с указанием главного класса.

Совет: Всегда явно указывайте версию maven-compiler-plugin и параметры source/target/release. Без этого Maven будет компилировать ваш код под Java 1.6, и вы получите ошибки при использовании модулей, records, switch-expression и других современных возможностей Java.

Maven-профили

Профили в Maven позволяют создавать различные конфигурации для разных окружений. Например, вы можете иметь профиль для разработки (с H2 базой данных), профиль для тестирования (с PostgreSQL) и профиль для продакшна (с Oracle).

Профили активируются через командную строку с флагом -P:

<profiles>
    <profile>
        <id>dev</id>
        <properties>
            <db.url>jdbc:h2:mem:test</db.url>
            <db.driver>org.h2.Driver</db.driver>
        </properties>
    </profile>

    <profile>
        <id>prod</id>
        <properties>
            <db.url>jdbc:postgresql://prod-server:5432/mydb</db.url>
            <db.driver>org.postgresql.Driver</db.driver>
        </properties>
        <dependencies>
            <dependency>
                <groupId>org.postgresql</groupId>
                <artifactId>postgresql</artifactId>
                <version>42.7.1</version>
            </dependency>
        </dependencies>
    </profile>
</profiles>

Запуск с профилем:

mvn clean package -Pdev
mvn clean package -Pprod

Профили также могут включать/исключать зависимости, менять переменные окружения, настраивать плагины и даже определять разные исходники для компиляции.

Gradle — гибкость и производительность

Gradle появился в 2012 году как современная замена Maven. Он использует ту же философию «convention over configuration», но добавляет гибкость и значительно лучшую производительность. Gradle использует DAG (Directed Acyclic Graph) для определения порядка выполнения задач и кэширует результаты промежуточных шагов.

Основные преимущества Gradle перед Maven:

  • Производительность — инкрементальная сборка, кэширование, демон для поддержки JVM в памяти
  • Гибкость — язык конфигурации (Groovy или Kotlin) вместо XML
  • Управление зависимостями — более мощный механизм разрешения конфликтов
  • Multi-project builds — лучшая поддержка мультипроектных сборок
  • Android — является официальным инструментом сборки Android-приложений

build.gradle — Groovy DSL vs Kotlin DSL

Gradle поддерживает два языка конфигурации: Groovy DSL (файл build.gradle) и Kotlin DSL (файл build.gradle.kts). Оба делают одно и то же, но синтаксис отличается.

Groovy DSL более компактный и «скриптовый»:

plugins {
    id 'java'
    id 'application'
}

group = 'com.example'
version = '1.0.0'

java {
    sourceCompatibility = JavaVersion.VERSION_21
    targetCompatibility = JavaVersion.VERSION_21
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'com.google.code.gson:gson:2.10.1'
    runtimeOnly 'org.postgresql:postgresql:42.7.1'
    testImplementation 'org.junit.jupiter:junit-jupiter:5.10.1'
}

application {
    mainClass = 'com.example.App'
}

test {
    useJUnitPlatform()
}

Kotlin DSL более строгий и с типизацией:

plugins {
    java
    application
}

group = "com.example"
version = "1.0.0"

java {
    sourceCompatibility = JavaVersion.VERSION_21
    targetCompatibility = JavaVersion.VERSION_21
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("com.google.code.gson:gson:2.10.1")
    runtimeOnly("org.postgresql:postgresql:42.7.1")
    testImplementation("org.junit.jupiter:junit-jupiter:5.10.1")
}

application {
    mainClass.set("com.example.App")
}

tasks.test {
    useJUnitPlatform()
}

Какой DSL выбрать? Если вы только начинаете, рекомендуем Kotlin DSL — он обеспечивает автодополнение в IDE, проверку типов и более понятные сообщения об ошибках. Groovy DSL проще для скриптов и быстрых экспериментов.

Gradle-задачи (tasks)

Вместо фаз жизненного цикла Maven использует задачи (tasks). Каждая задача — это отдельная единица работы, которую Gradle может выполнить. Задачи связаны между собой зависимостями — Gradle строит граф зависимостей и выполняет задачи в оптимальном порядке.

Основные задачи Gradle:

Задача Описание Аналог Maven
clean Удаляет папку build clean
compileJava Компилирует основной исходный код compile
test Запускает单元 тесты test
build Компилирует, тестирует, упаковывает package
fatJar Создаёт JAR со всеми зависимостями Нет аналога (нужен плагин)

Запуск задач в Gradle:

./gradlew build
./gradlew clean build
./gradlew test
./gradlew fatJar
./gradlew tasks --all

Команда ./gradlew tasks --all покажет полный список доступных задач. Это очень полезно при знакомстве с новым проектом.

Gradle-зависимости — области видимости

Gradle использует конфигурации вместо scope. Каждая зависимость принадлежит к одной или нескольким конфигурациям. Основные конфигурации:

Конфигурация Описание Аналог Maven
implementation Доступна при компиляции и рантайме, скрыта от потребителей compile
api Доступна потребителям (нужен java-library плагин) compile
compileOnly Доступна только при компиляции provided
runtimeOnly Доступна только в рантайме runtime
testImplementation Доступна только в тестах test

Важное отличие implementation от api: при использовании implementation зависимость не видна потребителям вашего модуля. Это ускоряет сборку, потому что при изменении зависимой библиотеки нужно перекомпилировать только ваш модуль, а не всех потребителей.

Gradle Wrapper — воспроизводимость сборки

Gradle Wrapper (файлы gradlew, gradlew.bat, gradle/wrapper/gradle-wrapper.properties) — это механизм для обеспечения воспроизводимости сборки. Он гарантирует, что все разработчики и CI/CD системы используют одну и ту же версию Gradle.

Вместо того чтобы требовать установки Gradle на каждом компьютере, Wrapper автоматически скачивает нужную версию при первом запуске. Это решает проблему «у меня работает, у тебя нет».

Создание Wrapper:

gradle wrapper --gradle-version 8.5

./gradlew --version

Файл gradle-wrapper.properties содержит URL для скачивания и контрольную сумму:

distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

Файлы Wrapper должны быть добавлены в Git. Это стандартная практика — каждый, кто клонирует репозиторий, может сразу запустить ./gradlew build без установки Gradle.

Почему Wrapper так важен? Представьте, что вы обновили Gradle до версии 8.5, а ваш коллега всё ещё использует 7.6. Ваши конфигурации могут работать по-разному. Wrapper решает эту проблему — он фиксирует версию Gradle в репозитории и автоматически скачивает её.

Создание fat JAR в Gradle

Fat JAR (или uber JAR) — это JAR-файл, содержащий не только ваш код, но и все зависимости. Это удобно для деплоя — вы копируете один файл и запускаете его командой java -jar app.jar.

В Gradle создание fat JAR реализуется через задачу jar или плагин shadow:

plugins {
    id 'java'
    id 'application'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'com.google.code.gson:gson:2.10.1'
    implementation 'org.slf4j:slf4j-api:2.0.9'
    runtimeOnly 'org.slf4j:slf4j-simple:2.0.9'
}

application {
    mainClass = 'com.example.App'
}

jar {
    manifest {
        attributes 'Main-Class': 'com.example.App'
    }
    duplicatesStrategy = DuplicatesStrategy.EXCLUDE
    from {
        configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
    }
}

После запуска ./gradlew jar вы получите JAR-файл в папке build/libs/, который можно запустить командой java -jar build/libs/my-application-1.0.0.jar.

Сравнение Maven и Gradle

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

Критерий Maven Gradle
Язык конфигурации XML Groovy / Kotlin
Производительность Средняя Высокая (кэширование,增量)
Гибкость Ограниченная Высокая
Обучаемость Проще для новичков Сложнее, но мощнее
Экосистема Очень большая Большая, растёт
Стандарт де-факто Да Да (особенно Android)
Multi-project Поддержка Отличная поддержка

Если вы начинаете новый проект и не знаете, что выбрать:

  • Выберите Maven, если вы новичок, если проект простой, или если команда уже использует Maven.
  • Выберите Gradle, если вам нужна гибкость, высокая производительность, или если вы разрабатываете Android-приложение.
  • В любом случае — оба инструмента профессиональные и широко используются в индустрии.

Практика: создание Maven-проекта с нуля

Давайте создадим Maven-проект с нуля и подключим к нему JUnit 5 и Lombok. Следуйте инструкциям пошагово.

Шаг 1: Создание структуры проекта

mkdir -p src/main/java/com/example/demo
mkdir -p src/main/resources
mkdir -p src/test/java/com/example/demo
mkdir -p src/test/resources

Шаг 2: Создание pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<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
         http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>demo-project</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <properties>
        <maven.compiler.release>21</maven.compiler.release>
        <junit.version>5.10.1</junit.version>
        <lombok.version>1.18.30</lombok.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.12.1</version>
                <configuration>
                    <release>21</release>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                            <version>${lombok.version}</version>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.2.3</version>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.3.0</version>
                <configuration>
                    <archive>
                        <manifest>
                            <mainClass>com.example.demo.App</mainClass>
                        </manifest>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

Шаг 3: Код приложения

package com.example.demo;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class User {
    private String name;
    private int age;
    private String email;

    public boolean isAdult() {
        return age >= 18;
    }

    public String getGreeting() {
        return "Привет, " + name + "!";
    }
}

Шаг 4: Главный класс

package com.example.demo;

import java.util.List;

public class App {
    public static void main(String[] args) {
        List<User> users = List.of(
            new User("Алексей", 25, "alexey@mail.ru"),
            new User("Мария", 17, "maria@mail.ru"),
            new User("Дмитрий", 32, "dmitry@mail.ru"),
            new User("Анна", 19, "anna@mail.ru")
        );

        System.out.println("=== Список пользователей ===");
        for (User user : users) {
            System.out.println(user.getGreeting());
            System.out.println("  Возраст: " + user.getAge());
            System.out.println("  Email: " + user.getEmail());
            System.out.println("  Совершеннолетний: " + user.isAdult());
            System.out.println();
        }

        long adultCount = users.stream()
            .filter(User::isAdult)
            .count();
        System.out.println("Совершеннолетних: " + adultCount + " из " + users.size());
    }
}

Шаг 5: Тест

package com.example.demo;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class UserTest {

    @Test
    void shouldReturnTrueForAdult() {
        User user = new User("Тест", 25, "test@mail.ru");
        assertTrue(user.isAdult());
    }

    @Test
    void shouldReturnFalseForMinor() {
        User user = new User("Тест", 15, "test@mail.ru");
        assertFalse(user.isAdult());
    }

    @Test
    void shouldReturnCorrectGreeting() {
        User user = new User("Ольга", 30, "olga@mail.ru");
        assertEquals("Привет, Ольга!", user.getGreeting());
    }

    @Test
    void shouldHaveCorrectName() {
        User user = new User("Сергей", 40, "sergey@mail.ru");
        assertEquals("Сергей", user.getName());
    }

    @Test
    void shouldHaveCorrectEmail() {
        User user = new User("Ирина", 22, "irina@mail.ru");
        assertEquals("irina@mail.ru", user.getEmail());
    }
}

Шаг 6: Запуск сборки и тестов

mvn clean compile
mvn clean test
mvn clean package
java -jar target/demo-project-1.0.0.jar

Вывод программы:

=== Список пользователей ===
Привет, Алексей!
  Возраст: 25
  Email: alexey@mail.ru
  Совершеннолетний: true

Привет, Мария!
  Возраст: 17
  Email: maria@mail.ru
  Совершеннолетний: false

Привет, Дмитрий!
  Возраст: 32
  Email: dmitry@mail.ru
  Совершеннолетний: true

Привет, Анна!
  Возраст: 19
  Email: anna@mail.ru
  Совершеннолетний: true

Совершеннолетних: 3 из 4

Обратите внимание: В pom.xml мы использовали переменные в секции <properties>. Это позволяет задать версии зависимостей в одном месте и использовать их через ${variable}. При обновлении версии нужно изменить только одно значение.

Multi-module проекты

Крупные проекты обычно разбиваются на несколько модулей. Например, интернет-магазин может состоять из модулей: core (модели данных), web (веб-интерфейс), api (REST API), service (бизнес-логика). Это упрощает навигацию, параллельную разработку и повторное использование кода.

В Maven multi-module проект имеет родительский pom.xml и дочерние модули:

<project>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>my-shop</artifactId>
    <version>1.0.0</version>
    <packaging>pom</packaging>

    <modules>
        <module>core</module>
        <module>service</module>
        <module>web</module>
        <module>api</module>
    </modules>

    <properties>
        <maven.compiler.release>21</maven.compiler.release>
        <junit.version>5.10.1</junit.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.junit.jupiter</groupId>
                <artifactId>junit-jupiter</artifactId>
                <version>${junit.version}</version>
                <scope>test</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
</project>

В Gradle multi-module проект использует файл settings.gradle для указания модулей:

rootProject.name = 'my-shop'

include 'core'
include 'service'
include 'web'
include 'api'

Каждый модуль имеет свой build.gradle (или pom.xml), но общие зависимости и настройки выносятся в родительский файл. Это уменьшает дублирование и упрощает обновление версий.

Полезные команды Maven и Gradle

Вот шпаргалка с самыми полезными командами для повседневной работы:

Maven

mvn clean install              # Собрать и установить в локальный репозиторий
mvn dependency:tree             # Показать дерево зависимостей
mvn dependency:analyze          # Анализ неиспользуемых зависимостей
mvn versions:display-dependency-updates  # Проверить обновления зависимостей
mvn help:effective-pom          # Показать итоговый POM с учётом наследования
mvn archetype:generate          # Создать проект из шаблона
mvn -DskipTests package         # Собрать без запуска тестов
mvn dependency:resolve          # Скачать все зависимости

Gradle

./gradlew build                 # Полная сборка
./gradlew clean build           # Очистка и сборка
./gradlew dependencies          # Показать дерево зависимостей
./gradlew test                  # Запустить тесты
./gradlew fatJar                # Создать JAR со всеми зависимостями
./gradlew tasks --all           # Все доступные задачи
./gradlew build -x test         # Собрать без тестов
./gradlew --rerun-tasks build   # Полная пересборка

Типичные ошибки и как их избежать

Ошибка: «Unable to resolve dependency»

Менее не может найти библиотеку. Причины: опечатка в GAV, библиотека удалена из Maven Central, проблемы с сетью. Решение: проверьте GAV, очистите локальный кэш (rm -rf ~/.m2/repository), проверьте подключение к интернету.

Ошибка: «Could not find or load main class»

JAR не содержит манифест с указанием главного класса или главный класс указан неверно. Решение: проверьте конфигурацию maven-jar-plugin или application в Gradle.

Ошибка: «Unsupported class file major version»

Вы компилируете код под более новую версию Java, чем установлена на компьютере. Решение: установите нужную версию JDK или понизьте maven.compiler.release.

Ошибка: «Duplicate class» при сборке fat JAR

При объединении JAR-файлов обнаруживаются одинаковые классы. Решение: используйте duplicatesStrategy = DuplicatesStrategy.EXCLUDE в Gradle или настройте maven-shade-plugin в Maven.

Совет: Если вы столкнулись с конфликтом зависимостей, выполните mvn dependency:tree (Maven) или ./gradlew dependencies (Gradle). Эти команды покажут полное дерево зависимостей с указанием того, какая библиотека тянет какую. Это поможет найти источник конфликта.

Сборка и деплой — полная картина

Давайте посмотрим на полный цикл от написания кода до запуска приложения:

  1. Разработка — вы пишете Java-код и тесты
  2. Компиляция — mvn compile или ./gradlew compileJava превращает .java в .class
  3. Тестирование — mvn test или ./gradlew test запускает все тесты
  4. Упаковка — mvn package или ./gradlew build создаёт JAR/WAR
  5. Проверка — интеграционные тесты, статический анализ
  6. Установка — mvn install или ./gradlew publishToMavenLocal делает JAR доступным для других проектов
  7. Деплой — mvn deploy отправляет JAR в корпоративный репозиторий

Этот процесс автоматизируется с помощью CI/CD систем (Jenkins, GitHub Actions, GitLab CI). Инструмент сборки — ключевой компонент конвейера, потому что он обеспечивает воспроизводимость: каждый разработчик и каждая CI-система собирают проект одинаково.

Итоги урока

Итоги урока

  • Инструменты сборки (Maven, Gradle) автоматизируют управление зависимостями, компиляцию, тестирование и упаковку
  • Maven использует XML (pom.xml) и философию «convention over configuration» — стандартная структура проекта избавляет от лишней конфигурации
  • Каждый Maven-проект идентифицируется тройкой GAV: groupId, artifactId, version
  • Жизненный цикл Maven включает фазы: clean, compile, test, package, install, deploy
  • Scope (compile, provided, test, runtime) определяет, когда зависимость доступна
  • Gradle использует язык конфигурации (Groovy или Kotlin) вместо XML и обеспечивает лучшую производительность
  • Gradle Wrapper (gradlew) гарантирует одинаковую версию Gradle у всех участников команды
  • Fat JAR объединяет код приложения и все зависимости в один исполняемый файл
  • Multi-module проекты разбивают крупные приложения на логические модули для удобства разработки
  • Команды dependency:tree и dependencies помогают диагностировать конфликты версий

Тест: Инструмент сборки Maven/Gradle

6 вопросов

Maven структура

Premium