Конвертируем Spring XML в Java-based Configurations без слёз
Как-то я засиделся на работе добавляя новую функциональность в один «небольшой» и довольно старенький сервис написанный на Spring.
Редактируя очередной XML файл Spring конфигурации я подумал:, а чего это в 21 веке мы всё еще не перевели наш проект на Java-based конфигурации и наши разработчики постоянно правят XML?
С этими мыслями и была закрыта крышка ноутбука в этот день…
Первый подход: «Да сейчас руками быстро все сконвертирую, делов-то!»
Вначале я попробовал решение в лоб: по-быстрому сконвертировать XML конфигурации в Java классы в текущей задаче.
Переведя с десяток бинов руками выходило, что на перевод одной конфигурации у меня уходит примерно час, а это значит что на перевод всего проекта уйдёт порядка недели.
И еще есть большая вероятность человеческого фактора внесения ошибок: не туда скопировал, перепутал порядок полей и т. д, а это еще трата N времени на ровном месте.
Плюс проектов с XML у меня на самом деле еще и несколько и надо бы и их перевести. Собственно в этот момент и появилась идея автоматизировать конвертацию.
Второй подход: «Автоматическая конвертация. От идеи к реализации»
Стало понятно, что тут нужна автоматическая конвертация. Была надежда, что уже есть что-то готовое и я быстро разберусь с этой проблемой, но оказалось что — нет.
Тогда появилась идея написать свою утилиту. Но чтобы не погрязнуть в Spring (а за много лет там было написано столько всего, что ого-го) было сделано решено ввести на старте несколько ограничений:
Конвертор не должен явно обращаться к классам проекта — это важное ограничение введено намерено чтобы не уйти во все тяжкие рефлексии и случайно не написать второй Spring. Исключения тут составляют Java конфигурации импортированные в XML.
Чтение конфигураций с bean definitions должно быть аналогично чтению самого Spring — теми же reader-ами — org.springframework.beans.factory.xml.XmlBeanDefinitionReader.
Генерация конфигурации должна быть на базе собственной модели — чтобы был строгий контроль поддерживаемых описаний бинов.
Конвертируются типовые бины, вся «экзотика» допереводится руками.
В итоге схема работы утили получилась следующая:
Через тернии к звездам
Часть из этого была известна заранее, часть нашлась по ходу дела — набралось много интересного про XML конфигурации Spring. Я просто обязан всем этим поделиться :-)
Spring допускает в XML конфигурация много вольностей и «трюков», самые интересные найденные много описаны ниже.
Spring позволяет делать многократные вложения бинов и это вполне — норм
Пришлось реализовать рекурсивный обход описаний, как при проверке валидации бинов, так и в момент генераторе кода.
Поддержка импортов Java конфигураций
Да, оказывается все новое у нас в проектах уже пишется на Java-конфигурациях и чтобы сконвертировать старое нужно уметь зачитать и новые конфигурации.
Тут на помощь пришел спринговый ConfigurationClassPostProcessor, который как раз умеет дочитать описание бинов объявленных в классах.
В конверторе есть ключ для включения строгого режима, который проверяет, что все импорты присутствуют в classpath
А еще есть коллекции бинов
Довольно просто получилось добавить генерацию ArrayList и HashMap по коду.
Но остался не поддерживаемым случай, когда поле назначения типа массив, это спринг уже угадывает сам по коду проекта получая тип поля куда сеттятся Mergeable объекты.
А еще в XML можно переменные окружения использовать
Этот случай получилось поддержать. В момент генерации класса конфигурации переменные окружения добавляются полями с аннотациейorg.springframework.beans.factory.annotation.Value
Часть бинов создается через фабрики
Тут все не так однозначно, если с фабричным методом все понятно (т.к. класс бина описывается в XML), то поддержку AbstractFactoryBean
на полях сделать не удалось, поэтому бины с фабриками пропускаются и остаются жить в XML.
А еще в XML можно кастомные неймспейсы делать и расширять DSL
Ну, а чего б нет, и их довольно много даже у самого Spring (Например: http://www.springframework.org/schema/c, http://www.springframework.org/schema/p). XML конфигурации с такими xmlns не смогут корректно прочитаться, поэтому первую конвертацию следует делать с флагом -s
что отловить и по возможности убрать кастомные xmlns.
А еще спринг умеет угадывать тип поля из XML
Привести String к int вообще мелочь, на самом деле даже можно досоздавать объекты (например поле Resource со значение file: — досоздаст объект класса FileSystemResource)
public class MyBean {
private Resource resource;
}
Спокойно прожевывает конфигурацию, и создавая new FileSystemResource("my_file.txt")
Этот случай уходит в ручную доконвертацию. Т.е. утилита выставит String значение в конструктор или seter, дальше нужно руками привести поле к нужному классу.
А еще можно писать код прямо в XML через Expression Language #{}
Большие ребята могут себе позволить поддерживать свой EL. Я к сожалению не могу :) Бины с EL будут пропущены и останутся в XML.
А еще спринг умеет сам поискать поля которые разработчик забыл прописать в XML
А вот это вообще киллер фича, которую лучше показать на примере:
Допустим есть класс:
public class MyBean {
private final MyBean1 service1;
private final MyBean2 service2;
MyBean(MyBean1 service1, MyBean2 service2) {
this.service1 = service1;
this.service2 = service2;
}
...
}
И конфиг к нему
Так вот, при отсутствии одного из параметра конструктора, Spring поищет бины с таким типом и если в контексте ровно один бин такого типа, то он его сам доавтовайрит и корректно создаст бин. Утилита конечно такое делать не умеет, т.к. не ходит в исходники проекта.
Это нужно будет поправить сами. В ходе конвертации наших проектов был найден ряд таких бинов и добавлены необходимые поля в ручную.
А еще в проекте XML файлы могут лежать по разным модулям и ссылаться на бины друг друга через ref без явных импортов или зависимости модулей
При сборке все XML конфигурации оказываются в ресурсах и Spring их найдет и корректно поднимет контекст. При это явный импорт одной XML конфигурации в другую не требуется.
Тут пришлось отказаться от перевода модулей одного репозитория по отдельности, а сделать сканирование всего проекта и создания общего описания бинов. И уже потом по этому общему описанию переводить конфигурации.
Я уж молчу что Spring вообще плевать на приватность полей и методов
Имхо это большой минут, т.к. теряется понимание зон видимости. При переводе на Java конфигурацию стало видно какие классы/методы/модули на сам деле не приватные в проекте и утекли. К счастью у меня таких классов было немного.
Итоги
В результате получилась универсальная утилита, которая позволила перевести часть наших активно развивающихся сервисов в Java конфигурацию, что значительно повысило удовольствие работы с проектами.
Какие плюсы конвертации можно выделить:
Повышена комфортность собственной работы, а так уменьшение по времени на правки конфигов: начинают в полную силу работать плюшки IDE: рефакторинг, автодополнение, генерация кода и т.д.
Повышена прозрачность конфигурации: нет неявной автоподстовновки, нет обращений к приватным классам, полям, классам
Все зависимости между модулями теперь стали явными — теперь если один модуль требует бин другом модуля, эту зависимость требуется явного объявить в pom.xml/build.gradle модуля, что позволяет отлавливать некорректную связанность при написании кода или на ревью.
Пример сконвертированной конфигурации
Код и релизы находятся тут — spring-xml-to-java-converter (лицензия MIT)
Что уже сейчас поддерживается:
Мультимодульные проекты.
Неявные зависимости конфигураций.
Автоматическое удаление сконвертированных бинов из XML конфигураций.
Бины без «id».
Параметризованные бины constructor-arg/property.
Бины с переменными окружения.
Вложенные бины.
Бины с list/set.
Бины с фабричным методом.
Аттрибуты lazy, depend-on, init-method, destroy-method, scope, primary.
Что НЕ поддерживается:
Именованные параметры конструкторов полей.
Абстрактные бины и бины с родителями.
Бины с фабриками, когда не указан явно класс создаваемого фабрикой объекта.
Бины с EL выражениями, либо со ссылкой на classpath.
Бины со ссылкой на бин, который отсутствует в созданном BeanDefinitionRegistry.
Подробная инструкция по работы с утилитой находится в readme репозитория.
P.S. Стоит или не стоит переводить свой проект?
Мое лично мнение, что любой Spring-проект рано или поздно стоит перевести на Java-based конфигурацию, но хочется отметить несколько важных моментов.
Когда стоит задуматься, а готов ли проект к переводу:
Если проект плохо покрыт тестами или вы не готовы потратить время на регрессионное тестирование.
Когда проект в архиве — не стоит переводить проект «на будущее» без релиза. Это может сыграть злую шутку, например когда нужно будет срочно выкатить hotfix.
Когда в XML конфигурации много самописных xmlns расширений — это все придется сконвертировать руками.
Когда проект является частью родительского проекта на XML — если в проект работает много команд следует заранее договорится о переводе своего модуля, чтобы все были готовы.
Когда точно стоит переводить:
Спасибо что дочитали, надеюсь было полезно (⊙‿⊙).