Учебные материалы

Урок 2: Введение в Spring IoC контейнер

Этот урок освещает работу с Spring Framework IoC контейнером и основан на оригинальной документации §5. The IoC container.

Что вы создадите

Вы создадите некоторое количество классов, в которых будет рассмотрена функциональность Spring Framework IoC контейнера.

Что вам потребуется

Настройка проекта

Прежде чем вы начнете изучать этот урок, вам необходимо внести некоторые изменения в проект. Для начала создайте структуру папок src/main/resources и переместите в него файл настроек логгирования log4j.properties, тем самым поместив его в classpath проекта. Теперь немного измените файл сборки pom.xml, добавив и изменив в нем следующее:

...

<properties>
    <java-version>1.7</java-version>
    <sl4j-version>1.5.8</sl4j-version>
</properties>
...

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>jcl-over-slf4j</artifactId>
    <version>${sl4j-version}</version>
</dependency>

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-api</artifactId>
    <version>${sl4j-version}</version>
</dependency>

<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-log4j12</artifactId>
    <version>${sl4j-version}</version>
</dependency>

...

<build>
    <pluginManagement>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.2</version>
                <configuration>
                    <source>${java-version}</source>
                    <target>${java-version}</target>
                    <encoding>${file.encoding}</encoding>
                    <compilerArgument>-Xlint:all</compilerArgument>
                    <showWarnings>true</showWarnings>
                    <showDeprecation>true</showDeprecation>
                </configuration>
            </plugin>
        </plugins>
    </pluginManagement>
</build>

...

И наконец, создайте структуру папок src/main/java/lessons/starter/. В данном пакете вы будете создавать классы с методами public static void main(String[] args), которые вы будете запускать для того, чтобы можно было видеть результаты действий в процессе изучения данного материала.

Введение

Inversion of Control (IoC), также известное как Dependency Injection (DI), является процессом, согласно которому объекты определяют свои зависимости, т.е. объекты, с которыми они работают, через аргументы конструктора/фабричного метода или свойства, которые были установлены или возвращены фабричным методом. Затем контейнер inject(далее "внедряет") эти зависимости при создании бина. Этот процесс принципиально противоположен, поэтому и назван Inversion of Control, т.к. бин сам контролирует реализацию и расположение своих зависимостей, используя прямое создание классов или такой механизм, как шаблон Service Locator.

Основными пакетами Spring Framework IoC контейнера являются org.springframework.beans и org.springframework.context. Интерфейс BeanFactory предоставляет механизм конфигурации по управлению любым типом объектов. ApplicationContext - наследует нитерфейс BeanFactory и добавляет более специфичную функциональность. Ниже в таблице представлены различия между ними:

Функционал
BeanFactory
ApplicationContext
Инициализация/автоматическое связывание бина
Да
Да
Автоматическая регистрация BeanPostProcessor
Нет
Да
Автоматическая регистрация BeanFactoryPostProcessor
Нет
Да
Удобный доступ к MessageSource(для i18n)
Нет
Да
ApplicationEvent публикация
Нет
Да

В большинстве случаев предпочтительно использовать ApplicationContext, поэтому в дальнейшем будет использоваться только он и его реализации. Поскольку он включает в себя всю функциональность BeanFactory, его можно и нужно использовать, за исключением случаев, когда приложение запускается на устройствах с ограниченными ресурсами, в которых объем потребляемой памяти может быть критичным, даже в пределах нескольких килобайт, либо когда вы разрабатываете приложение, в котором необходима поддержка совместимости со сторонними библиотеками, использующими JDK 1.4 или не поддерживают JSR-250. Spring Framework активно использует BeanPostProcessor для проксирования и др., поэтому, если вам необходима поддержка такой функциональности, как AOP и транзакций, то при использовании BeanFactory необходимо добавить вручную регистрацию BeanPostProcessor и BeanFactoryPostProcessor, как показано ниже:

ConfigurableBeanFactory factory = new XmlBeanFactory(...);

// теперь зарегистрируем необходимый BeanPostProcessor экземпляр
MyBeanPostProcessor postProcessor = new MyBeanPostProcessor();
factory.addBeanPostProcessor(postProcessor);

// запускаем, используя factory
XmlBeanFactory factory = new XmlBeanFactory(new FileSystemResource("beans.xml"));

// получаем какое-то значения свойства из Properties-файла
PropertyPlaceholderConfigurer cfg = new PropertyPlaceholderConfigurer();
cfg.setLocation(new FileSystemResource("jdbc.properties"));

// теперь заменяем значение свойства на новое
cfg.postProcessBeanFactory(factory);
Аннотации @Autowired, @Inject, @Resource и @Value обрабатываются Spring реализацией BeanPostProcessor, поэтому вы не можете их применять в своих собственных BeanPostProcessor и BeanFactoryPostProcessor, а только лишь явной инициализацией через XML или @Bean метод.

Описание работы IoC контейнера

Ниже представлена диаграмма, отражающая, как работает Spring. Ваши классы приложения совмещаются с метаданными конфигурации, в результате чего будет создан и инициализирован ApplicationContext, а на выходе вы получите полностью настроенное и готовое к выполнению приложение.

ApplicationContext представляет собой Spring IoC контейнер и необходим для инициализации, настройки и сборки бинов для построения приложения.

В метаданных конфигурации разработчик описывает как инициализировать, настроить IoC контейнер и собрать объекты в вашем приложении. В данном и других уроках этого цикла везде, где возможно, будет использоваться подход на основе аннотаций и Java-конфигурации. Если вы сторонник XML-конфигурации, либо хотите посмотреть как делать тоже самое через XML, обратитесь к оригинальной документации по Spring Framework или соответствующего модуля/проекта.

Настройка IoC контейнера

Основными признаками и частями Java-конфигурации IoC контейнера являются классы с аннотацией @Configuration и методы с аннотацией @Bean. Аннотация @Bean используется для указания того, что метод создает, настраивает и инициализирует новый объект, управляемый Spring IoC контейнером. Такие методы можно использовать как в классах с аннотацией @Configuration, так и в классах с аннотацией @Component(или её наследниках). Класс с аннотацией @Configuration говорит о том, что он является источником определения бинов. Самая простейшая из возможных конфигураций выглядит следующим образом:

src/main/java/lessons/LessonsConfiguration.java

package lessons;

import org.springframework.context.annotation.Configuration;

/**
 * Конфигурационный класс Spring IoC контейнера
 */
@Configuration
public class LessonsConfiguration {
}
Полный @Configuration vs легкий @Bean режимы
Когда методы с аннотацией @Bean определены в классах, не имеющих аннотацию @Configuration, то относятся к обработке в легком режиме, то же относится и к классам с аннотацией @Component. Иначе, такие методы относятся к полному режиму обработки.
В отличие от полного, в легком режиме @Bean методы не могут просто так объявлять внутренние зависимости. Поэтому, в основном предпочтительно работать в полном режиме, во избежание трудноуловимых ошибок.

Для того, чтобы приступить к настройке и изучению Spring IoC контейнера, вы должны инициализировать ApplicationContext, который поможет также с разрешением зависимостей. Для обычной Java-конфигурации применяется AnnotationConfigApplicationContext, в качестве аргумента к которому передается класс, либо список классов с аннотацией @Configuration, либо с любой другой аннотацией JSR-330, в том числе и @Component:

src/main/java/lessons/starter/Starter.java

public class Starter {

    private static final Logger logger = LogManager.getLogger(Starter.class);

    public static void main(String[] args) {
        logger.info("Starting configuration...");

        ApplicationContext context = new AnnotationConfigApplicationContext(LessonsConfiguration.class);
    }
}

Как вариант, можно инициализировать контекст(ы) таким образом:

src/main/java/lessons/starter/Starter.java

public class Starter {

    private static final Logger logger = LogManager.getLogger(Starter.class);

    public static void main(String[] args) {
        logger.info("Starting configuration...");

        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.register(LessonsConfiguration.class);
        context.refresh();
    }
}

Использование @Bean аннотации

Как упоминалось выше, для того, чтобы объявить Bean-объект(далее просто бин), достаточно указать аннотацию @Bean тому методу, который возвращает тип бина как в классах с аннотацией @Configuration, так и в классах с аннотацией @Component(или её наследниках). Например, определим интерфейс какого-нибудь сервиса и его реализацию:

src/main/java/lessons/services/GreetingService.java

package lessons.services;

public interface GreetingService {
    String sayGreeting();
}

src/main/java/lessons/services/GreetingServiceImpl.java

package lessons.services;

public class GreetingServiceImpl implements GreetingService {

    @Override
    public String sayGreeting() {
        return "Greeting, user!";
    }
}

Теперь, для того, чтобы объект с типом GreetingService был доступен для использования, необходимо описать его в конфигурации следующим образом:

src/main/java/lessons/LessonsConfiguration.java

@Configuration
public class LessonsConfiguration {
    @Bean
    GreetingService greetingService() {
        return new GreetingServiceImpl();
    }
}

А для того, чтобы использовать его, достаточно выполнить следующее:

src/main/java/lessons/starter/Starter.java

public class Starter {

    private static final Logger logger = LogManager.getLogger(Starter.class);

    public static void main(String[] args) {
        logger.info("Starting configuration...");

        ApplicationContext context = new AnnotationConfigApplicationContext(LessonsConfiguration.class);
        GreetingService greetingService = context.getBean(GreetingService.class);
        logger.info(greetingService.sayGreeting());  // "Greeting, user!"
    }
}

Метод getBean() может принимать в качестве аргумента как класс(как показано выше), так и названия бина(подробнее будет рассмотрено ниже), либо другие варианты, с которыми вы можете ознакомится в документации. Однако такой подход не рекомендуется использовать в production-конфигурациях, т.к. для подобных целей существует механизм Dependency Injection (DI), собственно говоря, для чего и предназначен Spring IoC контейнер. Использование DI будет рассмотрено ниже в отдельной главе.

Именовать бины принято в соответствии со стандартным соглашением по именованию полей Java-классов. Т.е. имена бинов должны начинаться со строчной буквы и быть в "Верблюжьей" нотации.

По умолчанию, так, как будет назван метод определения бина, по такому имени и нужно получать бин через метод getBean() или автоматическое связывание. Однако вы можете переопределить это имя или указать несколько псевдонимов, через параметр name аннотации @Bean. Выглядеть это будет примерно так:

@Bean(name = "gServiceName")
@Bean(name = {"gServiceName", "gServiceAnotherNamed"})

Иногда полезно предоставить более подробное описание бина, например, в целях мониторинга. Для этого существует аннотация @Description:

@Bean
@Description("Текстовое описание бина greetingService")
GreetingService greetingService() {
    return new GreetingServiceImpl();
}

Жизненный цикл бина

Для управления контейнером жизненным циклом бина, вы можете реализовать метод afterPropertiesSet() интерфейса InitializingBean и метод destroy() интерфейса DisposableBean. Метод afterPropertiesSet() позволяет выполнять какие-либо действий после инициализации всех свойств бина контейнером, метод destroy() выполняется при уничтожении бина контейнером. Однако их не рекомендуется использовать, поскольку они дублируют код Spring. Как вариант, предпочтительно использовать методы с JSR-250 аннотациями @PostConstruct и @PreDestroy. Также существует вариант определить аналогичные методы как параметры аннотации @Bean, например так: @Bean(initMethod = "initMethod", destroyMethod = "destroyMethod").В качестве примера применения данных методов, интерфейсов и аннотаций вы можете ознакомиться в классе GreetingServiceImpl.

При совместном использовании методов, интерфейсов и аннотаций, описанных выше, учитывайте их порядок вызовов. Для методов инициализации порядок будет следующий:

  • Методы с аннотациями @PostConstruct в порядке их определения в классе
  • Метод afterPropertiesSet()
  • Метод, указанный в параметре initMethod аннотации @Bean

Для методов разрушения порядок будет следующий:

  • Методы с аннотациями @PreDestroy в порядке их определения в классе
  • Метод destroy()
  • Метод, указанный в параметре destroyMethod аннотации @Bean

Если вам необходимо реализовать свою собственную модель жизненного цикла бина, то в таком случае бин должен реализовывать один из интерфейсов, приведенных ниже:

public interface Lifecycle {

    void start();

    void stop();

    boolean isRunning();

}
public interface LifecycleProcessor extends Lifecycle {

    void onRefresh();

    void onClose();

}
public interface SmartLifecycle extends Lifecycle, Phased {

    boolean isAutoStartup();

    void stop(Runnable callback);

}

SmartLifecycle интересен тем, что наследует интерфейс Phased, в котором есть метод int getPhase();. Суть в том, что порядок создания бинов, реализующих этот интерфейс, зависит от возвращаемого методом значения и чем оно меньше, тем раньше всех будет создан бин и тем позже он будет разрушен.

Если вы на данном этапе запустите Starter.java, то в логах увидите, что методы разрушения не вызываются, однако программа завершает свою работу корректно. Дело в том, что для обычных приложений для этих целей стоит инициализировать контекст с типом AbstractApplicationContext, который также реализует ApplicationContext и имеет метод registerShutdownHook(). В итоге, у вас должно быть премерно следующее:

src/main/java/lessons/starter/Starter.java

public class Starter {

    private static final Logger logger = LogManager.getLogger(Starter.class);

    public static void main(String[] args) {
        logger.info("Starting configuration...");

        AbstractApplicationContext context = new AnnotationConfigApplicationContext(LessonsConfiguration.class);
        GreetingService greetingService = context.getBean(GreetingService.class);
        logger.info(greetingService.sayGreeting());  // "Greeting, user!"
        context.registerShutdownHook();
    }
}

После этого у вас появятся результаты работы методов при разрушении бина. Однако стоит заметить ещё раз, что это относится к обычным приложения, не относящимся к web-приложения(поскольку для них применяется отдельный тип контекста и подобный метод в них уже есть).

В некоторых случаях необходимо производить манипуляции с ApplicationContext'ом, например, в самом бине. Для этого существуют интерфейсы *Aware, полный список которых приведен в таблице 5.4 документации. Поэтому когда ApplicationContext создает экземпляр бина, он учитывает соответствующий интерфейс и передает ссылку на соответствующий ресурс.

Как было описано выше, Spring IoC контейнеру требуются метаданные для конфигурации. Одну из таких аннотаций мы уже рассмотрели, это @Bean, рассмотрим теперь и другие.

Другой основной аннотацией является @Component, а также её наследники @Repository, @Service и @Controller. Все они являются общими шаблонами для любых компонентов, управляемыми контейнеером. @Repository, @Service и @Controller рекомендуется использовать в тех случаях, когда вы можете отнести аннотируемый класс к определенному слою, например DAO, либо когда вам необходима поддержка функциональности, которую предоставляет аннотация. Также эти аннотации могут иметь дополнительный смысл в будущих версиях Spring Framework. В остальных же случаях достаточно использовать аннотацию @Component.

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

src/main/java/lessons/LessonsConfiguration.java

@Configuration
@ComponentScan
public class LessonsConfiguration {
    @Bean
    GreetingService greetingService() {
        return new GreetingServiceImpl();
    }
}

По умолчанию, такая конфигурация сканирует на наличие классов с аннотацией @Component и его потомков в том пакете, в котором сама находится, а также в подпакетах. Однако, если вы хотите, чтобы сканирование было по определенным каталогам, то это можно настроить, просто добавив в аннотацию @ComponentScan параметр basePackages с указанием одного или нескольких пакетов. Выглядеть это будет примерно таким образом: @ComponentScan(basePackages = "lessons.services"), а классу GreetingServiceImpl при этом необходимо добавить аннотацию @Component.

Стоит упомянуть ещё одну мета-аннотацию @Required. Данная аннотация применяется к setter-методу бина и указывает на то, чтобы соответствующее свойство метода было установлено на момент конфигурирования значением из определения бина или автоматического связывания. Если же значение не будет установлено, будет выброшено исключение. Использование аннотации позволит избежать NullPointerException в процессе использования свойства бина. Пример использования:

src/main/java/lessons/services/GreetingServiceImpl.java

package lessons.services;

public class GreetingServiceImpl implements GreetingService {

    private ApplicationContext context;

    @Required
    public void setContext(ApplicationContext context) {
        this.context = context;
    }
}

Области видимости(scopes) бинов

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

Вы можете контролировать не только какие зависимости и значения конфигурации вы можете подключить в объекте, который создан из определения бина, но также область видимости из того же определения бина. Это мощный и гибкий подход, при котором вы можете выбрать область видимости создаваемых объектов. Изначально, Spring Framework поддерживает несколько вариантов, некоторые доступны, только если вы используете web-aware ApplicationContext. Также вы можете создать свою собственную облать видимости. Ниже приведен список областей видимостей, описанных в документации на момент написания урока:

  • singleton - По умолчанию. Spring IoC контейнер создает единственный экземпляр бина. Как правило, используется для бинов без сохранения состояния(stateless)
  • prototype - Spring IoC контейнер создает любое количество экземпляров бина. Новый экземпляр бина создается каждый раз, когда бин необходим в качестве зависимости, либо через вызов getBean(). Как правило, используется для бинов с сохранением состояния(stateful)
  • request - Жизненный цикл экземпляра ограничен единственным HTTP запросом; для каждого нового HTTP запроса создается новый экземпляр бина. Действует, только если вы используете web-aware ApplicationContext
  • session - Жизненный цикл экземпляра ограничен в пределах одной и той же HTTP Session. Действует, только если вы используете web-aware ApplicationContext
  • global session - Жизненный цикл экземпляра ограничен в пределах глобальной HTTP Session(обычно при использовании portlet контекста). Действует, только если вы используете web-aware ApplicationContext
  • application - Жизненный цикл экземпляра ограничен в пределах ServletContext. Действует, только если вы используете web-aware ApplicationContext

С более подробной информацией о настройке приложения для применения областей видимости request, session, global session и application вы можете ознакомиться в документации. Пример реализации собственной области видимости будет рассмотрено в отдельном уроке.

Для того, чтобы указать область видимости бина, отличный от singleton, необходимо добавить аннотацию @Scope("область_видимости") методу объявления бина или классу с аннотацией @Component:

src/main/java/lessons/services/GreetingServiceImpl.java

@Component
@Scope("prototype")
public class GreetingServiceImpl implements GreetingService {
    //...
}

Использование @Configuration аннотации

Как упоминалось выше, классы с аннотацией @Configuration указывают на то, что они являются источниками определения бинов, public-методов с аннотацией @Bean.

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

src/main/java/lessons/LessonsConfiguration.java

@Configuration
@ComponentScan
public class LessonsConfiguration {
    @Bean
    BeanWithDependency beanWithDependency() {
        return new BeanWithDependency(greetingService());
    }

    @Bean
    GreetingService greetingService() {
        return new GreetingServiceImpl();
    }
}

Однако работает такое взаимодействие только в @Configuration-классах, в @Component-классах такое не работает.

Представим теперь ситуацию, когда у вас есть бин с областью видимости singleton, который имеет зависимость от бина с областью видимости prototype.

src/main/java/lessons/services/CommandManager.java

public abstract class CommandManager {
    protected abstract Object createCommand();
}

src/main/java/lessons/LessonsConfiguration.java

@Configuration
@ComponentScan
public class LessonsConfiguration {
    @Bean
    @Scope("prototype")
    public Object asyncCommand() {
        return new Object();
    }

    @Bean
    public CommandManager commandManager() {
        // возвращаем новую анонимную реализацию CommandManager
        // с новым объектом
        return new CommandManager() {
            protected Object createCommand() {
                return asyncCommand();
            }
        };
    }
}

Большая часть приложений строится по модульной архитектуре, разделенная по слоям, например DAO, сервисы, контроллеры и др. Создавая конфигурацию, можно также её разбивать на составные части, что также улучшит читабельность и панимание архитектуры вашего приложения. Для этого в конфигурацию необходимо добавить аннотацию @Import, в параметрах которой указываются другие классы с аннотацией @Configuration, например:

src/main/java/lessons/AnotherConfiguration.java

@Configuration
public class AnotherConfiguration {
    @Bean
    BeanWithDependency beanWithDependency() {
        return new BeanWithDependency();
    }
}

src/main/java/lessons/LessonsConfiguration.java

@Configuration
@ComponentScan
@Import(AnotherConfiguration.class)
public class LessonsConfiguration {
    @Bean
    GreetingService greetingService() {
        return new GreetingServiceImpl();
    }
}

Таким образом, при инициализации контекста вам не нужно дополнительно указывать загрузку из конфигурации AnotherConfiguration, все останется так, как и было:

src/main/java/lessons/starter/Starter.java

public class Starter {

    private static final Logger logger = LogManager.getLogger(Starter.class);

    public static void main(String[] args) {
        logger.info("Starting configuration...");

        ApplicationContext context = new AnnotationConfigApplicationContext(LessonsConfiguration.class);
        GreetingService greetingService = context.getBean(GreetingService.class);
        BeanWithDependency withDependency = context.getBean(BeanWithDependency.class);
        logger.info(greetingService.sayGreeting()); // "Greeting, user!"
        logger.info(withDependency.printText()); // "Some text!"
    }
}

В большинстве случаев, имеются такие случаи, когда бин в одной конфигурации имеет зависимость от бина в другой конфигурации. Поскольку конфигурация является источником определения бинов, то разрешить такую зависимость не является проблемой, достаточно объявить поле класса конфигурации с аннотацией @Autowired(более подробно оисано в отдельной главе):

src/main/java/lessons/AnotherConfiguration.java

@Configuration
public class AnotherConfiguration {

    @Autowired GreetingService greetingService;

    @Bean
    BeanWithDependency beanWithDependency() {
        //что-нибудь делаем с greetingService...
        return new BeanWithDependency();
    }
}

При этом LessonsConfiguration остается без изменений:

src/main/java/lessons/LessonsConfiguration.java

@Configuration
@ComponentScan
@Import(AnotherConfiguration.class)
public class LessonsConfiguration {
    @Bean
    GreetingService greetingService() {
        return new GreetingServiceImpl();
    }
}

Классы с аннотацией @Configuration не стремятся на 100% заменить конфигурации на XML, при этом, если вам удобно или имеется какая-то необходимость в использовании XML конфигурации, то к вашей Java-конфигурации необходимо добавить аннотацию @ImportResource, в параметрах которой необходимо указать нужное вам количество XML-конфигураций. Выглядит это следующим способом:

src/main/java/lessons/LessonsConfiguration.java

@Configuration
@ImportResource("classpath:/lessons/xml-config.xml")
public class LessonsConfiguration {
    @Value("${jdbc.url}")
    String url;
    //...
}

src/main/java/lessons/xml-config.xml

<beans>
    <context:property-placeholder location="classpath:/jdbc.properties"/>
</beans>

jdbc.properties

jdbc.url=jdbc:hsqldb:hsql://localhost/xdb

Процесс разрешения зависимостей

IoC контейнер выполняет разрешение зависимостей бинов в следующем порядке:

  • Создается и инициализируется ApplicationContext с метаданными конфигурации, которые описывают все бины. Эти метаданные могут быть описаны через XML, Java-код или аннотации
  • Для каждого бина и его зависимостей вычисляются свойства, аргументы конструктора или аргументы статического фабричного метода, либо обычного(без аргументов) конструктора. Эти зависимости предоставляются бину, когда он(бин) уже создан. Сами зависимости инициализируются рекурсивно, в зависимости от вложенности в себе других бинов. Например, при инициализации бина А, котый имеет зависимость В, а В зависит от С, сначала инициализируется бин С, потом В, а уже потом А
  • Каждому свойству или аргументу конструктора устанавливается значение или ссылка на другой бин в контейнере
  • Для каждого свойства или аргумента конструктора подставляемое значение конвертируется в тот формат, который указан для свойства или аргумента. По умолчанию Spring может конвертировать значения из строкового формата во все встроенные типы, такие как int, long, String, boolean и др.

Spring каждый раз при создании контейнера проверяет конфигурацию каждого бина. И только бины с областью видимости(scope) singleton создаются сразу вместе со своими зависимостями, в отличие от остальных, которые создаются по запросу и в соответствии со своей областью видимости. В случае цикличной зависимости(когда класс А требует экземпляр В, а классу В требуется экземпляр А) Spring IoC контейнер обнаруживает её и выбрасывает исключение BeanCurrentlyInCreationException.

Spring контейнер может разрешать зависимости между бинами через autowiring(далее, автоматическое связывание). Данный механизм основан на просмотре содержимого в ApplicationContext и имеет следующие преимущества:

  • Автоматическое связывание позволяет значительно сократить количество инструкций для указания свойств или аргументов конструктора
  • Автоматическое связывание позволяет обновлять конфигурацию, несмотря на развитие ваших объектов. К примеру, вам необходимо добавить зависимость в классе и эта зависимость может быть разрешена без необходимости модификации конфигурации. Поэтому автоматическое связывание может быть особенно полезным при разработке, не исключая возможность переключения на явное описание, когда кодовая база будет стабильна

Для того, чтобы воспользоваться механизмом автоматического связывания, Spring Framework предоставляет аннотацию @Autowired. Примеры применения приведены ниже:

src/main/java/lessons/services/AutowiredClass.java

public class AutowiredClass {

    @Autowired //к полям класса
    @Qualifier("main")
    //@Autowired(required = false) //чтобы не бросалось исключение,
                                   //если не с кем связать
                                   //рекомендуется использовать @Required
    private GreetingService greetingService;

    @Autowired //к полям класса в виде массива или коллекции
    private GreetingService[] services;

    @Autowired //к Map, где ключами являются имена бинов, значения - сами бины
    private Map<String, GreetingService> serviceMap;

    @Autowired //к конструктору
    public AutowiredClass(@Qualifier("main") GreetingService service) {}

    @Autowired //к обычным методам с произвольным названием аргументов и их количеством
    public void prepare(GreetingService prepareContext){/* что-то делаем... */}

    @Autowired //к "традиционному" setter-методу
    public void setContext(GreetingService service) { this.greetingService = service; }
}

Т.к. кандидатов для автоматического связывания может быть несколько, то для установки конкретного экземпляра необходимо использовать аннотацию @Qualifier, как показано ниже. Данная аннотация может быть применена как к отдельному полю класса, так и к отдельному аргументу метода или конструктора:

public class AutowiredClass {
    //...

    @Autowired //к полям класса
    @Qualifier("main")
    private GreetingService greetingService;

    @Autowired //к отдельному аргументу конструктора или метода
    public void prepare(@Qualifier("main") GreetingService greetingService){
        /* что-то делаем... */
    };

    //...

}

Соответственно, у одной из реализации GreetingService должна быть установлена соответствующая аннотация @Qualifier:

src/main/java/lessons/services/GreetingServiceImpl.java

@Component
@Qualifier("main")
public class GreetingServiceImpl implements GreetingService {
    //...
}

Spring также поддерживает использование JSR-250 @Resource аннотации автоматического связывания для полей класса или параметров setter-методов:

public class AutowiredClass {
    //...

    @Resource //По умолчанию поиск бина с именем "context"
    private ApplicationContext context;

    @Resource(name="greetingService") //Поиск бина с именем "greetingService"
    public void setGreetingService(GreetingService service) {
        this.greetingService = service;
    }

    //...

}

Использование стандартных JSR-330 аннотаций

Spring Framework поддерживает JSR-330 аннотации. Эти аннотации работают таким же способом, как и Spring аннотации. Для того, чтобы работать с ними, необходимо добавить в pom.xml следующую зависимость:

<dependency>
    <groupId>javax.inject</groupId>
    <artifactId>javax.inject</artifactId>
    <version>1</version>
</dependency>

Ниже приведена таблица сравнения JSR-330 и Spring аннотаций для DI:

Spring
javax.inject.*
Различия
@Autowired
@Inject
@Inject не имеет параметра required
@Component
@Named
-
@Scope("singleton")
@Singleton
По умолчанию, область видимости у JSR-330 похожа на Spring prototype. Поэтому, если вы хотите использовать область видимости в соответствии с Spring, стоит использовать именно Spring аннотацию @Scope.
@Qualifier
@Named
-
@Value
-
Нет аналога
@Required
-
Нет аналога
@Lazy
-
Нет аналога

Работа с окружением(Environment)

Environment является абстракцией, интегрированной в контейнер для работы с двумя ключевыми понятиями: профили и свойства.

Профили

Профиль является именованной группой определений бинов, зарегистрированных контейнером только в том случае, если профиль активен. Профиль определения бинов позволяет зарегистрировать различные бины для различных окружений. Например, у вас есть два стенда приложения, testing, где вы проводите тестирование на наличие ошибок и production, собственно работающее приложение. При этом у вас навеняка будут различаться какие-то параметры в окружениях, такие как подключение к БД и др. Профили вам очень помогут, т.к. вам не придется каждый раз изменять что-либо и пересобирать для каждого окружения.

Допустим, в вашей production-конфигурации определяется бин dataSource:

@Bean
public DataSource dataSource() throws Exception {
    Context ctx = new InitialContext();
    return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}

В то время, как для testing-конфигурации предназначены начальные sql-скрипты my-schema.sql и my-test-data.sql. Чтобы каждый раз не пересобирать приложение с разными конфигурациями, изпользуем профили:

@Configuration
public class AppConfig {

    // Для testing
    @Bean
    @Profile("dev")
    public DataSource devDataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.HSQL)
            .addScript("classpath:schema.sql")
            .addScript("classpath:test-data.sql")
            .build();
    }

    // Для production
    @Bean
    @Profile("production")
    public DataSource productionDataSource() throws Exception {
        Context ctx = new InitialContext();
        return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
    }
}
Если конфигурация имеет аннотацию @Profile, то все бины в ней и конфигурации, указанные в аннотации @Import, будут действовать, если активен соответствующий профиль, указанный в @Profile конфигурации. Например, если конфигурация имеет аннотацию @Profile({"p1", "p2"}), то она будет действовать, если профили "р1" и/или "р2" будут активны. При @Profile({"p1", "!p2"}) конфигурация действует, если профиль "р1" активен или профиль "р2" не активен.

Профили можно активировать несколькими способами, программно:

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("dev");
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();

Установкой значения переменной среды -Dspring.profiles.active="profile1,profile2", системное свойство JVM и даже через JNDI.

Вы также можете указать профиль по умолчанию через @Profile("default"), которое будет действовать только в том случае, если не задано ни одно из других свойств. Также вы можете задать свои значения профилей по умолчанию через метод Environment#setDefaultProfiles, либо через переменную среды spring.profiles.default.

Свойства

Свойства играют важную роль практически во всех приложениях и могут иметь происхождение из различных источников: свойства файлов, системные свойства JVM, окружение операционной системы, JNDI и др. Свойства представлены набором объектов PropertySource и значения в итоге получаются из тех же источников, как System.getProperties() и System.getenv(). Для получения значений применяется несколько способов:

ApplicationContext ctx = new GenericApplicationContext();
Environment env = ctx.getEnvironment();
boolean containsFoo = env.containsProperty("foo");
System.out.println("Does my environment contain the 'foo' property? " + containsFoo);
@Configuration
@PropertySource("classpath:app.properties")
public class AppConfig {
    @Autowired
    Environment env;

    @Bean
    public TestBean testBean() {
        TestBean testBean = new TestBean();
        testBean.setName(env.getProperty("testbean.name"));
        return testBean;
    }
}
@Configuration
@PropertySource("classpath:/com/${my.placeholder:default/path}/app.properties")
public class AppConfig {
    @Autowired
    Environment env;

    @Bean
    public TestBean testBean() {
        TestBean testBean = new TestBean();
        testBean.setName(env.getProperty("testbean.name"));
        return testBean;
    }
}

Где my.placeholder представлен в виде уже имеющегося свойства, такого как переменная среды или др.

Собственные условия работы бина или конфигурации

Spring Framework предоставляет возможность на основе вашего алгоритма включить или выключить определение бина или всей конфигурации через интерфейс @Conditional, в качестве параметра которого указывается класс, реализующий интерфейс org.springframework.context.annotation.Condition, с единственным методом matches(...), который возвращает значение true или false. Ниже приведен пример реализации аннотации @Profile:

@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
    if (context.getEnvironment() != null) {
        // Читаем атрибуты аннотации @Profile
        MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
        if (attrs != null) {
            for (Object value : attrs.get("value")) {
                if (context.getEnvironment().acceptsProfiles(((String[]) value))) {
                    return true;
                }
            }
            return false;
        }
    }
    return true;
}

Дополнительные возможности ApplicationContext

Как было сказано в самом начале, пакет org.springframework.beans.factory предоставляет базовую функциональность для управления и манипуляции бинами, включая программным способом. Пакет org.springframework.context добавляет интерфейс ApplicationContext, который расширяет интерфейс BeanFactory и другие, добавляя функциональность в более фреймворк-ориентированном стиле. Многие используют ApplicationContext в декларативном стиле, но даже не создавая его программно, а полагаясь на работу классов ContextLoader, автоматически инициализируется ApplicationContext, как часть запуска обычного Java EE web-приложения. Фреймворк-ориентированный стиль, помимо всего прочего, предоставляет такую функциональность, как:

  • Работа с сообщениями в i18n-стиле через MessageSource интерфейс
  • Доступ к ресурсам, таким как URL и файлы через ResourceLoader интерфейс
  • Публикация событий бинами, реализующими интерфейс ApplicationListener через использование ApplicationEventPublisher интерфейса
  • Загрузка нескольких(иерархических) контекстов, каждый из которых сосредоточен на конкретном слое, таком как web, через HierarchicalBeanFactory интерфейс

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

Для начала, вам необходимо создать файл с сообщениями в вашем classpath с содержимым:

src/main/resources/messages.properties

message=Alligators rock!
argument.required=The {0} argument is required.

Далее необходимо определить бин типа MessageSource в вашей конфигурации:

src/main/java/lessons/LessonsConfiguration.java

@Configuration
public class LessonsConfiguration {
    @Bean
    MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasename("messages");
        return messageSource;
    }
    //...
}

Тогда для получения сообщений нужно выполнить следующее:

src/main/java/lessons/starter/Starter.java

public class Starter {

    private static final Logger logger = LogManager.getLogger(Starter.class);

    public static void main(String[] args) {
        logger.info("Starting configuration...");

        ApplicationContext context = new AnnotationConfigApplicationContext(LessonsConfiguration.class);
        logger.info("Message: " + context.getMessage("message", null, Locale.getDefault()));
        logger.info("Argument.required: " + context.getMessage("argument.required", new Object[]{"Test_Argument"}, Locale.getDefault()));
        //...
    }
}

А в консоли должно отобразится следующее:

Message: Alligators rock!
Argument.required: The Test_Argument argument is required.

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

src/main/resources/messages_en_GB.properties

message=Alligators rock!
argument.required=Ebagum lad, the {0} argument is required, I say, required.

src/main/java/lessons/starter/Starter.java

public class Starter {

    private static final Logger logger = LogManager.getLogger(Starter.class);

    public static void main(String[] args) {
        logger.info("Starting configuration...");

        ApplicationContext context = new AnnotationConfigApplicationContext(LessonsConfiguration.class);
        logger.info("Message: " + context.getMessage("message", null, Locale.getDefault()));
        logger.info("Argument.required: " + context.getMessage("argument.required", new Object[]{"Test_UK_Argument"}, Locale.UK));
        //...
    }
}

Стандартные и собственные события

Обработка событий в ApplicationContext предоставляется через класс ApplicationEvent и интерфейс ApplicationListener. Если бин реализует ApplicationListener и развернут в контексте, то каждый раз, когда ApplicationEvent публикуется в ApplicationContext, уведомляется бин. Фактически, это реализация стандартного шаблона проектирования Observer. Spring предоставляет следующие стандартные события:

  • ContextRefreshedEvent - публикуется, когда ApplicationContext инициализирован или обновлен, например, при использовании метода AbstractApplicationContext#refresh()
  • ContextStartedEvent - публикуется, когда ApplicationContext запущен методом ConfigurableApplicationContext#start()
  • ContextStoppedEvent - публикуется, когда ApplicationContext остановлен методом ConfigurableApplicationContext#stop()
  • ContextClosedEvent - публикуется, когда ApplicationContext закрыт методом ConfigurableApplicationContext#close()
  • RequestHandledEvent - web-событие, которое оповещает о том, что все бины из HTTP запроса были обработаны. Применимо только для web-приложений с использованием Spring DispatcherServlet

Вы можете создать собственные события, расширяя класс ApplicationEvent:

public class BlackListEvent extends ApplicationEvent {

    private final String address;
    private final String test;

    public BlackListEvent(Object source, String address, String test) {
        super(source);
        this.address = address;
        this.test = test;
    }

    //...

}

Для публикации необходимо вызвать метод ApplicationEventPublisher#publishEvent(). Обычно это делается созданием класса, который реализует ApplicationEventPublisherAware и регистрируется как Spring бин:

public class EmailService implements ApplicationEventPublisherAware {

    private List<String> blackList;
    private ApplicationEventPublisher publisher;

    public void setBlackList(List<String> blackList) {
        this.blackList = blackList;
    }

    public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }

    public void sendEmail(String address, String text) {
        if (blackList.contains(address)) {
            BlackListEvent event = new BlackListEvent(this, address, text);
            publisher.publishEvent(event);
            return;
        }
        // отправка письма...
    }

}

В процессе конфигурирования, Spring контейнер определит, что EmailService реализует ApplicationEventPublisherAware и автоматически вызовет setApplicationEventPublisher(). В реальности, переданный параметр будет указывать на сам контейнер. Вы просто взаимодействуете с контекстом приложения через интерфейс ApplicationEventPublisher.

Для получения собственного события, создается класс, реализующий ApplicationListener и регистрируется как Spring бин:

public class BlackListNotifier implements ApplicationListener {

    private String notificationAddress;

    public void setNotificationAddress(String notificationAddress) {
        this.notificationAddress = notificationAddress;
    }

    public void onApplicationEvent(BlackListEvent event) {
        // уведомляются заинтересованные участники в notificationAddress...
    }

}

Итог

Лучше всего писать код в DI-стиле, где код, полученный из Spring IoC контейнера, уже имеет нужные зависимости, инициализированные во время создания контейнера. Кроме того, небольшие прослойки кода, которые требуются для связи с другими частями кода, можно использовать в singleton-стиле, что позволяет отделить его от другого кода и использовать все его преимущества. В целом, Spring IoC контейнер предоставляет хорошие возможности и способы для гибкой настройки инфраструктуры вашего приложения!

comments powered by Disqus