Статьи

Inject4Spring: принцип IoC для метаданных Spring

Inject4Spring — это небольшая библиотека, которая расширяет среду Spring, добавляя инверсию возможностей управления на уровне конфигурации контекста Spring.

Та проблема, которая должна быть решена

Некоторое время назад я писал о необходимости и возможностях определения зависимостей в контексте Spring, используя «противоположное» направление ссылок.

Spring отлично подходит для обеспечения возможностей инверсии управления (IoC) для кода Java. Он скрывает детали сборки приложения от кода и перемещает его на другой уровень — к настройке контейнера, который выполняет фактическое подключение и внедрение бинов.

Однако, хотя он освобождает разработчика от подробностей сборки приложения, он фактически не использует принцип IoC, как только мы начинаем работать на уровне конфигурации Spring. При создании метаданных (конфигурация XML для bean-компонентов) всегда следует знать, что bean-компонент имеет ссылки на другие bean-компоненты, должен знать имена этих bean-компонентов и т. Д. Здесь у нас есть четкая структура приложения, и иногда это может быть просто проблемой.

Одним словом, если у нас есть, скажем, два bean-компонента, определенных в контексте, и первый bean-компонент ссылается на другой bean-компонент, эта ссылка должна быть описана непосредственно в XML-файле конфигурации контекста bean-компонента как часть ссылки на определение bean-компонента. Это случайный и «естественный» Spring-способ определения зависимостей между компонентами.

Однако для некоторых типов приложений этот подход не работает. Основной недостаток этого заключается в следующем: при определении ссылающегося бина предполагается, что имя бина, к которому он относится, известно.

К сожалению, для приложений, построенных с использованием архитектуры плагинов, это предположение становится серьезным ограничением, поскольку оно не позволяет создавать действительно расширяемые приложения без необходимости изменения контекста Spring — конечно, это вообще не соответствует общей идее плагинов. И это еще более печально, если мы рассмотрим функциональность, которая уже включена в Spring — например, создание контекста приложения из нескольких файлов, которые могут быть разрешены динамически (скажем, с помощью прямого списка их местоположений или с помощью подстановочных знаков без classpath).

Следовательно, для получения действительно расширяемых приложений нам нужно иметь возможность «расширять» существующее содержимое контекста Spring.

С этой точки зрения приложение может включать в себя, например, один или несколько файлов конфигурации Spring с bean-компонентами, которые представляют «точки расширения», и несколько динамически загружаемых модулей плагинов, которые могут включать в себя bean-компоненты, которые могут быть подключены к этим точкам расширения.

Следующая картина иллюстрирует эту концепцию:

 

Что там следует рассматривать как точку расширения? Ну, ответ довольно прост — только свойства некоторых бобов. Мы назвали bean, мы назвали свойство — чтобы мы могли указать точку, в которой мы могли бы ввести нашу ссылку довольно точно.

Конечно, при расширении контекста нам нужно знать имена bean-компонентов и свойств, которым мы можем внедрять bean-компоненты из расширенных контекстов. Однако эта проблема полностью отличается от оригинального подхода Spring — в таком случае bean-компоненты в «основном» контексте представляют собой своего рода динамический API (довольно забавно, но я полагаю, что такое определение контекста может рассматриваться как API без API), и в момент определения bean-компонентов в «основном» контексте мы совершенно не знаем, как они будут настроены позже (или даже, возможно, сторонним разработчиком плагинов).

Хорошо, теперь, когда у нас есть точки расширения, единственное, что нам нужно, чтобы воплотить в жизнь всю концепцию такого внедрения, — это просто указать, что bean-компоненты должны быть подключены за пределами ссылки на определение bean-компонента.

Другими словами, здесь нам нужен механизм, который сказал бы Spring, что bean-компоненты должны быть подключены не в «основном» контексте, а непосредственно в «расширяющем» контексте.

Следующая картина иллюстрирует разницу между «нормальным» и «противоположным» направлениями ссылок:

Теперь мы почти готовы двигаться дальше. Единственное, что нам нужно рассмотреть на данный момент, это типы ссылок. Spring предоставляет несколько стандартных способов указания ссылок между компонентами:

  • используя обычную ссылку;
  • использование списка бобов;
  • используя карту бобов;
  • используя набор бобов.

И, конечно же, есть возможность указать значение конкретного свойства. Если мы посмотрим на эти способы определения отношений между bean-компонентами, станет ясно, что вполне возможно использовать противоположное направление внедрения для их поддержки.

Обзор Inject4Spring

Ну, это был фон для задач, которые решаются библиотекой Inject4Spring. Это небольшая (около 35 тыс. В jar) библиотека, которую я написал около года назад, чтобы поддерживать такие «противоположные» направления указания ссылок между bean-компонентами. На данный момент мы использовали его в нескольких проектах, разработанных в SoftAMIS . Inject4Spring выпущен под лицензией Apache, поэтому его можно использовать как в открытых, так и в коммерческих приложениях.

В целом, хотя он может использоваться для Spring 1.x, в первую очередь он ориентирован на Spring 2.x, так как он сильно зависит от функциональности пользовательских пространств имен, представленной в Spring 2.0. На самом деле, с точки зрения использования, все функциональные возможности этой библиотеки предоставляются через набор пользовательских тегов XML, которые принадлежат пространству имен «inject».

Вот краткий обзор возможных типов зависимостей в Spring и пользовательских тегов, включенных в Inject4Spring, которые им соответствуют:



В общем, основная цель Inject4Spring — предоставить возможности IoC для конфигурации bean-компонентов в контексте Spring (хотя на уровне кода эта задача решается Spring).

Как использовать Inject4Spring

Хорошо, было слишком много слов выше. Похоже, сейчас самое время привести несколько примеров. На самом деле, эти примеры взяты из контекста, который использовался для модульных тестов для Inject4Spring, поэтому они немного искусственны. Во всяком случае, они должны отражать использование довольно ясно

Прежде всего (при условии, что inject4spring.jar включен в classpath), необходимо добавить схему, предоставленную Inject4Spring, в контекст. Следующий фрагмент XML делает это:

<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:inject="http://www.soft-amis.org/schema/inject4spring"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.soft-amis.org/schema/inject4spring
http://www.soft-amis.org/schema/inject4spring.xsd">

 

Здесь мы просто добавили определение нагнетающих имен и определить схему XSD для него.

Хорошо, теперь давайте предположим, что у нас есть два довольно простых компонента:

  1. тот, который будет рассматриваться как описанный в «базовом» контексте (и который предоставляет несколько точек расширения через свои свойства) — позволяет этому компоненту иметь класс InjectionTargetBean;
  2. и еще один, который будет внедрен в этот бин через Inject4Spring — мы будем называть его класс InjectedBean;

Хотя не слишком важно, какие методы и свойства объявлены в классе InjectedBean , структура InjectionTargetBean представляется более важной для нашего примера. Поэтому мы покажем его структуру ниже (обратите внимание, что показаны только поля — соответствующие геттеры и сеттеры удалены для экономии места):

public class InjectionTargetBean {
protected List<String> fValuesList = null;
protected List<InjectedBean> fBeansList = null;
protected List<InjectedBean> fBeansListToCreate = null;
protected Set<InjectedBean> fBeansSet = null;
protected Set<String> fValuesSet = null;

protected String fValue = null;
protected InjectedBean fBeanRefEmpty = null;
protected InjectedBean fBeanRefOverride = null;

protected Map<String, String> fValuesMap = null;
protected Map<String, InjectedBean> fBeansMap = null;

// constructor, setters and getters are omitted....
}

Хорошо, теперь мы почти готовы. Давайте предположим, что у нас есть компонент с именем InjectTarget, который принадлежит классу InjectionTargetBean .

Вот как мы можем внедрить bean-компонент из расширения контекста в этот bean-компонент для различных сценариев:

  1. Инъекция боба в ссылку (краткая форма)
    <bean name="InjectionBean"
    class="org.softamis.inject4spring.InjectedBean">
    <inject:to-ref target="InjectTarget"
    name="beanRefEmpty"/>
    </bean>
  2. Инъекция боба в справочную (многословная форма)
    <inject:bean-to-ref target="InjectTarget"
    name="beanRefEmpty"
    ref="InjectionBean"/>
  3. Инъекция боба в список (краткая форма)
    <bean name="InjectionBean"
    class="org.softamis.inject4spring.InjectedBean">
    <inject:to-list target="InjectTarget" name="beansList"/>
    </bean>
  4. Инъекция боба в список (подробная форма)
    <inject:ref-to-list target="InjectTarget" 
    name="beansList"
    ref="InjectionBean"/>
  5. Инъекция боба в набор (краткая форма)
    <bean name="InjectionBean"
    class="org.softamis.inject4spring.InjectedBean">
    <inject:to-set target="InjectTarget" name="beansSet"/>
    </bean>
  6. Инъекция боба в набор (многословная форма)
    <inject:ref-to-set target="InjectTarget" 
    name="beansSet"
    ref="InjectionBean"/>
  7. Инъекция боба в карту (краткая форма)
    <bean name="InjectionBean"
    class="org.softamis.inject4spring.InjectedBean">
    <inject:to-map target="InjectTarget"
    name="beansMap" key="injected"/>
    </bean>
  8. Инъекция боба в карту (подробная форма)
    <inject:ref-to-map target="InjectTarget" 
    name="beansMap"
    ref="InjectionBean"
    key="injected"/>
  9. И вот несколько способов указать / переопределить значение простого свойства для целевого компонента
        <inject:value-to-property target="InjectTarget" 
    name="value"
    value="OverridenValue"/>

    <inject:value-to-list target="InjectTarget"
    name="valuesList"
    value="InjectedValue"/>

    <inject:value-to-set target="InjectTarget"
    name="valuesSet"
    value="InjectedValue"/>

    <inject:value-to-map target="InjectTarget"
    name="valuesMap"
    key="valueKey"
    value="InjectedValue"/>

Что ж, я надеюсь, что образцы наглядны и на самом деле есть только несколько вещей, которые следует добавить туда:

  1. Есть два набора тегов, которые делают то же самое, но в немного другой форме. Краткая форма тегов позволяет вставлять пользовательский тег XML как часть определения InjectionBean . Инъекция в подробном виде может использоваться на верхнем уровне (в основном, на том же уровне, где описывается тег bean-компонента, и он просто относится к целевому bean-компоненту и тому, который должен быть введен.
  2. Переопределение существующих значений допускается текущей реализацией. Другими словами, если изначально InjectionTargetBean был настроен так, чтобы иметь конкретные значения для простых свойств, ссылки на bean-компоненты и карты с указанными ключами — после использования тега внедрения, как указано выше, старые значения будут переопределены новым, предоставленным тегами из схемы ввода ;
  3. Возможно, это немного ограничивает, но во время выполнения bean-соединений Inject4Spring выполняет определение соответствующих свойств (свойств, для которых будет выполняться внедрение) и требует, чтобы они были явно определены до внедрения. Другими словами, даже если у вас нет соответствующей ссылки на момент определения InjectionTargetBean в контексте Spring (и там внедрение будет выполнено позже), оно все равно должно быть объявлено (скажем, с использованием нулевого элемента). Следующий пример иллюстрирует это:
      
    <bean name="InjectTarget"
    class="org.softamis.inject4spring.InjectionTargetBean">
    <property name="beanRefOverride">
    <bean class="org.softamis.inject4spring.InjectedBean"
    p:value="OriginalBeanToOverride"/>
    </property>
    <property name="beanRefEmpty">
    <null/>
    </property>
    <property name="beansList">
    <list>
    <bean class="org.softamis.inject4spring.InjectedBean"
    p:value="Original"/>
    </list>
    </property>
    <property name="beansListToCreate">
    <null/>
    </property>
    <property name="beansMap">
    <map>
    <entry key="toOverride">
    <bean class="org.softamis.inject4spring.InjectedBean"
    p:value="Original"/>
    </entry>
    </map>
    </property>
    <property name="beansSet">
    <set/>
    </property>
    <property name="value" value="ValueShouldBeOverridenByInjection"/>
    <property name="valuesList">
    <list/>
    </property>
    <property name="valuesMap">
    <map/>
    </property>
    <property name="valuesSet">
    <set/>
    </property>
    </bean>

    I understand that for someone that last requirement could be restrictive, but by our experience it’s quite convenient since allows to identify (more or less) possible extension points. Anyway, I»m open for your suggestions there.

How it works

I suppose that I will not dig into details too deep there. Anyway, it’s open source and all source code for Inject4Spring is freely available. If somebody will be interested, I can write separate entry regarding that.

However, I’ll just highlight implementation there. The general idea behind Inject4Spring is more than simple and utilizes all mechanisms provided by Spring framework. To be more precise, everything we need there is custom implementation of BeanFactoryPostProcessor, custom implementation of NamespaceHandler, small hacking of Spring bean factory internals (BeanDefinitionBuilder and related classes), strong coffee, good music and some time to mix that all pieces together.

The entire chain of processing is simple: custom NamespaceHandler is provided to Spring. During parsing custom tags, one is invoked and behind the scene creates custom BeanFactoryPostProcessor that is parametrized by list of commands that should be executed. One command, roughly speaking, is holder of data from appropriate custom tag.

As soon as BeanFactoryPostProcessor is invoked by Spring, it simply plays own list of commands and ones perform necessary setup of target bean within BeansRegistry.

So actually I’ve fooled Spring context there, since as soon as my BeanFactoryPostProcessor finished it’s operation, resulting definition of beans in Spring BeansFactory are the same as if we describe dependencies in usual Spring way. However, despite of that, such approach solves problems addressed at the begining of my entry.

License and download

Inject4Spring is released under Apache License, so it could be used both in open source and commercial applications. At the moment, you may to download it directly from this site, but probably later I’ll move it as project on SourceForge or something like that.

At the moment it’s quite stable and we’ve used it during last year for several projects. However, if you’ll have some comments, issues, requests for improvements — please do not hesitate contacting me.

Well, that’s all for now. Hope you’ll find it useful for you. Enjoy!