Концепция Inversion of Control и основы Spring

Добавлено : 6 Nov 2008, 18:20

Паттерны уменьшения зависимостей между компонентами системы

J2EE – большая и сложная спецификация, охватывающая, тем не менее, далеко не все нюансы реализации. Кроме того, многие реализации серверов приложений содержат возможности, выходящие за рамки спецификации. Разработка под конкретный сервер приложений вольно или невольно приводит к тому, что код приложения включает участки, зависимые от этого сервера. Это создает немало проблем при попытке переноса приложения под другой сервер. Spring переносим между разными серверами приложений, и поддерживает WebLogic, Tomcat, Resin, JBoss, WebSphere и другие серверы приложений.

Концепция, лежащая в основе инверсии управления, часто выражается "голливудским принципом": "Не звоните мне, я вам сам позвоню". IoC переносит ответственность за выполнение действий с кода приложения на фреймворк. В отношении конфигурирования это означает, что если в традиционных контейнерных архитектурах наподобие EJB, компонент может вызвать контейнер и спросить: "где объект Х, нужный мне для работы?", то в IoC сам контейнер выясняет, что компоненту нужен объект Х, и предоставляет его компоненту во время исполнения. Контейнер делает это, основываясь на подписях методов (таких, как свойства JavaBean) и, возможно, конфигурационных данных в формате XML.

В этой статье будут рассмотрены основные паттерны ослабления связей между компонентами системы, а также использование паттерна IoC в Sping Framework.

Для рассмотрения паттернов вынесения зависимостей будем использовать простой фрагмент кода, где класс LoginManager использует класс UserList, чтобы получить доступ к списку активных пользователей системы.

public class LoginManager {
  
private UserList myUserList = new UserList();
   .....
  
public boolean authenticateUser(String theUserName, String thePassword){
     
User aUser = myUserList.getUserByName(theUserName);
     
return thePassword.equals(aUser.getPassword());
  
}
  
....
}

Единственное тонкое место в этом коде – это то, что LoginManager зависит от UserList. У такой зависимости можно отметить следующие недостатки:

  • Если захочется каким-то образом изменить способ хранения пользователей, например, использовать базу данных или LDAP, придется переписать LoginManager, чтобы он создавал соответствующий класс для работы со списком пользователей.
  • Если предположить, что класс UserList зависим от платформы, например, использует JNI или пользуется каким-либо API, специфичным для какой-то платформы, то LoginManager будет также платформенно-зависимым.

Таким образом, из-за зависимости компонентов страдает переносимость и возможность их повторного использования.

Рассмотрим способы выхода из этой неприятной ситуации. Необходимо сделать так, чтобы класс LoginManager мог работать с любым хранилищем пользовательской базы. Для этого определим такие хранилища, как UserStorage.

public interface UserStorage{
  
User getUserByName(String theUserName);
}

public class UserList implements UserStorage { .... }

public class LoginManager {
  
private UserStorage myUserList = new UserList();
   .....
  
public boolean authenticateUser(String theUserName, String thePassword){
     
User aUser = myUserList.getUserByName(theUserName);
     
return thePassword.equals(aUser.getPassword());
  
}
  
....
}

Такая организация обладает гораздо большей гибкостью. Чтобы класс был действительно универсальным, достаточно добавить setUserStorage(UserStorage theStorage) в LoginManager. Любые способы хранения пользователей могут быть легко добавлены. Этот способ открывает дорогу для создания тестов отдельно для классов UserList и LoginManager. В случае с LoginManager может быть использован класс-заглушка MockUserStorage.


Рисунок 1. Начальная диаграмма классов.


Рисунок 2. Диаграмма классов с вынесением зависимости.

Итак, мы имеем прекрасные переносимые компоненты LoginManager, UserList, JdbcUserStorage, LdapUserStorage. Не стоит думать, что мы избавились от необходимости соединять их вместе. Для использования этих компонентов необходим некий класс RuntimeAssembler, который будет делать грязную работу по соединению компонентов в единую систему.

public final class SimpleSystemAssembler {
  
public void main(String[] args) {
     
LoginManager aManager = new LoginManager();
      aManager.setUserStorage
(new UserList());
      aManager.authenticateUser
("user", "test");
  
}
}

public final class ComplexSystemAssembler {
  
public void main(String[] args) {
     
LoginManager aManager = new LoginManager();
      aManager.setUserStorage
(new JdbcUserStorage("jdbc:mysql:...", "mysql",
           
"mysql"));
      aManager.authenticateUser
("user", "test");
  
}
}

RuntimeAssembler-классы не предназначены для повторного использования или наследования от них. В больших системах эти классы максимально запутаны. IoC-контейнеры как раз и предназначены для упрощения соединения компонентов. Все они позволяют использовать API и создавать RuntimeAssember-классы, при этом XML-конфигурация системы и RuntimeAssembler не понадобятся.

Альтернативой паттерну вынесения зависимости (Dependency Injection) является паттерн Service Locator. Он широко используется в J2EE. Так, ServiceLocator может инкапсулировать все JNDI-вызовы J2EE-системы или создание (получение) UserStorage-реализации в нашем примере. Основным отличием Dependency Injection и Service Locator является то, что в Service Locator для получения реализации UserStorage используется вызов объекта ServiceLocator (см. код ниже), в то время как в Dependency Injection ассемблер создает связь автоматически.

// Service Locator
public LoginManager()
{
  
myUserList = ServiceLocator.getUserStorage();
}


Рисунок 3. Диаграмма для Dependency Injection.


Рисунок 4. Диаграмма для Service Locator.

Очевидно, что в паттерне ServiceLocator есть зависимость между LoginManager и ServiceLocator, в то время как LoginManager не зависит от RuntimeAssembler в Dependency Injection. Каждый может выбирать один из этих IoC-паттернов, в зависимости от своих нужд и задач. Далее будет рассмотрен IoC-контейнер, реализованный в Spring Framework.

Введение в Spring

SpringFramework Spring Framework представляет собой набор готовых решений для использования всех основных Enterpise Java технологий — JDBC, ORM, JTA, Servlets/JSP, JMX и многих других. Абстрактные классы, фабрики и бины разработаны таким образом, чтобы программисту оставалось написать только свою логику.

Spring также содержит прекрасные классы, реализующие паттерн MVC (модель-вид-контроллер) для Web-приложений. Абстрактные DAO-классы упрощают работу с persistance layer, будь то JDBC, JDO или Hibernate, и позволяют избежать больших трудностей при смене технологии. Транзакции реализуются собственным Transaction API, который позволяет использовать различные менеджеры транзакций, в том числе JTA J2EE-сервера приложений и Hibernate. Не стоит думать, что Spring является просто IoC-контейнером, он также предоставляет фундаментальную библиотеку для использования известных Java-технологий


Рисунок 5. Возможности IoC-контейнера Spring

Простой пример

Рассмотрим особенности IoC-контейнера Spring. Сам контейнер представляет собой реализацию интерфейса BeanFactory. Обычно используется реализация XmlBeanFactory, которая читает runtime-конфигурацию из XML-файла.

<beans>
  <bean id="helloWorld" class="samples.HelloWorldImpl">
    <property name="message"><value>Sergei</value></property>

  </bean>
</beans>

Этот простой фрагмент XML создает в контейнере bean с именем helloWorld, и устанавливает его свойство message в Sergei. Реализация может быть следующей:

interface HelloWorld {
  
void sayMessage();
}

public class HelloWorldImpl implements HelloWorld {
  
private String myMessage;
  
  
public void setMessage(String theMessage) {
     
myMessage = theMessage;
  
}
  
  
public void sayMessage() {
     
System.out.println("Hello world!Hello " + myMessage + "!");
  
}
}

public class Application {
  
public static void main(String[] args) {
     
BeanFactory aBeanFactory = new XmlBeanFactory("sample-beans.xml");
      HelloWorld aHelloWorld =
(HelloWorld) aBeanFactory
            .getBean
("helloWorld");
     
// выводит "Hello world!Hello Sergei!" в System.out
     
aHelloWorld.sayMessage();
  
}
}

Создание объектов

В приведенном примере за конструирование объекта helloWorld отвечает контейнер – атрибут class элемента bean соответствует вызову конструктора без параметров. Spring поддерживает широкий спектр механизмов создания объектов – вызов конструктора с параметрами или без, использование фабрик классов или фабричных методов. Использование всех этих методов довольно прямолинейно, единственным нюансом является то, что при использовании конструктора с параметрами необходимо указывать либо тип, либо индекс параметра конструктора (атрибуты type и index соответственно).

  • Конструктор без параметра:
<bean id="helloWorld" class="sample.helloWorldImpl">
  ....
</bean>  
  • Конструктор с параметрами:
<bean id="exampleBean" class="examples.ExampleBean">
    <constructor-arg><ref bean="anotherExampleBean"/></constructor-arg>
    <constructor-arg><ref bean="yetAnotherBean"/></constructor-arg>
    <constructor-arg type="int"><value>1</value></constructor-arg>
</bean>
<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>
  • Фабричный метод:
<bean id="exampleBean" class="examples.ExampleBean" factory-method="createInstance">
    <constructor-arg><ref bean="anotherExampleBean"/></constructor-arg>
    <constructor-arg><ref bean="yetAnotherBean"/></constructor-arg>
    <constructor-arg><value>1</value></constructor-arg>
</bean>
<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>  
ПРИМЕЧАНИЕ

При использовании статического фабричного метода не гарантируется, что созданный объект будет того же класса, где определен фабричный метод.

  • Использование фабрики классов:
<bean id="exampleBean" factory-bean="factory" factory-method="createInstance">
    <constructor-arg><ref bean="anotherExampleBean"/></constructor-arg>
    <constructor-arg><ref bean="yetAnotherBean"/></constructor-arg>
    <constructor-arg><value>1</value></constructor-arg>
</bean>
<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>
<bean id="factory" class="examples.UserFactory"/>  

В элементе constructor-arg поддерживаются те же значения, что и в элементе property, полный их синтаксис будет рассмотрен далее.

В описаниях bean'ов могут быть заданы два режима работы – режим прототипа (используется по умолчанию) и режим синглтона (единственный экземпляр). В первом режиме каждый запрос объекта с заданным именем будет возвращать новый объект с указанными свойствами и связями, во втором режиме будет создан единственный экземпляр, и он будет возвращаться при каждом вызове к BeanFactory, независимо от контекста. Это поведение регулируется атрибутом singleton элемента bean.

// синглтон 
<bean id="helloWorld" class="sample.helloWorldImpl" singleton="true">
  ....
</bean>
// прототип 
<bean id="helloWorld" class="sample.helloWorldImpl" singleton="false"> 
  ....
</bean>
ПРИМЕЧАНИЕ

Значение singleton по умолчанию – true.

Установка зависимостей и свойств компонентов

Возникает вопрос: "Какие типы данных могут быть установлены при помощи элемента property?", - ответ: "Любые". Spring использует технологию JavaBeans для установки свойств объектов, эта технология частично используется в JSP для установки свойств типа String и примитивных типов, но в Spring она используется гораздо шире – интерфейс PropertyEditor позволяет устанавливать значения свойств любых типов. Поддерживаются стандартные коллекции из java.util: List, Set, Map, Properties, а также ссылки на объекты в контейнере по имени и значение null.

  • java.util.Properties – задается элементом props, отдельные свойства добавляются при помощи вложенного элемента prop, где атрибут key задает имя свойства, а текст внутри – значение.
  • java.util.Map – задается элементом map, отдельные элементы добавляются при помощи вложенного элемента entry, где атрибут key задает имя свойства, а значение – это значение внутреннего элемента. Внутренний элемент entry может представлять любой объект, в том числе и Map.
  • java.util.List, java.util.Set – представляются элементами list и set соответственно. Каждый внутренний элемент представляет собой значение элемента списка (множества).
  • ссылка на объект из контейнера – представляется элементом ref, причем ссылка на объект, определенный в том же файле, использует атрибут local, а в любом из конфигурационных файлов – bean.
  • Значение null соответствует литералу null.

Примеры

<beans>
  <bean id="moreComplexObject" class="example.ComplexObject">
    <!-- вызывает setPeople(java.util.Properties) -->
    <property name="people">
      <props>
        <prop key="HarryPotter">The magic property</prop>
        <prop key="JerrySeinfeld">The funny property</prop>
      </props>
    </property>
    <!-- вызывает setSomeList(java.util.List -->
    <property name="someList">
      <list>
        <value>a list element followed by a reference</value>
        <ref bean="myDataSource"/>
      </list>
    </property>
    <!-- вызывает setSomeMap(java.util.Map)-->
    <property name="someMap">
      <map>
        <entry key="yup an entry">
          <value>just some string</value>
        </entry>
        <entry key="yup a ref">
          <ref bean="myDataSource"/>
        </entry>
      </map>
    </property>
    <!-- вызывает setSomeSet(java.util.Set) -->
    <property name="someSet">
      <set>
        <value>just some string</value>
        <ref bean="myDataSource"/>
      </set>
    </property>

  </bean>
</beans>

Жизненный цикл объектов

Контейнер Spring поддерживает методы инициализации и разрушения объектов. В любом объекте, реализующем InitializingBean, после задания значений всех декларированных свойств будет автоматически вызван метод afterPropertiesSet. На самом деле реализация этого интерфейса не обязательна, Spring предоставляет возможность вызова любого метода, указанного в атрибуте init-method в определении bean-а. Аналогичная ситуация и с методом разрушения, который вызывается при разрушении контейнера, интерфейс и атрибут называются DisposableBean и destroy-method соответственно.

Другие возможности

В этой статье кратко описаны основные возможности контейнера Spring, за более подробным описанием возможностей можно обратиться к Spring Reference Documentation, замечу лишь, что Spring активно использует не только объектно-ориентированный подход, но и аспектно-ориентированный. Аспекты и замещения методов могут быть также легко прописаны в конфигурационном файле, как и bean. Таким образом, контейнер Spring предоставляет широкий спектр опций соединения компонентов в систему, и позволяет разработчику сосредоточиться на разработке переносимых компонентов с использованием ООП и развивающейся АОП. Огромным преимуществом этого контейнера перед другими является набор классов, позволяющих использовать современные Java/J2EE технологии совместно с классами вашей предметной области.