Spring: IOC

Spring: IOC


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

  • spring-core. Основные компоненты, классы, интерфейсы и т.д.
  • spring-beans. Включает IOC и Depencency Injection. Дополняет spring-core
  • spring-context. Реализует ApplicationContext. Дополняет 2 предыдущие библиотеки
  • spring-context-support. Позволяет подключать сторонние библиотеки
  • spring-expressions. Реализует SpringExpressionLanguage (SpEL) для управления объектами

@Component

Маркирует класс как бин, включая его в Spring Context на основе конфигурации из @Configuration после сканирования пакетов на наличие Spring бинов (см. @ComponentScan)

Карта аннотаций

@Value Добавляет переменные приложения в бин
@Autowired Выполняет добавление зависимогго бина. Может применяться для полей класса, конструктора и setter-методов. Посредством @Qualifier детализирует инъекцию
Аналогом является @Inject и @Resource из пакета javax.inject
@Required Помечает зависимость от бина как обязательную. Начиная с Spring 5.1 может применяться только в конструкторе
@Lazy Помечает инъекцию как отложенную. Поиск кандидатов на удовлетворение зависимости выполянется только когда к переменной действительно идет обращение (в runtime)
@DependsOn Позволяет манипулировать очередностью инициализации и разрушения бинов. Зависимый позже создается и раньше разрушается
@AliasFor Предоставляет возможность выдачи псевдонимов членам аннотации
@Secured Определяет доступные роли для вызова Spring Web методов

@Configuration

Вводит настройки работы Spring приложения. Может выполнять специфическую инициализацию бинов (см. @Bean) или дополнительные аннотации (см. @Profile, @Scope)

Создание конфигурации из XML

Для инициализации из XML можно воспользоваться несколькими опциями для строки поиска:

  • без префикса. Поиск в корневой директории для основного ClassLoader
  • classpath. Поиск в рамках известного JVM :classpath. Будет использован ClassPathXmlApplicationContext
  • file. поиск по абсолютному или относительному пути. Будет использован FileSystemXmlApplicationContext
  • http. Поиск используя указанный URL. Будет использован WebApplicationContext

Создание @Bean

Выполняется в классе с аннотацией @Configuration. Создаваемый бин должен создаваться в методе с целевым именем бина:

1
2
3
4
5
@Configuration
public class CfgBean {
@Bean
public MyBean myBean() {...}
}

По умолчанию все бины в Spring являются Singleton. Создание зависимых бинов так же допустимо

1
2
3
4
5
@Bean
public MyRootBean rootBean() { ... }

@Bean
public MyChildBean childBean(MyRootBean root) { ... }

В принципе, любой объект можно сделать бином, используя @Bean. При инъекции достаточно следовать рекомендациям по поиску кандидатов на инъекции.

@ComponentScan

Рекомендуется передавать пакет (в виде строки), в рамках которого будет осуществляться поиск бинов. В случае отсутствия данного ограничения Spring будет искать бины во всех библиотеках, входящих в известный JVM :classpath

static и @Bean

В случае если метод, создающий бин будет иметь идентификатор static, он будет создаваться в момент инициализации ApplicationContext, т.е. до самого бина @Configuration

Бин Environment

Предоставляет доступ к окружению, в котором работает Spring

Типы инъекций

В случае XML конфигурации, уникальность бинам придается с помощью атрибутов id или name. Для конфигурирования аннотациями используется имя класса (может быть передано при определении аннтотации).
Аннотации, которые автоматически создают бины: @Component, @Repository, @Service, @Controller, а так же, все расширяющие их.

При регистрации нового бина с помощью @Bean можно передать несколько псевдонимов для него:

1
2
@Bean(name = {"beanOne", "secondBean"}
public MyBean createBean() {...}

Аннотация @Description не влияет ни не что, является исключительно информативной.

Инъекция через конструктор

Инъекции передаются через конструктор и перечисляются в качестве параметров:

1
2
3
4
5
6
7
8
9
10
11
12
public class Resolver {
private MyBean bean;

public Resolver() {
// do something else
}

@Autowired
public Resolver(MyBean myBean) {
this.bean = myBean;
}
}

Аннотация @Autowired у конструктора является обязательной только в том случае если конструкторов несколько.

Поиск кандидатов на использование в качестве параметра-бина, выполняется последовательно:

  1. Поиск по типу
  2. Поиск по Qualifier
  3. Поиск по имени параметра и имени бина
  4. throw new NoUniqueBeanDefinitionException в случае, если кандидатов несколько
  5. throw new NoSuchBeanDefinitionException в случае, если кандидата найти не удалось

Выбор всегда отдается в пользу более приоритетного кандидата.

Стандартные @Autowired и @Qualifier можно заменить на @Inject и @Named (лучше делать именно так, это позволит избежать рефакторинга аннотаций при смене фреймворка, например, на Quarkus)

Инъекция @Value

Помимо бинов в @Autowired можно вставлять примитивы из параметров приложения.

Ограничения @Autowired

В @Autowired присутствует атрибут required, который можно установить в значение false. В этом случае наличие бина считается не обязательным (не будет выкинуто NoSuchBeanDefinitionException). Но данная опция недопустима для конструктора!

Инъекция через setter

Все инъекции через setter выполняются после выполнения конструктора для создания объекта.
Метод, который следует использовать для инъекции необходимо помечать @Autowired (или @Inject). При этом, можно указать required со значением false (придется следить за возникновением NullPointerException).
Так же как и с конструктором - если инъекция считается обязательной, но кандидат на инъекцию не найден, то будет NoSuchBeanDefinitionException.

Setter vs Constructor

Setter Constructor
Может быть опциональным Может быть final полем класса
Позволяет избежать большого количества аргументов в конструкторе Является обязательным
Может порождать NPE

Инъекции по полям

Аналогичен инъекциям через setter, но имеет ряд минусов: сложно накладывать тестирование; если required=false, а кандидата не будет найдено, то будет NullPointerException(!); ввиду того что приходится использовать Java Reflection, производительность немного снижается (критично при большом количестве Prototype бинов)

Scope

Выделяются:

  • Singleton. Одна сущность на все приложение. Тип по умолчанию. Определение: <ничего>, @Scope("singleton"), @Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
  • Prototype. Каждое новое обращение выполняется к новой сущности. Определение: @Scope("prototype"), @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
  • Request. Каждый новый HTTP запрос выполняется создание новой сущности. Определение: @Scope("request"), @Scope(ConfigurableBeanFactory.SCOPE_REQUEST)
  • Session. Каждое открытие новой HTTP сессии выполняется создание новой сущности. Определение: @Scope("session"), @Scope(ConfigurableBeanFactory.SCOPE_SESSION)
  • Application. Сущность создается одна для всего Global Application Context. Доступно только для web-aware Spring Application Context. Определение: @Scope("application"), @Scope(ConfigurableBeanFactory.SCOPE_APPLICATION)
  • Socket. Сущность создается для каждого нового обращения к socket. Уникально для WebSocket приложения. Определение: @Scope("socket"), @Scope(ConfigurableBeanFactory.SCOPE_SOCKET)
  • Thread. Должно быть подключено пользователем. Определение: @Scope("thread"), @Scope(ConfigurableBeanFactory.SCOPE_THREAD)

Специфический scope

Де-факто просто псевдонимизация стандартных областей (scope). Ниже создание пользовательского scope:

1
2
3
4
5
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public @interface MyCustomScope {
@AliasFor(annotation = Scope.class)
ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
}

Тогда применение пользовательского scope будет таким:

1
2
3
4
5
6
@Configuration
Public MyConfig {
@Bean
@MyCustomScope
public MyCustomBean myCustomBean() { ... }
}

Или

1
2
3
@Component
@MyCustomScope
Public MyCustomBean { ... }

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

  • Инициализация. Первый этап, при котором выполняется чтение описания бин, создание сущности, инъекция зависимых бинов, выделение ресурсов и т.д.
  • Использование. Основной этап - использование в логике приложения. 99% срока жизни бина приходится именно на этот этап
  • Разрушение. Высвобождение ресурсов, “разрыв” зависимостей, сборка мусора GC

При этом жизненый цикл бина с учетом контекста будет следующим:

  1. Создание ApplicationContext
  2. Загрузка/чтение описания бинов
  3. Обработка описания бинов для их последующей инициализации
  4. Инициализация зависимых бинов
  5. Инъекция зависимостей
  6. Икончание инициализации бинов
  7. Использование согласно логики приложения
  8. Остановка ApplicationContext, разрушение бинов

При этом, пп. 2 и 3 включают:

  • Поиск @Configuration
  • Поиск претендентов на регистрацию бинов относительно конфигураций
  • вызов BeanFactory и PostProcessor. Эти бины должны реализовать org.springframework.beans.factory.config.BeanFactoryPostProcessor - они создаются автоматически до создания “обычных” бинов.
    Ниже будут представлены способы перехвата инициализации бинов
  • Выполнение @PostConstruct методов после инъекции зависисмостей. Метод должен быть void и без параметров. Модификаторы доступа не важны (лучше выбрать удобные для тестирования), поскольку используется reflection.
  • Определение отложенной инициализации так же доступно при определении @Bean(initMethod="..."). Данная опция очень полезна, если невозможно изменить код бина

П. 8 допускает расширение стандартного разрушения способами:

  • Путем определения @PreDestroy
  • Путем указания @Bean(destroyMethod = "...")

Инъекция не-бинов

Аннотация @Value умеет преобразовывать строки из параметров приложения в любые примитивы и их обертки.
Так же, по умолчанию, Spring умеет преобразовывать даты форматируя их из dd/MM/yyyy и yyyy/MM/dd

Для конвертации других типов следует использовать конвертер

1.Создать бин конвертера (можно через @Bean)

1
2
3
4
@Component
public class MyConverter implements Converter<String, LocalDate> {
public LocalDate convert(String src) { ... }
}

2.Добавить конвертер в фабрику

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration
public class MyConfig {
@Autowired
MyConverter myConverter;

@Bean
ConversionService conversionService(ConversionServiceFactoryBean factory) {
Return factory.getObject();
}

@Bean
ConversionServiceFactoryBean conversionServiceFactoryBean() {
ConversionServiceFactoryBean factory = new ConversionServiceFactoryBean ();
factory.setConverters(Set.of(myConverter));
return factory;
}
}

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

SpringExpressionLanguage может быть использован для вставки значений через @Value

1
2
3
4
5
6
7
8
9
10
11
@Bean
Properties myCustomProperties() {
Properties props = new Properties();
props.setProperty("p1", "value1");
props.setProperty("property2", "second value");
}

@Bean
MyBean myBean(@Value("#{myCustomProperties.p1}") String p1, @Value("#{myCustomProperties.property2}") String p2) {
...
}

Кастомизаци FactoryBean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MyFactoryBean implements FactoryBean<MyCustomBean> {
@Override
MyCustomBean getObject() throws Exception {
return random.nextBoolean() ? new MyCustomBeanFirstImpl() : new MyCustomBeanSecondImpl();
}

@Override
Class<?> getObjectType() {
Return MyCustomBean.class;
}

@Override
boolean isSingleton() {
return random.nextBoolean();
}
}

@Autowired и дженерики

Инъекции типизированных бинов с помощью дженериков часто очень полезны:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
class JdbcUserRepo extends JdbAbstractRepo<User> implements UserRepo {...}

@Component
class JdbcOrderRepo extends JdbAbstractRepo<Order> implements OrderRepo {...}

@Component
class SomeElement {
@Autowired
JdbcAbstractRepo<User> userRepo;
@Autowired
JdbcAbstractRepo<Order> orderRepo;
}

При этом, если бы оба класса наследовались от одного типа JdbAbstractRepo<MarketEntity>, Spring бы не смог определить какой бин требуется и бросил бы ошибку NoUniqueBeanDefinitionException. Разрешить подобную проблему можно было бы с помощью @Qualifier("beanName").

Spring vs JSR

Спринг аннотации дублируют некоторые аннотации JSR:

Spring JSR Комментарий
@Component @Named Может заменить все кроме @Configuration
@Qualifier @Qualifier
@Autowired @Inject
@Autowired + @Qualifier @Resource(name="beanName")

@Lazy

Инъекция бина производится только во вромя первого к нему обращения. Удобно использовать если объект очень большой. Данную аннотацию следует использовать очень аккуроатно.

1
2
3
4
5
6
7
8
9
10
@Component
@Lazy
public class SomeLazyBean {...}

@Configuration
public class Initialiser {
@Bean
@Lazy
public SomeLazyBean someLazyBean() { ... }
}

При инъекции признак обязательности необходимо снимать:

1
2
3
@Autowired(required=false)
@Lazy
SomeLazyBean lazyBean;

Но лучше использовать обертку в виде Optional в сочетаниии с setter:

1
2
3
4
private Optional<SomeLazyBean> lazyBean = Optional.empty();
@Autowired(required=false)
@Lazy
void setSomeLazyBean(SomeLazyBean lazyBean) { this.lazyBean = Optional.of(lazyBean); }
 Comments
Comment plugin failed to load
Loading comment plugin