Конвертируем 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 позволяет делать многократные вложения бинов и это вполне - норм
<bean id="BeanWithConstructorWithCreateSubBeanWithSubBeanAndMap"
class="pro.akvel.spring.converter.testbean.BeanWithConstructorWithCreateSubBeanWithSubBeanAndMap">
<constructor-arg>
<bean class="pro.akvel.spring.converter.testbean.SubBeanWithSubBean">
<constructor-arg>
<bean class="pro.akvel.spring.converter.testbean.SubSubBean">
<constructor-arg>
<bean class="pro.akvel.spring.converter.testbean.SubSubBeanWithMap">
<constructor-arg type="java.lang.String">
<null/>
</constructor-arg>
</bean>
</constructor-arg>
</bean>
</constructor-arg>
</bean>
</constructor-arg>
</bean>
Пришлось реализовать рекурсивный обход описаний, как при проверке валидации бинов, так и в момент генераторе кода.
Поддержка импортов Java конфигураций
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:task="http://www.springframework.org/schema/task"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/task
http://www.springframework.org/schema/task/spring-task-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<context:annotation-config/>
<import resource="classpath:pro/akvel/spring/converter/xml/configs/spring-bean-configuration-import.xml"/>
</beans>
Да, оказывается все новое у нас в проектах уже пишется на Java-конфигурациях и чтобы сконвертировать старое нужно уметь зачитать и новые конфигурации.
Тут на помощь пришел спринговый ConfigurationClassPostProcessor, который как раз умеет дочитать описание бинов объявленных в классах.
В конверторе есть ключ для включения строгого режима, который проверяет, что все импорты присутствуют в classpath
А еще есть коллекции бинов
<bean id="BeanWithPropertyList"
class="pro.akvel.spring.converter.testbean.BeanWithPropertyList">
<property name="prop1">
<list>
<ref bean="bean1"/>
<ref bean="bean2"/>
</list>
</property>
<property name="prop2">
<set>
<ref bean="bean1"/>
</set>
</property>
</bean>
Довольно просто получилось добавить генерацию ArrayList и HashMap по коду.
Но остался не поддерживаемым случай, когда поле назначения типа массив, это спринг уже угадывает сам по коду проекта получая тип поля куда сеттятся Mergeable объекты.
А еще в XML можно переменные окружения использовать
<bean id="BeanWithPlaceholder"
class="pro.akvel.spring.converter.testbean.BeanWithPlaceholder">
<constructor-arg value="test${pl1}passed"/>
<constructor-arg value="${pl2}"/>
<property name="property1" value="test${pl1}passed"/>
<property name="property2" value="${pl2}"/>
<property name="property3" value="${pl1} and ${pl2}"/>
</bean>
Этот случай получилось поддержать. В момент генерации класса конфигурации переменные окружения добавляются полями с аннотацией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")
<bean id="my-bean" class="MyBean">
<property name="resource" value="my_file.txt" />
</bean>
Этот случай уходит в ручную доконвертацию. Т.е. утилита выставит 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;
}
...
}
И конфиг к нему
<bean id="my-bean" class="MyBean">
<constructor-arg ref="service1"/>
</bean>
Так вот, при отсутствии одного из параметра конструктора, 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 - если в проект работает много команд следует заранее договорится о переводе своего модуля, чтобы все были готовы.
Когда точно стоит переводить:
Проект активно развивается и изменяется - разработка станет сильно приятнее и эффективнее после конвертации.
Спасибо что дочитали, надеюсь было полезно (⊙‿⊙).