Конвертируем Spring XML в Java-based Configurations без слёз

748d2cb9b6061cbb750d3d1676f45c8b.png

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

  • Генерация конфигурации должна быть на базе собственной модели — чтобы был строгий контроль поддерживаемых описаний бинов.

  • Конвертируются типовые бины, вся «экзотика» допереводится руками.

В итоге схема работы утили получилась следующая:

23ebd775176b485861b61df299100745.gif

Через тернии к звездам

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

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

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

© Habrahabr.ru