ТЕХНОЛОГИЯ ЭКСПЕДИА ГРУПП - ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ

Дзен и искусство разрешения зависимостей Maven

Практическое руководство по распутыванию проблем с зависимостями Maven

«О, какую запутанную паутину (зависимостей) мы плетем»

Пользователи Maven знают, что одной из наиболее часто используемых и мощных функций является Механизм зависимости. По большей части разрешение различных зависимостей в проекте просто работает без особых сюрпризов, и достаточно просто использовать сам Maven или инструменты, предоставляемые различными IDE, для выявления конфликтов версий. Однако что-то может пойти не так, когда есть взаимозависимые отношения между разными версиями одной и той же зависимости, особенно если новые версии артефакта не имеют обратной совместимости с предыдущими версиями. К сожалению, не все используют семантическое управление версиями должным образом (я смотрю на вас, Avro), поэтому часто недостаточно полагаться на номера версий для выявления проблем несовместимости. Вместо этого может потребоваться проверить содержимое различных артефактов и то, как они используются, чтобы определить причину проблем.

Проблемы, вызванные этими типами проблем с зависимостями, обычно не проявляются во время компиляции в коде, который вы пишете, а вместо этого проявляются в виде исключений и ошибок при фактическом запуске кода, например:

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

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

Типичные ошибки и исключения

«Хорошо, Хьюстон, у нас тут проблема»

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

  • java.lang.NoSuchMethodError - если ожидаемый метод не может быть найден в пути к классам во время выполнения. Это похоже, но немного отличается от NoSuchMethodException , поскольку метод мог присутствовать во время компиляции, но находился в версии зависимости с ограничением «предоставлено», которая больше не присутствует во время выполнения. Метод может действительно существовать, но типы и / или порядок переданных ему аргументов изменились от версии к версии. Дополнительные сведения см. В подробном сообщении об ошибке.
  • java.lang.NoSuchMethodException - если ожидаемый метод не может быть найден динамически во время выполнения, часто через отражение в каком-то коде, который вы не можете контролировать. Как указано выше, метод может действительно существовать, но типы и / или порядок переданных ему аргументов изменились от версии к версии. Дополнительную информацию см. В подробном сообщении об ошибке.
  • java.lang.NoSuchFieldError - если ожидаемого поля нет в версии зависимости, которая была разрешена.
  • java.lang.ClassNotFoundException - если ожидаемый класс не может быть найден динамически во время выполнения при использовании чего-то вроде Class.forName(). Оскорбительный вызов часто происходит «под капотом» в каком-то другом артефакте, который вы, вероятно, не контролируете, и может относиться к классу, который присутствует в другой версии зависимости.
  • java.lang.NoClassDefFoundError - если ожидаемое определение класса не может быть найдено в пути к классам во время выполнения. Это похоже, но немного отличается от ClassNotFoundException , поскольку класс мог присутствовать во время компиляции, но находился в версии зависимости, ограниченной как «предоставленная», которая отсутствует во время выполнения.
  • Различные другие дочерние классы java.lang.LinkageError - существует много странных и замечательных способов, с помощью которых классы могут изменяться без обратной совместимости, этот родительский класс захватывает большинство из них.

Поиск первопричины

«Потому что любовь к разным версиям - корень всех зол».

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

Плагин зависимостей Maven

Первый порт вызова, если вам нравится использовать CLI, - это Maven Dependency Plugin. Это дает ряд полезных целей, но поначалу их использование может показаться непосильным. Определенно стоит прочитать документацию, чтобы понять, что все они делают. Я считаю, что наиболее полезными целями для такого рода проблем являются:

  • dependency: tree - чтобы получить обзор всех зависимостей и их взаимосвязи.
  • dependency: copy-dependencies - чтобы выгрузить все артефакты в папку для дальнейшей проверки.

Инструменты IDE

Многие IDE имеют встроенные функции или плагины, которые предоставляют пользовательский интерфейс в той или иной форме поверх описанного выше Maven Dependency Plugin. В этом разделе рассматривается Eclipse, но многие другие IDE предлагают нечто подобное. Eclipse довольно хорошо предоставляет визуальный и текстовый способ изучения зависимостей в проекте Maven, включая транзитивные зависимости. Как правило, если вы знаете имя зависимости, в которой находится проблемный класс, вы можете сделать следующее:

  1. Щелкните вкладку «иерархия зависимостей».
  2. Введите имя зависимости в поисковый «фильтр» вверху справа.
  3. В представлении иерархии слева теперь будет отображаться Все, что зависит от этого артефакта, и вы обычно можете быстро обнаружить проблемы, если в вашем проекте или в зависимостях восходящего потока (или в обоих) используются совершенно разные версии зависимости.
  4. Представление «Эффективный POM» также может быть полезным, чтобы заставить Maven показать вам, как он объединяет все различные родительские POM и зависимости в один POM, который затем можно сканировать, чтобы увидеть окончательные разрешенные версии всех зависимостей.

На скриншоте ниже показан пример проекта с множественной зависимостью от разных версий Guava:

Не менее четырех различных основных версий Guava - разобраться в них будет очень весело.

Расположение исходного кода

Если вы можете изменить код приложения до того, как возникнет проблема, вы можете попросить загрузчик классов Java сообщить вам полное местоположение того, откуда он загружает проблемный класс, а затем записать это в журнал. Например, если бы мы пытались выяснить, какая версия CoreMatchers class использовалась и откуда она была загружена, мы могли бы сделать это:

System.out.println(CoreMatchers.class.
                   getProtectionDomain().
                   getCodeSource().
                   getLocation());

который, в свою очередь, распечатал бы что-то вроде:

file:/some/path/to/1.9.0/mockito-all-1.9.0.jar

Это может уже дать вам ключ к пониманию того, что происходит, если вывод показывает неожиданную версию в имени файла jar или, возможно, вы видите, что класс загружается из uber jar, который, как вы не осознавали, содержит дублированную версию класса. обсуждаемый.

Подробный загрузчик классов

Если у вас нет возможности изменить код, как описано выше, но вы можете передавать флаги javacommand, которая запускает приложение, то вы можете запустить загрузчик классов Java в подробном режиме, передав ему аргумент -verbose:class. Затем это будет регистрировать загрузку и выгрузку каждого класса во время выполнения. Обратной стороной этого подхода является то, что у вас будет много выходных журналов, которые нужно будет просмотреть, чтобы найти свою проблему, но разумное использование grep может помочь.

Так, например, если вы запустите команду java следующим образом:

java -verbose:class com.your.package.TestApplication

Затем вы увидите множество выводов в следующих строках:

[Loaded java.security.UnresolvedPermission from /usr/lib/jvm/jre/lib/rt.jar]
[Loaded java.security.BasicPermissionCollection from /usr/lib/jvm/jre/lib/rt.jar]
[Loaded com.expediagroup.dataplatform.TestApplication from file:/path/to/workspace/test-project/target/test-project-0.0.1-SNAPSHOT.jar]

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

Решения - включить, исключить, переместить или взломать

«Не вините Maven. Найти решение."

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

  • Найдите волшебную комбинацию зависимостей и версий, которые совпадают и не конфликтуют. К сожалению, иногда может случиться так, что внесение изменений устраняет первоначальную проблему, но приводит к появлению новой, поэтому может потребоваться изрядное количество проб и ошибок.
  • Будьте явным в отношении нужных вам зависимостей, а не полагайтесь на неявное разрешение транзитивных зависимостей. Явность может сделать ваш файл POM более подробным, но это цена, которую вы платите.
  • Выберите последнюю версию зависимости и внесите все необходимые изменения, чтобы все остальное использовало ее. Использование новейших и лучших версий полезно по ряду причин - безопасность, функции, исправления ошибок и т. Д. получите эти дополнительные преимущества. Конечно, это не всегда возможно, и вам, возможно, придется пойти на компромисс с более ранней версией, особенно если у вас есть другие зависимости вне вашего контроля, которые, в свою очередь, требуют более старых версий проблемной зависимости.
  • Сократите время цикла от внесения изменений до проверки, устраняет ли они проблему. В случаях, когда проблема может быть воспроизведена только при удаленном развертывании, это может означать написание нескольких простых сценариев оболочки для выполнения mvn package, а затем scp артефактов в другом месте. Не переусердствуйте, но время, проведенное здесь, может сэкономить вам много времени, потраченного на выполнение повторяемых вручную шагов.
  • Вносите по одному изменению за раз и проверяйте ситуацию, в которой оно воспроизводится. Если это решит проблему, запустите как можно больше других тестов, чтобы убедиться, что вы случайно не сломали что-то еще.

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

Добавить явную зависимость

Если ваш POM еще не имеет прямой зависимости от проблемной зависимости, вы должны сделать это и явно указать номер версии, а не использовать механизм зависимостей Maven для выбора этого на основе различных переходных конфликтов версий. Это заставит использовать определенную версию и переопределит любые другие переходные версии. Если все остальное работает с этой версией, то все готово. Часто это не так просто, и вам может потребоваться явно изменить версии различных других зависимостей и попытаться выровнять их все так, чтобы они работали с версией, которую вы выбрали. Обратите внимание, что это решение, вероятно, не поможет вам, если у вас есть путь к классам среды выполнения, который содержит версии зависимостей, которые вы не можете контролировать. В этом случае вам может потребоваться использовать затенение и перемещение (обсуждается позже).

Например, чтобы заставить конкретную версию Guava, которую вы могли наследовать как транзитивную зависимость раньше, добавьте прямую зависимость к определенной ее версии в свой POM:

<dependency>
  <groupId>com.google.guava</groupId>
  <artifactId>guava</artifactId>
  <version>30.1.1</version>
<dependency>

Исключить проблемные зависимости

Если вы зависите от артефакта, который имеет зависимость от проблемной версии другого артефакта, вы можете исключить эту зависимость и удалить ее с изображения. Помните, что вы можете использовать подстановочные знаки «*» для удаления нескольких артефактов с одинаковым Maven groupId, что может сделать вещи менее подробными.

Например, чтобы исключить проблемную версию Guava, от которой зависит hive-metastore (которое требуется для этого проекта), сделайте что-то вроде этого:

<dependency>
  <groupId>org.apache.hive</groupId>
  <artifactId>hive-metastore</artifactId>
  <version>${hive.version}</version>
  <scope>provided</scope>
  <exclusions>
    <exclusion>
      <groupId>com.google.guava</groupId>
      <artifactId>guava</artifactId>
    </exclusion>
  </exclusions>
</dependency>

Измените версии в восходящих проектах

Если у вас есть контроль над проектами, от которых вы зависите, и которые вызывают проблемы, вы можете внести в них изменения, чтобы использовать версии, которые будут работать ниже по течению. Часто это происходит, когда исходные проекты не обновляются до последних версий с новыми версиями, как ваш собственный проект. Итак, теперь вы можете стать хорошим гидом или бойскаутом и перетащить их в будущее, обновив их, чтобы использовать более новые версии, которые подойдут вам. Конечно, это может привести к всевозможным сопутствующим убыткам в восходящих проектах, которые вам теперь нужно исправить, и ваша проблема теперь выросла. Мне жаль вас, если это так, но утешайте себя, что вы делаете Правильное дело.

Затенение и перемещение проблемных классов

Если исходный проект зависит от версии артефакта, от которого также зависит ваш проект, и вы не можете вносить изменения в исходный проект, то вам следует подумать об использовании Maven Shade Plugin. Это используется для затенения (т.е. включения) и перемещения классов зависимости непосредственно в артефакт вашего проекта. Это делается путем переименования вышестоящих классов (и обновления всего импорта и т. Д.), Чтобы имена классов не конфликтовали с исходными именами. Это позволит вам использовать любую версию зависимости, которую вы хотите, не беспокоясь о других ее версиях, которые могут присутствовать, это особенно полезно, если вы не контролируете путь к классам во время выполнения. Обратите внимание, что если отражение используется для загрузки классов из проблемной зависимости или имена классов передаются через конфигурацию, это может вызвать проблемы - перемещение изменяет имена классов, поэтому эти механизмы потенциально будут неправильно загружать классы из неперемещенной версии зависимости. . Удачи с этим.

В качестве примера, чтобы использовать версию Guava, отличную от той, которая доступна в пути к классам среды выполнения, вы можете затенить и переместить пакеты Guava в файл jar вашего собственного артефакта с новым именем базового пакета с помощью префикса, такого как com.eg.shaded.com.google.* вместо com.google.*. . быть достигнуто так:

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-shade-plugin</artifactId>
  <version>3.1.0</version>
  <executions>
    <execution>
      <phase>package</phase>
      <goals>
        <goal>shade</goal>
      </goals>
      <configuration>        
        <artifactSet>
          <includes>
            <include>com.google.guava:guava</include>
          </includes>
        </artifactSet>
        <relocations>
          <relocation>
            <pattern>com.google</pattern>
            <shadedPattern>com.eg.shaded.com.google</shadedPattern>
          </relocation>
        </relocations>
      </configuration>
    </execution>
  </executions>
</plugin>

Если бы разработчики фреймворков различных сервисов, которые позволяют вам развертывать в них собственный код, понимали, как правильно изолировать пути к классам, вам не пришлось бы прибегать к подобным уловкам. К сожалению, разработчики Hadoop, Hive, Spark и т. Д. Не извлекли уроков из того, как контейнеры J2EE и Java Servlet справились намного за много лет до них.

«Те, кто не может извлечь уроки из истории, обречены повторять ее».

Создание собственной закрашенной и перемещенной версии исходных jar-файлов

Если вы окажетесь в ситуации, когда у вас есть несколько зависимостей восходящего потока (назовем их project-a и project-b), которые, в свою очередь, требуют разных конфликтующих версий такая же зависимость (library-d), и вы не можете изменить project-a или project-b, тогда вы в беде. Если вы используете версию library-d в project-a, тогда project-b генерирует исключение, и наоборот. Мне очень жаль, что до этого дошло, но выход есть. Вам нужно выбрать, какую версию library-d вы предпочитаете сохранить в качестве зависимости (возможно, самую новую версию), и удалить другую версию с изображения. В этом случае предположим, что вы решили сохранить версию project-b. Что вы можете сделать, так это создать модуль / проект Maven, который переупаковывает project-a (назовем это project-a-relocated), а также затеняет и перемещает версию библиотека-д. Это создаст новый артефакт, на который вы можете положиться вместо project-a.

Например, у нас есть project-a, который зависит от версии 11.0.1 Guava:

<dependency>
  <groupId>com.google.guava</groupId>
  <artifactId>guava</artifactId>
  <version>11.0.1</version>
</dependency>

и project-b, который зависит от версии 16.0.1:

<dependency>
  <groupId>com.google.guava</groupId>
   <artifactId>guava</artifactId>
   <version>16.0.1</version>
</dependency>

и ваш project-c, который зависит от них обоих:

<dependency>
  <groupId>com.expediagroup.dataplatform</groupId>
  <artifactId>project-a</artifactId>
  <version>1.0.0</version>
</dependency>

<dependency>
  <groupId>com.expediagroup.dataplatform</groupId>
  <artifactId>project-b</artifactId>
  <version>1.0.0</version>
</dependency>

Предположим, что теперь вы используете предыдущий совет и добавляете исключения или явные зависимости, чтобы принудительно выбрать Guava 11.0.1, но код из project-b затем не работает, поскольку он не может использовать эту более раннюю версию. Гуавы. Если вместо этого вы принудительно выберете Guava 16.0.1, тогда код из project-a завершится ошибкой, поскольку он не сможет использовать эту более позднюю версию Guava. Итак, что вы можете сделать сейчас, это создать новый проект Maven под названием project-a-relocated, чтобы сохранить все классы project-a «как есть», но переместить его использование Гуавы так:

<artifactId>project-a-relocated</artifactId>
<version>1.0.0</version>
<dependencies>
  <dependency>
    <groupId>com.expediagroup.dataplatform</groupId>
    <artifactId>project-a</artifactId>
    <version>1.0.0</version>
  </dependency>
</dependencies>
<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-shade-plugin</artifactId>
      <version>3.1.0</version>
      <executions>
        <execution>
          <phase>package</phase>
          <goals>
            <goal>shade</goal>
          </goals>
          <configuration>
           <artifactSet>
              <includes>
                <include>com.google.guava:guava</include>
                <include>com.eg.dataplatform:*</include>
              </includes>
            </artifactSet>
            <relocations>
              <relocation>
                <!-- only relocate guava classes -->
                <pattern>com.google</pattern>
                <shadedPattern>
                com.eg.dataplatform.shaded.com.google
                </shadedPattern>
              </relocation>
            </relocations>
          </configuration>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

И теперь в вашем проекте вместо того, чтобы зависеть от project-a, вы зависите от вывода вышеуказанного, то есть project-a-relocated:

<dependency>
  <groupId>com.expediagroup.dataplatform</groupId>
  <artifactId>project-a-relocated</artifactId>
  <version>1.0.0</version>
</dependency>

<dependency>
  <groupId>com.expediagroup.dataplatform</groupId>
  <artifactId>project-b</artifactId>
  <version>1.0.0</version>
</dependency>

Теперь у вас будет Guava 16.0.1, используемый project-b, в то время как project-a вместо этого будет использовать затененную и перемещенную версию Guava 11.0.1, которая была фактически удалена. из уравнения. Я никогда не говорил, что это будет кратко или красиво, но я видел эту работу.

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

Измените порядок пути к классам

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

Ужасы на примере реального мира

«Мечты разбиваются о камни реальности».

Мы закончим это реальным примером, в котором использовалась большая часть вышеперечисленных советов, с дополнительным сюрпризом из-за ужасающего шоу восходящего проекта, которое затеняет некоторые из его зависимостей , но затем не перемещает их. Когда это происходит, вы можете увидеть исключения, вызванные определенными классами, но вы не будете знать, что они вообще присутствуют, поскольку механизм зависимостей Maven не может их увидеть или сообщить вам. Вместо этого вам придется распаковать и проверить содержимое фактических jar-файлов зависимостей. Присоединяйтесь ко мне сейчас для этой последней печальной истории.

В этом примере у нас есть проект с открытым исходным кодом под названием Beekeeper, в котором есть модуль, который развертывается как приложение Spring Boot. При запуске он выдавал около 200 строк трассировки вложенного стека в файле журнала, но последние три строки указывали на проблему:

Caused by: java.lang.NoSuchMethodError: 'java.lang.String javax.servlet.ServletContext.getVirtualServerName()'
    at org.apache.catalina.authenticator.AuthenticatorBase.startInternal(AuthenticatorBase.java:1178) ~[tomcat-embed-core-9.0.16.jar:9.0.16]
    at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183) ~[tomcat-embed-core-9.0.16.jar:9.0.16]

Таким образом, похоже, что AuthenticatorBase Tomcat не доволен версией класса ServletContext Servlet API. В частности, в нем отсутствует метод getVirtualServerName(). Поэтому мы можем подозревать, что у нас есть разные версии зависимости servlet-api от пути к классам через различные транзитивные зависимости. Первое, что мы ищем, - это наличие нескольких зависимостей servlet-api с разными версиями, чтобы мы могли воспользоваться советом из более ранней версии и добавить некоторые исключения или явные зависимости для определенной версии, чтобы это работало. Итак, мы используем плагин зависимостей Maven, чтобы взглянуть, и вот что происходит:

mvn dependency:tree | grep servlet-api
...
[INFO] |  |  +- javax.servlet:servlet-api:jar:2.5:compile

Какие?!?! Существует только одна версия зависимости servlet-api от пути к классам, так как может возникнуть конфликт версий? Мы могли бы попробовать явно зависеть от другой версии serlvet-api, но их так много, какую из них выбрать? Это много проб и ошибок.

Битва между нами обострилась, теперь мы задаемся вопросом, может ли существовать другая версия класса ServletContext, тайно спрятанная внутри какой-то другой зависимости, чтобы полностью испортить наш день. Давайте посмотрим, скопировав все jar-файлы зависимостей в папку:

mvn dependency:copy-dependencies -DoutputDirectory=target/output

Теперь давайте просканируем все эти jar-файлы на предмет классов с именем ServletContext. Существуют различные визуальные инструменты, которые вы можете использовать для этого, или вы можете использовать некоторую магию bash в следующих строках:

for i in $(find . -name "*.jar"); do jar tvf $i | grep "javax/servlet/ServletContext.class" && echo $i ; done

4974 Mon Feb 04 16:30:46 GMT 2019 javax/servlet/ServletContext.class
./tomcat-embed-core-9.0.16.jar
1429 Wed May 10 14:20:30 BST 2006 javax/servlet/ServletContext.class
./servlet-api-2.5.jar

Подождите, это показывает, что класс ServletContext на самом деле присутствует в двух файлах jar - «servlet-api-2.5.jar» (чего и следовало ожидать) и «tomcat-embed-core-9.0.16.jar» (что на первый взгляд неожиданно). Здесь происходит следующее: Tomcat решил, что он будет только работать с определенной версией зависимости servlet-api. Не сразу понятно, какая версия используется, поскольку они не включили никаких метаданных в свой файл jar (tsk, tsk), но некоторые поиски в Интернете показывают, что это версия 4.0. Чтобы гарантировать использование этой версии, они решили включить классы servlet-api внутри их файла jar, чтобы, если у вас есть какая-либо другая версия servlet-api на пути к классам вы рискуете столкнуться с конфликтом. Это то, что здесь произошло, поскольку версия 2.5 явно старше 4.0 и не имеет прямой или обратной совместимости. Загрузчик классов среды выполнения также решил проявить жестокость и использовать режим упорядочивания классов «сломать все», чтобы поместить старый файл jar servlet-api выше в пути к классам, чтобы он загружался до tomcat-embed-core, который ломает все. Так что мы можем сделать? Если мы снова запустим это

mvn dependency:tree

мы можем посмотреть на иерархию, чтобы увидеть, откуда исходит транзитивная зависимость от версии 2.5 servlet-api (вывод обрезан, чтобы выделить ключевую информацию):

org.apache.hadoop:hadoop-mapreduce-client-core:jar:2.8.1:compile
 +- org.apache.hadoop:hadoop-yarn-common:jar:2.8.1:compile
    +- javax.servlet:servlet-api:jar:2.5:compile

Таким образом, встроенный Tomcat не работает со старой версией 2.5 servlet-api, от которой транзитивно зависит hadoop-mapreduce-client-core. В этом случае нам повезло в том, что то, как мы используем hadoop-mapreduce-client-core, не требует классов servlet-api, поэтому мы можем просто пойти с исключением :

<dependency>
  <groupId>org.apache.hadoop</groupId>
  <artifactId>hadoop-mapreduce-client-core</artifactId>
  <version>${hadoop.version}</version>
  <exclusions>
    <exclusion>
      <groupId>javax.servlet</groupId>
      <artifactId>servlet-api</artifactId>
    </exclusion>
  </exclusions>
</dependency>

и мы закончили. Номера версий теперь выровнены, из-за облаков выходит радуга, и приложение снова начинает работать.

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

Я оставлю вам модифицированную версию цитаты Роберта М. Пирсига, чья работа «Дзен и искусство ухода за мотоциклами» послужила основой для названия этой статьи:

«Лучшее место для улучшения взаимозависимостей в мире - сначала в собственном POM, а затем уже оттуда».

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

«Нам просто нужно продолжать, пока мы не выясним, что не так, или не выясним, почему мы не знаем, что не так».

Https://lifeatexpediagroup.com/