Добро пожаловать в статью о миграции приложения Spring Boot на Java 17 - трудный путь. В первый день мы: пытались использовать Java 17 со Spring Boot 2.3.3.RELEASE, не сработало обновили Lombok и MapStruct не удалось обновить ASM, так как Spring переупаковывает ASM обновлен Spring Boot до версии 2.5.7 покрыли JUnit и FasterJackson завершили день компиляцией нашего кода и зелеными юнит тестами. В этом посте мы рассмотрим миграции:  Spring Cloud Spring Data Spring Kafka У нас хорошее начало, но мы еще не закончили. Давайте перекомпилируем все и посмотрим, где мы находимся: Похоже, у нас возникла проблема с одним из наших интеграционных тестов, поэтому давайте углубимся в Spring Cloud. Spring Cloud предоставляет ряд инструментов для разработки распределенных систем, работающих в облаке. В нашем проекте мы используем два модуля: Spring Cloud Kubernetes и Spring Cloud Netflix. В настоящее время мы используем Spring Cloud Hoxton, в частности версию Hoxton.RELEASE. Согласно матрице совместимости, Hoxton не поддерживает Spring Boot 2.5.x. Нам нужно обновить как минимум до Spring Cloud 2020.0.3 (обратите внимание, что здесь также используется новая схема версий). Поиск в GitHub класса org.springframework.boot.context.properties.ConfigurationBeanFactoryMetadata показывает, что он был удален в версии 2.4. Давайте продолжим и обновим нашу версию Spring Cloud до 2020.0.4 (последней версии исправления на момент написания этой статьи). После обновления моя IDE сообщает, что используемую нами зависимость больше нельзя разрешить. Мы рассмотрим это ниже. Для справки приведены примечания к версии Spring Cloud 2020.0 для всех версий исправлений. Spring Cloud Kubernetes помогает разработчикам запускать приложения в Kubernetes. Несмотря на то, что она имеет ряд интересных функций, мы используем только его поддержку внешней конфигурации. Конфигурация нашего приложения - вы знаете, application.properties|yml, которая настраивает Spring Boot приложение - хранится в k8s ConfigMap, и Spring Cloud Kubernetes делает эту внешнюю конфигурацию доступной для приложения во время запуска. Возвращаемся к коду, наша IDE жалуется, что зависимость spring-cloud-starter-kubernetes-config не может быть разрешена. Согласно примечаниям к версии, 2020.0 была проведена реструктуризация существующих модулей spring-cloud-kubernetes и представлен второй клиент на основе официального Java-клиента Kubernetes. Существующая реализация Fabric8 была переименована (чтобы было понятно, какой клиент используется). Пользователи Spring Cloud Kubernetes теперь могут выбирать между двумя реализациями: переименованный fabric8 starters, или новый Java-клиент Kubernetes Я искал указания, когда использовать одну или другую, но ничего не нашел в документации, только примечания к версии. Я нашел это сообщение в блоге Рохана Кумара, который провел довольно хорошее сравнение этих двух реализаций. Обязательно загляните в его блог, чтобы найти несколько очень хороших сообщений о k8s. То, что будет дальше, представляет только мой опыт и полученные уроки. У вас может быть другой опыт, и я хотел бы услышать о нем от вас. Воспользуемся новым официальным Java-клиентом Kubernetes, переключившись с существующего клиента fabric8. Ничего не имею против клиента fabric8, я просто предпочитаю использовать официальные вещи. Кроме того, нам не нужны какие-либо функции, которые предоставляет только клиент fabric8. Я удалил зависимость spring-cloud-starter-kubernetes-config и добавил новую: Сначала все выглядело многообещающе. Локально проект скомпилирован, и юнит тесты/тесты интеграции были зелеными. Потрясающе, подумал я, это было легко. Оказывается, все не так просто. Я закоммитил свои изменения в ветке и сделал push в Bitbucket. Признаюсь, я большой поклонник отдельных веток для фич и горжусь этим. Я знаю, что некоторые из моих коллег будут меня за это проклинать (подражая Thomas Traude). Через несколько минут я получил уведомление о том, что моя сборка Jenkins была красной. Но сборка прошла на моей машине! Похоже, у нас есть ненадежные тесты. В зависимости от среды контекст приложения может не загружаться. Это определенное разочарование, но не волнуйтесь, мне нравятся подобные проблемы. Если вы спрашиваете себя, почему тесты терпят неудачу, если сборки выполняются в Kubernetes, это потому, что они этого не делают. Наши Jenkins задания не выполняются в Kubernetes, поскольку мы широко используем Testcontainers. Если вы ими не пользуетесь, обязательно попробуйте их, здорово. И их новое облачное решение выглядит очень многообещающим. Spring Cloud Kubernetes можно отключить в тестах с помощью свойства spring.cloud.kubernetes.enabled. Поместите это свойство в свои тесты — вот так, и все готово (или, по крайней мере, раньше оно работало). Я сначала не понял, в чем проблема, но ее надо было исключить. Мы успешно использовали новый Java-клиент Kubernetes в других проектах, а здесь тесты не были нестабильными. Я посмотрел еще раз, оказалось, что в наших проектах используются разные версии. Версия 2020.0.1 работает нормально.  Изменение, вводящее дополнительные свойства конфигурации в Java-клиенте Kubernetes, имело непреднамеренный побочный эффект; свойство spring.cloud.kubernetes.enabled больше не работает должным образом. Больше нет единого свойства для отключения Spring Cloud Kubernetes. О проблемах сообщалось здесь и здесь, и исправления запланированы в версии 2020.0.5. К сожалению, на момент написания этой статьи версия 2020.0.5 еще не выпущена. Это улучшение было включено в Spring Cloud 2020.0.2, что объясняет, почему версия 2020.0.1 работала в других проектах.  Согласно документации, эти новые функции можно отключить. И обратите внимание, что вы можете отключить компоненты конфигурации, установив следующие свойства в контексте Spring: kubernetes.informer.enabled=false # disables informer injectionkubernetes.reconciler.enabled=false # disables reconciler injection Что делать? Вместо того, чтобы отключать эти дополнительные свойства в наших тестах, я выбрал другое решение.  Вернемся к клиенту Fabric8. Замените зависимость spring-cloud-starter-kubernetes-client-config на эту: Локально сборка зеленая. Выполняем Push в Jenkins и ждем. Всегда помогает скрещивание пальцев, я так и поступил. И вы знаете, отлично сработало; без проблем, ничего, ноль, zip. Я люблю, когда все работает.  Я должен был знать это. Клиент fabric8 хорошо нам служит уже много лет. Не вносите беспорядок в работающую систему! Похоже, что Java-клиент Spring Cloud Kubernetes еще не готов. Java-клиент Kubernetes представил собственную интеграцию Spring, которая не интегрируется должным образом с конфигурацией Spring. Надеюсь, эти два проекта сотрудничают, и в будущем мы получим красивую чистую конфигурацию на основе Spring. Как только выйдет версия 2020.0.5 я попробую еще раз.  Однако это действительно поднимает важную тему: доверие и уверенность в библиотеках, от которых мы зависим, и в поставляемом нами программном обеспечении. С одной стороны, в этом проекте было выполнено крупное обновление Spring Cloud, поэтому я ожидал, что что-то сломается. С другой стороны, учитывая, что изменение произошло в версии исправления, я не ожидал, что это произойдет. И это вызывает у меня опасения. Так как было легко обойти ошибку, зачем вообще упоминать об этом? Я считаю важным обсудить и оставить свой отзыв. Когда подобные изменения происходят в версиях исправлений, это может подорвать доверие и уверенность. Особенно, когда пользователи ожидают другого поведения.  Согласно собственному заявлению Spring, релизы следуют календарному плану версий (только что узнал об этом сам), причем проекты используют семантическое управление версиями. Номера версии MAJOR.MINOR.PATCH, увеличиваются так:  1. MAJOR версия при внесении несовместимых изменений API,  2. MINOR версия, когда добавляется функциональность обратно совместимым образом, и  3. PATCH версия, когда исправляются обратно совместимые ошибки. Я интерпретирую это как стремление избегать ситуаций, подобных описанным выше. Вы можете интерпретировать это по-разному. Я также понимаю, что s**t случается. В подобных ситуациях я вспоминаю старую поговорку: «Не трогай работающее программное обеспечение». В мире облачных вычислений мы должны быть готовы и иметь возможность обновлять наше программное обеспечение, когда это необходимо. В этом и заключается проблема. Я не знаю, почему в Spring Cloud Kubernetes клиент Kubernetes перепрыгнул на версию 11.0.0 вместо исправления.  Однако у нас есть работающее решение, поэтому давайте перейдем к следующему проекту Spring Cloud. Spring Cloud Netflix - это коллекция очень популярных и успешных OSS проектов, подаренных Netflix компании Spring. Наше приложение-шлюз API использует Spring Cloud Netflix Zuul для обеспечения маршрутизации к серверным системам, а также служб аутентификации и авторизации с помощью OpenID Connect.  Оказывается, Zuul перешла в режим поддержания еще в 2018 году и была удалена из spring-cloud-netflix в этой версии. Ее заменила Spring Cloud Gateway. Переход с Zuul на Spring Cloud Gateway займет больше суток. Мы решили оставить эту миграцию на другой день, так что мы можем получить работающую систему к концу этого дня. Для этого мы реорганизовали POM, поэтому наше приложение шлюза API остается на Java 11 и продолжает использовать 2.3.3.RELEASE версию Spring Boot. Помните, что мы не собирались обновлять Spring Boot, а включили Java 17. Если Zuul нельзя использовать с Java 17, пусть будет так. Надеюсь, в будущем мы сможем рассказать об этом в отдельном сообщении в блоге. Скоро нам придется перенести с Zuul из-за его EOL. Мы завершили обновление Spring Cloud, давайте перейдем к следующему Spring модулю в нашем проекте. Spring Data - это набор проектов, обеспечивающих доступ к данным знакомым образом на основе Spring. Как указано в примечаниях к версии, Spring Boot 2.5.x обновлен до Spring Data 2021.0. В частности, Spring Boot 2.5.7обновлен до Spring Data 2021.0.7. Нет необходимости импортировать спецификацию, spring-boot-starter-parent управляет зависимостями Spring Data за нас. Для справки приведены примечания к версии Spring Data 2021.0. Они не содержат много информации, но статья в блоге « Что нового в Spring Data 2010.0 » дает достойный обзор. Наше приложение использует Spring Data Rest для предоставления сущностей JPA как REST API. Правильно, просто определите свои объекты JPA, отметьте репозиторий и вуаля, у вас есть простое приложение CRUD, которое запускается менее чем за 5 минут. К сожалению, обновление было не таким быстрым. Компилируя наше приложение, мы получаем следующую ошибку: Следующий класс больше не компилируется: Похоже, интерфейс RepositoryRestConfigurer изменился. Я попытался исследовать для этого некоторые примечания к версии, но безуспешно (примечания к версии Spring Data не особенно подробны). Если посмотреть на код на GitHub, этот метод устарел в 3.4 M2 (2020.0.0) и удален в 3.5 M1 (2021.0.0). Поскольку мы пропустили Spring Boot 2.4.x, мы не видели уведомления об устаревании в Spring Data 2020.0.x. В противном случае мы могли бы перенести наш код до его удаления. Еще один пример того, почему лучше часто обновляться. Исправить несложно, CorsRegistryдобавлен в метод configureRepositoryRestConfiguration. Теперь наш класс выглядит так: Теперь наш код компилируется, но у нас есть несколько неудачных тестов. Некоторые из наших тестов терпят неудачу из-за следующей ошибки: Что-то еще изменилось в Spring Data Rest. Опять же, я ничего не нашел в примечаниях к версии, но отследил коммит “Prevent duplicate controller registrations through class-level @RequestMapping”, который изменил поведение. Когда мы обнаружили экземпляры @BasePathAwareController и @RepositoryRestController, теперь мы отклоняем типы, которые используют @RequestMapping на уровне класса это приводит к неизбежной регистрации контроллера с Spring MVC. Оказывается, мы делали именно это: Мы настраиваем конечные точки остальных данных с помощью файла @RepositoryRestController. Это все еще возможно, но код необходимо адаптировать. Аннотации @RequestMapping на классе должны быть удалены, и путь добавляется к каждому методу. К счастью, в нашем API есть только несколько методов, но я могу представить, что это разочарует для более крупных API. Я не проверял поведение в нашем существующем приложении, но я так интерпретирую проблему. При предыдущей обработке «у нашего приложения фактически было бы 2 конечные точки rest, одна обслуживалась Spring Data Rest, другая - Spring MVC». Но, как я уже сказал, я этого не проверял. После внесения этого изменения эти тесты становятся зелеными, но теперь у нас есть другая проблема. После этого изменения еще одна серия тестов перестала работать. В некоторых случаях конечные точки Spring Data Rest по умолчанию были настроены и больше не соответствуют, поэтому мы получаем ошибки 404 или 405. Кажется, что настроенные конечные точки должны точно соответствовать конечным точкам Spring Data Rest по умолчанию, иначе они не распознаются. Я предполагаю, что раньше это работало из-за аннотации @RequestMapping(“/entities”), которая была получена Spring MVC и обработана как обычная конечная точка. Однако мне не удалось проверить, и я обновлю эту статью, если и когда у меня появится дополнительная информация. По умолчанию конечные точки Spring Data Rest используют другой тип содержимого application/hal+json. Изменяя конфигурацию Repository Rest API, можно изменить тип содержимого по умолчанию на application/jsonи «большая часть» наших тестов снова начнет проходить. Помните класс CustomRepositoryRestConfigurer сверху? Добавим дополнительную конфигурацию: Это исправляет некоторые тестовые сценарии, но не все. К сожалению, мы столкнулись с проблемой с нашими версиями контроллеров Repository Rest. Мы пытаемся версировать API, используя разные типы содержимого, например, application/json для версии 1 и application/vnd.app.v2+json для версии 2. К вашему сведению - Spring Boot Actuator поддерживает такое управление версиями: application/json, application/vnd.spring-boot.actuator.v2+json и application/vnd.spring-boot.actuator.v3+json. Некоторые из наших тестов терпят неудачу с такой ошибкой: Это сработало с Spring Boot 2.3.3-RELEASE, и я могу только предположить, что это было обработано Spring WebMVC, а не Spring Data Rest. Мы так и не нашли решения этой проблемы с помощью Spring Data Rest, поэтому мы реорганизовали API с конечной точкой Spring WebMVC Rest. Если кто-нибудь, читающий это, знает, как добиться этого с помощью Spring Data Rest, свяжитесь со мной, я хотел бы узнать, как это сделать. При этом, возможно, нет смысла даже делать это. Я не могу спросить разработчиков, почему это было сделано именно так, их здесь больше нет. Историю этого проекта можно рассказать только через его историю Git. Обновить Spring Data Rest было непросто, но это не имело ничего общего с самим Spring Data Rest. Я подозреваю, что мы неправильно используем Spring Data Rest, неправильно смешивая с WebMVC концепцией. Если бы мы не сделали этого с самого начала, все было бы намного проще. Теперь мы закончили миграцию Spring Data Rest. Пришло время перейти к следующему модулю Spring, Spring Kafka. Spring Kafka, или, скорее, Spring для Apache Kafka, - отличный способ использовать Kafka в ваших Spring проектах. Он предоставляет простые в использовании шаблоны для отправки сообщений и типичные Spring аннотации для использования сообщений. Мы используем Kafka для связи между нашими приложениями. Запустив наши тестовые примеры Kafka, мы получаем следующую ошибку: Оказывается, мы настраивали bean-компонент consumerConfigs и устанавливали null значения в его свойствах. Следующее изменение с HashMap на ConcurrentHashMap означает, что мы больше не можем задавать в настройках null значения. Мы провели рефакторинги нашего кода, и теперь тесты зеленые. Очень просто. Другой тестовый пример выполнился с этой ошибкой: Некоторые из наших Java-компонентов используют a @JsonFilter для управления сериализацией и десериализацией. Для этого необходимо настроить propertyFilter в ObjectMapper. Spring для Apache Kafka внес изменения в JsonSerializer, представив ObjectWriter. При создании экземпляра ObjectWriter конфигурация ObjectMapper копируется, а не указывается. Наш тестовый пример переконфигурировал ObjectMapper с соответствующим propertyFilter после создания экземпляра ObjectWriter. Следовательно, ObjectWriter ничего не знает о propertyFilter (поскольку конфигурация уже была скопирована). После некоторого рефакторинга, изменения способа создания и настройки JsonSerializer, наши тестовые примеры стали зелеными. Запуск нашей сборки $ mvn clean verify наконец привел к зеленой сборке. Все работает как надо. Мы внесли наши изменения в Bitbucket, и билд был построен прекрасно. Обновление Spring Kafka было очень простым и понятным. Желаю, чтобы все было так просто. Spring и Spring Boot отлично документируют свои версии, их примечания к версии поддерживаются в хорошем состоянии. При этом обновление было сложной задачей, потребовалось довольно много времени, прежде чем все снова заработало. Большая часть этого лежит на нас, поскольку мы не следуем лучшим практикам, рекомендациям и т. д. Большая часть этого кода была написана, когда команда только начинала работу со Spring и Spring Boot. Код со временем развивался без рефакторинга и применения лучших практик. В конце концов, это вас настигнет, но мы используем это как опыт обучения и улучшения. Наши тестовые примеры теперь значительно лучше, и мы будем внимательно следить за их развитием. Эта статья описывает нашу историю миграции и может отражать вашу историю, а может и не относиться к ней. В зависимости от версии Spring Boot, с которой вы работаете, используемых вами функций и модулей Spring, которые вы интегрируете в свои приложения, ваша миграция будет сильно отличаться. В конце концов, миграция приложения на Java 17 был вопросом обновления нашей версии Spring Boot. Я уверен, что это не было сюрпризом для всех, но эта статья была про трудный, а не легкий путь. Это так же просто и так же сложно, как поддерживать наши зависимости в актуальном состоянии. Мы знаем, что это лучшая практика, но это еще не сделано. Я хорошо понимаю. До прихода в кодоцентрическую компанию я почти 20 лет занимался разработкой продуктов и полностью понимал конкурирующие приоритеты. Если мы чему-то научились за последнюю неделю, так это тому, насколько мы зависимы и уязвимы от OSS. Очень важно иметь возможность быстро двигаться и быстро обновляться. Мы должны привыкнуть к постоянному обновлению наших приложений и версий Spring Boot не реже одного раза в шесть месяцев. Процесс обновления более плавный при переходе от одной версии к другой, без пропуска версий. И имейте в виду, что версии Spring Boot поддерживаются около года до достижения EOL. К счастью, есть инструменты для автоматизации этого процесса, такие как Dependabot, Renovate, Synk. Эти инструменты автоматически сканируют ваши зависимости, постоянно ищут новые версии и создают pull запросы, когда новая версия доступна. Если вы используете GitHub, скорее всего, вы уже знакомы с Dependabot.