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

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

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

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

Комментарии (5)


  1. sshikov
    18.04.2022 20:25

    а чего это в 21 веке мы всё еще не перевели наш проект на Java-based конфигурации и наши разработчики постоянно правят XML?

    XML конфигурация — это средство времени выполнения. Аннотации — времени компиляции. Они обрабатываются в разные моменты. Если вам не нужна конфигурация в runtime — да, для вас Java-based конфигурации конечно же логичнее (я пожалуй соглашусь, что такие кому она нужна — весьма редки, но у меня таки было пару полезных применений). И кстати, у большей на вид части ваших ограничений реализации ноги растут именно отсюда.

    >Мое лично мнение, что любой Spring-проект рано или поздно стоит перевести на Java-based конфигурацию
    Таки не любой. Хотя идея вполне хороша, и если переводить — то наверное стоит это делать автоматически, насколько возможно.


    1. Dmitry2019
      19.04.2022 08:55

      Совсем не любой. У меня есть проект, который используется в нескольких компаниях. И, благодаря XML конфигурации, мне не нужно перекомпилировать приложение под каждого клиента. Хотя постоянно есть желание перейти на конфигурацию через код, каждый раз понимаешь, что это повредит конфигурябельности.


      1. sshikov
        19.04.2022 19:18

        Ну я примерно этим же и занимался. Только у меня скажем клиент — это на самом деле классы для доступа на хадуп кластер, и там есть разные версии оного хадупа, с разным набором зависимостей, и примерно одним API. И подгрузить контекст динамически в зависимости от версии — самое милое дело.


    1. maxim-kiryanov
      19.04.2022 08:58

      Я давно уже на Spring не работал. Но на сколько помню, Java-based != Аннотации.

      В Java-based можно вполне себе код писать, и это runtime.

      Вот на сколько это полная замена XML, не уверен (имхо, полная замена + удобства).


      1. sshikov
        19.04.2022 19:16

        Ну да, я говорил про аннотации, но в общем это не сильно меняет дело. Разница между рантаймом и временем компиляции кое-какая все равно останется, в частности, далеко не всегда у вас в наличии есть весь код всех классов, иногда приложение собирается из модулей, которые доступны и известны в рантайме. И классы некоторые становятся известны тогда же. И xml файлы конфигурации можно вполне (и я делал так много раз) подгружать динамически.

        docs.spring.io/spring-framework/docs/5.0.0.RELEASE/spring-framework-reference/core.html#beans-java

        Ну вот хоть сюда загляните.

        1.12. Java-based container configuration
        1.12.1. Basic concepts: Bean and @Configuration
        The central artifacts in Spring’s new Java-configuration support are @Configuration-annotated classes and Bean-annotated methods.

        Я не исключаю что тоже что-то забыл, но где тут не про аннотации?