Конвертируем 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 — если в проект работает много команд следует заранее договорится о переводе своего модуля, чтобы все были готовы.

Когда точно стоит переводить:

  • Проект активно развивается и изменяется — разработка станет сильно приятнее и эффективнее после конвертации.

Спасибо что дочитали, надеюсь было полезно (⊙‿⊙).


ссылка на оригинал статьи https://habr.com/ru/post/661627/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *