Какие возможности предоставляет Spring для кастомизации своего поведения

    Всем привет. На связи Владислав Родин. В настоящее время я являюсь руководителем курса «Архитектор высоких нагрузок» в OTUS, а также преподаю на курсах, посвященных архитектуре ПО.

    Помимо преподавания, я занимаюсь написанием авторского материала для блога OTUS на хабре и сегодняшнюю статью хочу приурочить к запуску курса «Разработчик на Spring Framework», на который прямо сейчас открыт набор.




    Введение


    С точки зрения читателя код приложения, использующего Spring, выглядит достаточно просто: объявляются некоторые bean'ы, классы размечаются аннотациями, а дальше bean'ы inject'ятся куда нужно, все прекрасно работает. Но у пытливого читателя возникает вопрос: «А как это работает? Что происходит?». В этой статье мы попытаемся ответить на данный вопрос, но только не ради удовлетворения праздного любопытства.

    Spring framework известен тем, что он является достаточно гибким и предоставляет возможности для настройки поведения framework'а. Также Spring «изобилует» рядом достаточно интересных правил на применение некоторых аннотаций (например, Transactional). Для того, чтобы понять смысл этих правил, уметь их выводить, а также понимать что и как можно настраивать в Spring'е, необходимо понять несколько принципов работы того, что находится у Spring'а под капотом. Как известно, знание нескольких принципов освобождает от знания множества фактов. Я предлагаю ознакомиться с этими принципами ниже, если вы их, конечно, еще не знаете.

    Чтение конфигураций


    В самом начале работы необходимо распарсить конфигурации, которые есть в вашем приложении. Поскольку конфигураций бывает несколько видов (xml-, groovy-, java-конфигурации, конфигурация на основе аннотаций), для их чтения используются разные методы. Так или иначе собирается мапа вида Map<String, BeanDefinition>, в которой именам bean'ов ставится в соответствие их bean definition'ы. Объекты класса BeanDefinition представляют из себя мета-информацию по bean'ам и содержат id-шник bean'а, его имя, его класс, destroy- и init-методы.

    Примеры классов, принимающих участие в этом процессе: GroovyBeanDefinitionReader, XmlBeanDefinitionReader, AnnotatedBeanDefinitionReader, реализующие интерфейс BeanDefinitionReader.

    Настройка BeanDefinition'ов


    Итак, у нас есть описания bean'ов, но при этом самих bean'ов нет, они еще не созданы. Перед созданием bean'ов Spring предоставляет возможность настроить полученные BeanDefinition'ы. Для этих целей служит интерфейс BeanFactoryPostProcessor. Он выглядит следующим образом:

    public interface BeanFactoryPostProcessor {
        void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException;
    }
    

    Параметр метода данного интерфейса позволяет с помощью своего уже метода getBeanDefinitionNames получить имена, по которым можно вытаскивать BeanDefinition'ы из мапы и править их.

    Зачем это может понадобиться? Предположим, что некоторые bean'ы требуют реквизиты для подключения к какой-либо внешней системе, например, к базе данных. Мы хотим, чтобы bean'ы создавались уже с реквизитами, но сами реквизиты хранятся в property-файле. Мы можем применить один из стандартных BeanFactoryPostProcessor'ов — PropertySourcesPlaceholderConfigurer, который заменит в BeanDefinition'е название property на актуальное значение, хранящееся в property-файле. То есть фактически заменит Value(«user») на Value(«root») в BeanDefinion'е. Для того, чтобы это сработало, PropertySourcesPlaceholderConfigurer, конечно же, необходимо подключить. Но этим дело не ограничивается, вы можете зарегистрировать свой BeanFactoryPostProcessor, в котором реализовать любую нужную вам логику по обработке BeanDefinition'ов.

    Создание bean'ов


    На данном этапе у нас есть мапа, у которой по ключам располагаются имена bean'ов, а по значениям располагаются настроенные BeanDefinition'ы. Теперь необходимо создать эти bean'ы. Этим занимается BeanFactory. Но и здесь можно добавить кастомизацию, написав и зарегистрировав свой FactoryBean. FactoryBean представляет из себя интерфейс вида:

    public interface FactoryBean {
        T getObject() throws Exception;
        Class<?> getObjectType();
        boolean isSingleton();
    }

    Таким образом, BeanFactory создает bean сам, если отсутствует соответствующий классу bean'а FactoryBean, либо просит FactoryBean этот самый bean создать. Здесь есть маленький нюанс: если scope bean'а является singletone, то bean создается на данном этапе, если prototype, то каждый раз, когда этот bean будет нужен, он будет запрошен у BeanFactory.

    В итоге, у нас снова получается мапа, но уже несколько другая: по ключам находятся имена bean'ов, а по значениям сами bean'ы. Но это верно только для singletone'ов.

    Настройка bean'ов


    Теперь наступает самый интересный этап. У нас есть мапа, содержащая созданные bean'ы, но эти bean'ы пока еще не настроены. То есть мы не обработали аннотации, задающие состояние bean'а: Autowired, Value. Мы также не обработали аннотации, изменяющие поведение bean'а: Transactional, Async. Решить данную задачу позволяют BeanPostProcessor'ы, которые опять-таки являются реализациями соответствующего интерфейса:

    public interface BeanPostProcessor {
        Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;
        Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;
    }

    Мы видим 2 метода со страшными, но исчерпывающими названиями. Оба метода принимают на вход bean, у которого можно спросить какого он класса, а затем воспользоваться Reflection API для обработки аннотаций. Возвращают методы bean, возможно замененный на proxy.

    Для каждого bean'а перед помещением его в контекст происходит следующее: срабатывают методы postProcessBeforeInitialization у всех BeanPostProcessor'ов, затем срабатывает init-метод, а затем срабатывают методы postProcessAfterInitialization также у всех BeanPostProcessor'ов.

    У этих двух методов разная семантика, postProcessBeforeInitialization обрабатывает аннотации на состояние, postProcessAfterInitialization — поведение, потому как для обработки поведения применяется проксирование, что может привести к потере аннотаций. Именно поэтому поведение изменяется в самую последнюю очередь.

    Где же тут кастомизация? Мы можем написать свою аннотацию, BeanPostProcessor для нее, и Spring ее обработает. Правда, чтобы BeanPostProcessor сработал, его также необходимо зарегистрировать в виде bean'а.

    Например, для внедрения в поле рандомного числа мы создаем аннотацию InjectRandomInt (вешается на поля), создаем и регистрируем InjectRandomIntBeanPostProcessor, в первом методе которого обрабатываем созданную аннотацию, а во втором методе просто возвращаем пришедший bean.

    Для профилирования bean'ов создаем аннотацию Profile, которая вещается на методы, создаем и регистрируем ProfileBeanPostProcess'ор, в первом методе которого возвращаем пришедший bean, а во втором методе возвращаем прокси, оборачивающий вызов оригинального метода засечением и логгированием времени выполнения.



    Узнать подробнее о курсе


    OTUS. Онлайн-образование
    Цифровые навыки от ведущих экспертов

    Комментарии 2

      0
      Спасибо, хорошая статья!
        +1
        Спасибо за статью!
        Есть небольшая неточность:
        Поскольку конфигураций бывает несколько видов (xml-, groovy-, java-конфигурации, конфигурация на основе аннотаций), для их чтения используются разные методы.
        Существует 3 способа определения конфигураций (configuration definition): xml, groovy, java
        И 2 способа определения бинов (bean definition): внутри конфигурации, на основе аннотаций

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое