Главная > J2EE > Концепция Inversion of Control и основы Spring

Тема Зацепин
268

Java-разработчик 🧩
624
2 минуты

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

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

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

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

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

id="helloWorld"class="samples.HelloWorldImpl">name="message">Sergei

Этот простой фрагмент 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 соответственно).

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

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

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

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

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

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

Значение 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.

Примеры

id="moreComplexObject"class="example.ComplexObject">name="people">key="HarryPotter">The magic propertykey="JerrySeinfeld">The funny propertyname="someList">a list element followed by a referencebean="myDataSource"/>name="someMap">key="yup an entry">just some stringkey="yup a ref">bean="myDataSource"/>name="someSet">just some stringbean="myDataSource"/>

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

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

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

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

Теги: frameworksIoCj2eespring

Еще от автора

Применение WeakHashmap для списков слушателей

В статье от 11мая 1999 года Reference Objects были описаны основные идеи применения ссылочных объектов, но не приводилось детального описания. Данная статья позволит вам получить больше сведений, касающихся данной темы. В основном ссылочные объекты применяются для косвенных ссылок на память необходимую объектам. Ссылочные объекты хранятся в очереди (класс ReferenceQueue), в которой отслеживается доступность ссылочных объектов. Исходя из типа ссылочного объекта, сборщик мусора может освобождать память даже тогда, когда обычные ссылки не могут быть освобождены.

Заставки в Mustang

Согласно определению, данному в Wikipedia, заставка - это компьютерный термин, обозначающий рисунок, появляющийся во время загрузки программы или операционной системы. Заставка для пользователя является визуальным отображением инициализации программы. До выхода версии Java SE 6 (кодовое название Mustang) единственной возможностью применения заставки было создание окна, во время запуска метода main, и размещение в нем картинки. Хотя данный способ и работал, но он требовал полной инициализации исполняемой Java среды до появления окна заставки. При инициализации загружались библиотеки AWT и Swing, таким образом, появление заставки задерживалось. В Mustang появился новый аргумент командной строки, значительно облегчающий использование заставок. Этот способ позволяет выводить заставку значительно быстрее до запуска исполняемой Java среды. Окончательное добавление данной функциональности находится на рассмотрении в JCP.

Совмещение изображений

1 Введение 2 Правила визуализации и пример 3 Совмещение изображений в оперативной памяти 4 Постепенное исчезновение изображения 5 Ссылки и дополнительная информация

Еще по теме

Применение WeakHashmap для списков слушателей

В статье от 11мая 1999 года Reference Objects были описаны основные идеи применения ссылочных объектов, но не приводилось детального описания. Данная статья позволит вам получить больше сведений, касающихся данной темы. В основном ссылочные объекты применяются для косвенных ссылок на память необходимую объектам. Ссылочные объекты хранятся в очереди (класс ReferenceQueue), в которой отслеживается доступность ссылочных объектов. Исходя из типа ссылочного объекта, сборщик мусора может освобождать память даже тогда, когда обычные ссылки не могут быть освобождены.

Заставки в Mustang

Согласно определению, данному в Wikipedia, заставка - это компьютерный термин, обозначающий рисунок, появляющийся во время загрузки программы или операционной системы. Заставка для пользователя является визуальным отображением инициализации программы. До выхода версии Java SE 6 (кодовое название Mustang) единственной возможностью применения заставки было создание окна, во время запуска метода main, и размещение в нем картинки. Хотя данный способ и работал, но он требовал полной инициализации исполняемой Java среды до появления окна заставки. При инициализации загружались библиотеки AWT и Swing, таким образом, появление заставки задерживалось. В Mustang появился новый аргумент командной строки, значительно облегчающий использование заставок. Этот способ позволяет выводить заставку значительно быстрее до запуска исполняемой Java среды. Окончательное добавление данной функциональности находится на рассмотрении в JCP.

Использование потоков

1 Введение 2 Работа с выражениями типа Boolean 3 Класс JoptionPane 4 Приложение-счетчик 5 Ссылки

Перехват необрабатываемых исключений

В статье от 16 марта 2004 года Best Practices in Exception Handling были описаны приемы обработки исключений. В данной статье вы изучите новый способ обработки исключений при помощи класса UncaughtExceptionHandler добавленного в J2SE 5.0.

Использование класса LinkedHashMap

1 Введение 2 Сортировка хэш-таблицы 3 Копирование таблицы 4 Сохранение порядка доступа к элементам 5 Ссылки

Сказ про кодировки и java

С кодировками в java плохо. Т.е., наоборот, все идеально хорошо: внутреннее представление строк – Utf16-BE (и поддержка Unicode была с самых первых дней). Все возможные функции умеют преобразовывать строку из маленького регистра в большой, проверять является ли данный символ буквой или цифрой, выполнять поиск в строке (в том числе с регулярными выражениями) и прочее и прочее. Для этих операций не нужно использовать какие-то посторонние библиотеки вроде привычных для php mbstring или iconv. Как говорится, поддержка многоязычных тестов “есть в коробке”. Так откуда берутся проблемы? Проблемы возникают, как только строки текста пытаются “выбраться” из jvm (операции вывода текста различным потребителям) или наоборот пытаются в эту самую jvm “залезть” (операция чтения данных от некоторого поставщика).