Что такое транзитивные зависимости и как ими управлять?

Ответ

Транзитивные зависимости — это библиотеки, которые автоматически подключаются к проекту, потому что они требуются вашими прямыми зависимостями. Управление ими критически важно для избежания конфликтов версий и security-уязвимостей.

1. Проблемы транзитивных зависимостей:

  • Конфликт версий: Разные библиотеки требуют разные версии одной транзитивной зависимости
  • Раздувание сборки: Ненужные библиотеки увеличивают размер артефакта
  • Уязвимости: Устаревшие версии могут содержать security issues
  • Лицензионные конфликты: Несовместимые лицензии транзитивных зависимостей

2. Анализ зависимостей:

# Maven: отобразить дерево зависимостей
mvn dependency:tree
# С фильтрацией по группе или артефакту
mvn dependency:tree -Dincludes=com.google.guava:*

# Gradle: отобразить зависимости
./gradlew dependencies
# Для конкретной конфигурации
./gradlew app:dependencies --configuration implementation

3. Управление в Maven:

<!-- 1. Dependency Management для централизованного контроля версий -->
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.15.0</version> <!-- Фиксируем версию для всех модулей -->
        </dependency>
    </dependencies>
</dependencyManagement>

<!-- 2. Исключение конкретной транзитивной зависимости -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>

4. Управление в Gradle:

// 1. Исключение из конкретной зависимости
implementation('org.springframework.boot:spring-boot-starter-web') {
    exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'
}

// 2. Глобальные правила разрешения конфликтов
configurations.all {
    resolutionStrategy {
        // Принудительно использовать конкретную версию
        force 'com.google.guava:guava:32.0.0-jre'

        // Предпочитать последнюю версию при конфликте
        failOnVersionConflict()

        // Отключать транзитивные зависимости для всех
        // transitive = false // Не рекомендуется, лучше точечные exclude
    }
}

// 3. Запрет уязвимых версий через constraints
dependencies {
    constraints {
        implementation('org.apache.logging.log4j:log4j-core') {
            version {
                strictly '[2.17.0,)' // Запрещаем уязвимые версии <2.17.0
            }
        }
    }
}

5. Best Practices:

  • Регулярно обновлять зависимости (mvn versions:display-dependency-updates)
  • Использовать SCA-инструменты (Software Composition Analysis): Dependabot, Snyk, OWASP Dependency-Check
  • Фиксировать версии в CI/CD через dependency-lock файлы (Gradle) или maven-lockfile
  • Проверять лицензии (license-maven-plugin, gradle-license-plugin)
  • Минимизировать количество прямых зависимостей

Ответ 18+ 🔞

А, вот эта тема про транзитивные зависимости — это же просто песня, блядь! Сидишь такой, подключаешь одну библиотечку, а она, сука, тащит за собой пол-интернета, как пьяный мужик из подъезда друзей на шашлык. А потом оказывается, что эти «друзья» между собой на ножах, версии конфликтуют, и твой проект, вместо того чтобы собираться, начинает материться на тебя на непонятном языке ClassNotFound.

1. Какие подлянки эти транзитивы творят:

  • Версии дерутся: Одна библиотека орёт «Давай Guava 18.0!», а другая — «Нет, блядь, мне 32.0 нужна!». И стоит между ними твой бедный класслоадер, как ребёнок при разводе.
  • Сборка пухнет: Качается столько хлама, что итоговый jar-файл весит больше, чем установочный образ Windows 95. А нужна-то одна маленькая утилитка для работы с JSON.
  • Дыры безопасности: В глубине этого зоопарка сидит какая-нибудь библиотека версии 0.0.1-alpha, которую последний раз обновляли, когда Путин стал президентом в первый раз. И в ней, ясное дело, дыра размером с Калужскую область.
  • Лицензионный ад: Одна лицензия говорит «можно только смотреть», другая — «можно трогать, но не продавать», а третья — «вообще всё моё, иди нахуй». И ты сидишь и думаешь, не станет ли твой код потом собственностью Илона Маска.

2. Как посмотреть, что у тебя там в проекте завелось:

# Для мавенщиков: показывает дерево зависимостей, как родословную алкоголика
mvn dependency:tree
# Чтобы не читать всю эту простыню, ищешь конкретного виновника
mvn dependency:tree -Dincludes=com.google.guava:*

# Для градльщиков: та же фигня, но с другим акцентом
./gradlew dependencies
# Если зависимостей овердохуища, смотри только на нужный слой
./gradlew app:dependencies --configuration implementation

3. Если ты в лагере Maven, то вот тебе два главных козыря:

<!-- 1. Великий и ужасный Dependency Management — царь и бог версий. -->
<!-- Объявил тут версию — и она будет пропихана везде, как депутат в хороший ресторан. -->
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <version>2.15.0</version> <!-- Всё. Теперь везде будет 2.15.0. Никаких "а вот у меня 2.10.1". -->
        </dependency>
    </dependencies>
</dependencyManagement>

<!-- 2. Точечная зачистка. Не нравится, что spring тащит томкат? Вышвыриваем его нахуй. -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>

4. А если ты Gradle-мастер, то вот твои инструменты:

// 1. То же самое выпиливание, но на гроуви-синтаксисе.
implementation('org.springframework.boot:spring-boot-starter-web') {
    exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'
}

// 2. Ядерная дубинка — Resolution Strategy. Тут можно творить дичь.
configurations.all {
    resolutionStrategy {
        // Жёстко говорим: "Всем использовать Guava 32.0.0! Не нравится — идите лесом".
        force 'com.google.guava:guava:32.0.0-jre'

        // Говорим сборке падать, если версии поругались. Жестко, но честно.
        failOnVersionConflict()

        // ВАЖНО! Не вздумай просто так отключать ВСЕ транзитивы. Это как отрубить себе ногу, чтобы не чесалась пятка.
        // transitive = false // За комментировал, потому что это пиздец, а не решение.
    }
}

// 3. Защита от дурака (и от Log4Shell). Запрещаем подключать уязвимые версии.
dependencies {
    constraints {
        implementation('org.apache.logging.log4j:log4j-core') {
            version {
                strictly '[2.17.0,)' // Всё, что ниже 2.17.0 — даже не пытайся, говно опасное.
            }
        }
    }
}

5. Чтобы не было мучительно больно, запомни, чувак:

  • Обновляйся, блядь. Запускай mvn versions:display-dependency-updates и смотри, что уже протухло. Как холодильник раз в месяц.
  • Поставь сторожа. Используй Dependabot, Snyk или OWASP Dependency-Check. Пусть они орут на тебя, когда найдут дыру. Это лучше, чем когда на тебя будет орать клиент.
  • Заморозь версии. В Gradle — lock-файлы, в Maven — плагины для lockfile. Чтобы сегодня собралось, и завтра собралось, а не «ой, а у меня новая версия скачалась и всё сломалось».
  • Читай лицензии. Ну, или хотя бы запускай плагины, которые это делают за тебя (license-maven-plugin). А то потом окажешься в суде, ебать.
  • Меньше — значит лучше. Каждая прямая зависимость — это потенциальная головная боль. Подключай только то, без чего реально не прожить.