Spring Framework – многосторонний фреймворк, обладающий рядом возможностей, которые можно использовать как вместе, так и по отдельности:

  • Inversion of Control
  • упрощение доступа к базам данных
  • управление транзакциями
  • поддержка ORM – Object Relational Mapping для работы с базой данных на уровне объектов
  • поддержка аспектно-ориентированного программирования (Spring AOP)
  • … и много чего еще интересного!

Spring можно использовать не только в больших J2EE приложениях. Этот фреймворк подойдёт и для небольших проектов: как отдельно стоящих Java-приложений, так и веб-приложений, работающих в Servlet-контейнере, и даже апплетов. Я решил поделиться простым примером использования Spring в standalone приложении.

Первое, что я счёл необходимым использовать – Inversion of Control. IoC – это способ уменьшить зависимость отдельных компонентов системы друг от друга, при котором компонент “заявляет” зависимость от неких интерфейсов и сервисов, которые необходимы ему для работы, но понятия не имеет о том, каким образом эти интерфейсы и сервисы реализованы и откуда взять их реализацию. Простой пример “независимого” компонента, выдающего ленту новостей, можно записать так (с использованием JdbcTemplate – классического спобоба доступа к БД в Spring):

// NewsService.java
package springtest;
import java.util.List;
 
public interface NewsService {
    /** Возвращает список заголовков новостей */
    List<String> getHeaders();
}
// NewsServiceImpl.java
package springtest;
 
import java.util.List;
 
import javax.annotation.Resource;
import javax.sql.DataSource;
 
import org.springframework.jdbc.core.JdbcTemplate;
 
/** Реализация NewsService - загружает новости из базы данных */
public class NewsServiceImpl implements NewsService {
    private JdbcTemplate jt;
 
    /** @Resource - показывает, что этому объекту нужен DataSource для работы */
    @Resource
    public void setDataSource(DataSource ds) {
	jt = new JdbcTemplate(ds);
    }
 
    public List<String> getHeaders() {
	List<String> headers = jt.queryForList("SELECT header FROM news", String.class);
	return headers;
    }
}

Такой NewsServiceImpl уже может выполнять свою функцию при условии что некий “контейнер” передаст ему DataSource для доступа к базе. Таким контейнером может быть любой J2EE сервер приложений, но в случае простого приложения нет смысла использовать сервер. Все можно сделать гораздо проще: можно использовать Spring IoC.

Итак, наш NewsServiceImpl является полностью независимым и реюзабельным. Можно создать экземпляр NewsServiceImpl и работать с ним, как с обычным объектом (он и есть обычный объект), а можно поручить Spring обработать все зависимости, помеченные аннотацией @Resource. Тогда не придется вручную вызывать метод setDataSource. Сейчас покажем, как это сделать :) .

Во-первых, надо проделать инициализацию BeanFactory, через который будут создаваться объекты. BeanFactory можно сконфигурировать так, что он будет обрабатывать аннотации вроде @Resource и предпринимать некоторые действия – для @Resource это будет поиск подходящего объекта и назначение его туда, где стоит @Resource.

Создать и инициализировать BeanFactory можно разными способами, но проще всего, наверное, создать XML файл конфигурации (назовём его beans.xml):

<?xml version="1.0" encoding="UTF-8"?>
<!-- beans.xml -->
<beans
	xmlns="http://www.springframework.org/schema/beans"
	xmlns:tx="http://www.springframework.org/schema/tx"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="
	http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
	http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd
	http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd">
 
	<!-- Даём установку: интерпретировать аннотации -->
	<context:annotation-config />
 
	<!-- Конфигурируем подключение к базе данных -->
	<bean id="mainDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
		<property name="driverClassName" value="com.mysql.jdbc.Driver" />
		<property name="url" value="jdbc:mysql://localhost:3306/test1?useUnicode=true&amp;characterEncoding=UTF-8&amp;dumpQueriesOnException=true" />
		<property name="username" value="root" />
		<property name="password" value="1" />
	</bean>
 
	<!-- Транзакции тоже будем конфигурировать аннотациями, очень удобно :) -->
	<tx:annotation-driven transaction-manager="txManager" mode="proxy" />
 
	<!-- Менеджер транзакций: привязываем его к нашему DataSource -->
	<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<property name="dataSource" ref="mainDataSource" />
	</bean>
 
</beans>

и, собственно, инициализация и пример вызова NewsService:

// Main.java
package springtest;
 
import java.util.List;
 
import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.*;
import org.springframework.context.support.ClassPathXmlApplicationContext;
 
public class Main {
 
    /** Точка входа в программу */
    public static void main(String[] args) {
	Main me = new Main();
	me.init();
	me.run();
    }
 
    private ConfigurableApplicationContext cx;
 
    private void init(){ 
	cx = new ClassPathXmlApplicationContext("beans.xml", Main.class);
	DefaultListableBeanFactory bf = (DefaultListableBeanFactory) cx.getBeanFactory();
 
	// Добавляем в BeanFactory объявление сервиса, выдающего новости:
	bf.registerBeanDefinition("newsService",
		new AnnotatedGenericBeanDefinition(NewsServiceImpl.class));
    }
 
    private void run() {
	// Получаем сервис новостей (он тут же будет инициализирован)
	NewsService news = cx.getBean("newsService", NewsService.class);
 
	// Пользуемся :)
	List<String> headers = news.getHeaders();
	System.out.println("Headers: " + headers);
    }
}

Что же получилось? Spring сам создаёт экземпляр NewsService, производя его инициализацию, в соответствии с аннотациями в самом NewsServiceImpl. Причём “клиент” этого сервиса (в данном случае, это метод run()) не имеет ни малейшего представления, какие компоненты нужны NewsService для работы. Вся забота о конфигурировании и связи NewsService с необходимыми ему сервисами/компонентами ложится на Spring Framework. Удобно, не так ли?

Если однажды в NewsService добавтся ещё одна или несколько зависимостей, например от какого-нибудь RssService, то это повлияет только на NewsServiceImpl: в нём появится лишь что-то вроде

@Resource
private RssService rss;

или

@Resource
public void setRssService(RssService rss) { ... }

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

По мере “роста” проекта наверняка кроме NewsService появятся ещё некие “сервисы” и компоненты, которые точно так же можно зарегистрировать в BeanFactory. Тут стоит отметить, что способ регистрации объявлений в BeanFactory можно выбрать любой: можно разместить объявление в XML-конфигурации (DataSource в предыдущем примере как раз так и объявлен), а можно поступить, как с NewsService – добавить его объявление “руками” в java-коде. Насколько одно или другое удобнее – решается в каждом отдельном случае, но на мой взгляд, для небольшого проекта справедливо следующее:

  • “руками” зарегистрировать в BeanFactory можно те компоненты, которые не надо конфигурировать и которые обязаны всегда присутствовать в приложении. При этом объявления в java-коде позволяют отсеить часть возможных ошибок рано, еще на этапе компиляции. Кроме того, в этом случае отпадает необходимость правки внушительных XML, состоящих из одних статических объявлений, которые никогда не меняются.
  • в XML удобно вынести объявления bean-ов, у которых могут поменяться настройки; в нашем примере это подключение к базе данных. Неразумно конфигурацию подключения к БД забивать “хардкодом” :) . Ещё объявления бинов удобно хранить в XML, если используется test-driven разработка, тогда можно “на лету” заменить рабочий компонент заглушкой и тестировать проект.

А что делать, если требуется создать объект, который не описан в BeanFactory? Для этого в BeanFactory есть методы, позволяющие создать экземпляр указанного класса, после чего этот экземпляр будет “обработан” для разрешения его зависимостей, точно так же, как это делается с объявленными bean-ами. К примеру, создадим такой объект:

class TestClass {
    @Resource
    private NewsService news;
 
    public String getNews() {
	return news.getHeaders().toString();
    }
}

для этого достаточно написать:

TestClass test = (TestClass) cx.getBeanFactory().createBean(
	TestClass.class,
	AutowireCapableBeanFactory.AUTOWIRE_NO,
	false);
System.out.println("Test class: " + test.getNews());

где cx – это созданный ранее ApplicationContext из примера выше. Метод createBean() создает нам экземпляр TestClass, уже настроенный в соответствии с указанными в нём аннотациями. В документации по Spring Framework можно найти описание методов, которые “связывают” уже созданные объекты.

Код создания экземпляра можно обернуть в более удобный для использования класс:

public interface ObjectFactory {
    /**
     * Создаёт экземпляр implClass, разрешает зависимости и возвращает экземпляр
     * интерфейса interfaceClass, который реализован в implClass
     */
    <T> T createInstance(Class<? extends T> implClass, Class<T> interfaceClass);
}
 
public class SpringObjectFactory implements ObjectFactory {
    private final AutowireCapableBeanFactory factory;
 
    public SpringObjectFactory(AutowireCapableBeanFactory factory) {
	this.factory = factory;
    }
 
    @Override
    public <T> T createInstance(Class<? extends T> implClass, Class<T> interfaceClass) {
	Object bean = factory.createBean(implClass, AutowireCapableBeanFactory.AUTOWIRE_NO, false);
 
	// Возвращённый bean может быть на самом деле Proxy, если Spring
	// решит перехватить вызовы методов implClass
	// (это будет, если implClass использует декларативные транзакции, к примеру).
	// ПОЭТОМУ приведение типа к implClass может быть невозможно, но к
	// interfaceClass - запросто, если implClass реализует этот интерфейс.
	return interfaceClass.cast(bean);
    }
}

После чего можно добавить ObjectFactory в BeanFactory:

bf.registerSingleton("factory", new SpringObjectFactory(bf));

Далее, можно пользоваться удобным способом создания полностью сконфигурированных JavaBean-ов:

@Resource
public void setFactory(ObjectFactory factory) {
    NewsService news = factory.createInstance(NewsServiceImpl.class, NewsService.class);
    //.... пользуемся сервисом NewsService
}

Приведённый пример иллюстрирует одну из возможностей использования Spring IoC в проекте, благодаря которой облегчается проектирование приложения и устраняются зависимости между компонентами (но остаются зависимости от интерфейсов!). Для построения “красивой” системы достаточно каждый сервис/компонент представить в виде интерфейса, а потребителям этих сервисов добавить необходимые аннотации @Resource; зарегистрировать реализации этих сервисов в BeanFactory, после чего наслаждаться автоматическим связыванием компонентов!

Буду рад, если эта статья поможет кому-то. Спасибо за внимание.

  • Print this article!
  • Digg
  • del.icio.us
  • Facebook
  • Mixx
  • Google Bookmarks
  • E-mail this story to a friend!
  • LinkArena
  • LinkedIn
  • MisterWong
  • StumbleUpon
  • Technorati
  • Twitter