Spring Framework (или коротко Spring) — универсальный фреймворк с открытым исходным кодом для Java-платформы.
Spring является собой свободной альтернативной Java EE (или Jakarta EE), предоставляющая функционал для enterprise-разработки. Spring имеет множество расширений (MVC, Data и т.д.) и активной поддерживается сообществом
Центральной частью Spring является контейнер Inversion of Control (инверсия управления). Он нужен для:
По сути, то же самое, что и Dependency Injection в C#
Сами объекты, находящиеся в контейнере (еще называемом контекстом), называются бинами (bean)
Чтобы установить Spring, воспользуемся магическими строчками:
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.30</version>
</dependency>
</dependencies>
dependencies {
implementation('org.springframework:spring-context:5.3.30')
}
Зависимый объект может передаваться зависящему:
Чтобы Spring понял, какие классы должны стать бинами и участвовать в инверсии управления, их нужно
context.xml
Рассмотрим способ, включающий в себя xml-конфиг. Создадим две сущности - UserRepository
и UserService
:
public class UserRepository {
public String getData() {
return "Данные из репозитория";
}
}
public class UserService {
private final UserRepository userRepository;
private String endpoint;
public void setEndpoint(String endpoint) { this.endpoint = endpoint; }
public String getEndpoint() { return this.endpoint; }
// Конструктор для инъекции зависимости
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void processData() {
System.out.println("Обработка данных: " + userRepository.getData());
}
}
Далее заполняем наш context.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
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.xsd">
<!-- Определяем бин для UserRepository -->
<bean id="userRepository" class="UserRepository"/>
<!-- Создаем бин UserService с инъекцией зависимости через конструктор -->
<bean id="userService" class="org.example.models.UserService">
<constructor-arg ref="userRepository"/>
<!-- Можно указать свойство -->
<property name="endpoint" value="google.com"/>
</bean>
</beans>
Теперь в Main.java
достаем контекст из конфига и используем его:
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Main {
public static void main(String[] args) {
// Загрузка контекста Spring из xml-файла
ApplicationContext context = new ClassPathXmlApplicationContext("context.xml");
// Получаем бин UserService из контейнера
UserService userService = context.getBean("userService", UserService.class);
// Используем сервис
userService.processData();
}
}
Здесь мы вручную создали только ClassPathXmlApplicationContext
- все остальные объекты создал Spring
Вместо xml-конфига, можно создать конфиг-класс, в котором вручную прокинуть зависимости:
@Configuration
public class AppConfig {
// Указываем, что это бин
@Bean
public UserRepository userRepository() {
return new UserRepository();
}
@Bean
public UserService userService() {
return new UserService(userRepository());
}
}
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Main {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
UserService userService = context.getBean("userService", UserService.class);
userService.processData();
}
}
Это все надо делать ручками, поэтому перешли к сканированию пакета и аннотациям. Есть две аннотации, которые способствуют этому:
@Component
- так помечаем класс, который будет участвовать во внедрении зависимости@Autowired
- так помечаем метод (в том числе конструктор), которому будут передаваться зависимости из контейнера (также возможно приватное поле, которому будет передано зависимость)В нашем примере это:
@Component
public class UserRepository {
public String getData() {
return "Данные из репозитория";
}
}
@Component
public class UserService {
private final UserRepository userRepository;
// Указываем, куда надо засунуть зависимость
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void processData() {
System.out.println("Обработка данных: " + userRepository.getData());
}
}
Далее аннотированные классы можно показать Spring либо с указанием пакета, в котором они находятся:
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Main {
public static void main(String[] args) {
// Загрузка контекста Spring из сканирования пакета
ApplicationContext context = new AnnotationConfigApplicationContext("org.example.models");
UserService userService = context.getBean("userService", UserService.class);
userService.processData();
}
}
либо через отдельный класс конфига (так называемого JavaConfig), в котором указать пакет:
@Configuration
@ComponentScan("org.example.models") // Указываем пакет для сканирования
public class AppConfig {
}
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Main {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
UserService userService = context.getBean("userService", UserService.class);
userService.processData();
}
}
Вместо @Component
можно использовать @Service
, @Repository
, @Controller
, чтобы повысить читаемость
Если класс имеет несколько конструкторов, то можно добавить аннотацию @Primary
для указания главного конструктора, которому будут передаваться зависимости
Чтобы задать скоуп (жизненный цикл) компонента, можно использовать аннотации:
@Scope("prototype")
@Scope("singleton")
// или
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
// ну и там еще есть request, session, application, websocket
или в xml-конфиге:
<bean id="userService" class="org.example.models.UserService" scope="singleton"/>
Как это работает?
Сначала Spring достает все нужные ему сущности.
XmlBeanDefinitionReader
.AnnotationBeanDefinitionReader
ищет все @Configuration
, в которых могут быть дополнительные конфиги. Далее ClassPathBeanDefinitionScanner
сканирует пакет на наличие @Component
-классовТеперь все считанные классы и интерфейсы запаковываются в объекты BeanDefinition
, которые описывают будущие бины
По умолчанию, все BeanDefinition
остаются не изменными, однако если в бинах случайно затесалась реализация BeanFactoryPostProcessor
, то он используется для изменения описания бинов до их непосредственного создания. Пример такого BeanFactoryPostProcessor
:
public class CustomBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
// Модифицируем существующий бин
BeanDefinition dbConfigDef = beanFactory.getBeanDefinition("dbConfig");
dbConfigDef.getPropertyValues().add("url", "jdbc:postgresql://default-host:5432/db");
// Создаем новый бин
GenericBeanDefinition newBeanDef = new GenericBeanDefinition();
newBeanDef.setBeanClassName("java.lang.String");
newBeanDef.getConstructorArgumentValues().addGenericArgumentValue("ЛОЛ!");
((DefaultListableBeanFactory)beanFactory).registerBeanDefinition("myBean", newBeanDef);
}
}
Все эти описания бинов хранятся в мапе. После этого они создаются при помощи BeanFactory
Если объект создается суперсложно, то его создание можно делегировать объекту класса, реализующего FactoryBean
, например:
import org.springframework.beans.factory.FactoryBean;
// Создаем строки
public class StringFactoryBean implements FactoryBean<String> {
private String prefix;
private int counter = 0;
public void setPrefix(String prefix) {
this.prefix = prefix;
}
@Override
public String getObject() {
return prefix + "-" + (counter++);
}
@Override
public Class<?> getObjectType() {
return String.class;
}
@Override
public boolean isSingleton() {
return false;
}
}
Теперь можно получить объекты или фабрику:
String str = context.getBean("customStringFactory", String.class);
StringFactoryBean factory = context.getBean("&customStringFactory", StringFactoryBean.class);
ПОСЛЕ ЭТОГО, в ход вступают реализации BeanPostProcessor
, которые могут дополнительно произвордить действия над созданными бинами перед и/или после инициализации (например, положить в прокси). Под инициализацией понимаются методы бинов, аннотированные @PostConstruct
или указанные в xml как init-method
: <bean id="userService" class="com.example.models.UserService" init-method="init"/>
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;
@Component
public class CustomBeanPostProcessor implements BeanPostProcessor {
// Вызывается ПЕРЕД инициализацией бина
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
System.out.println("Преинициализация: " + beanName);
return bean;
}
// Вызывается ПОСЛЕ инициализации бина
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
System.out.println("Постинициализация: " + beanName);
// Добавляем прокси для важного сервиса
if (bean instanceof ImportantService) {
return makeProxy(bean);
}
return bean;
}
private Object makeProxy(Object bean) {
System.out.println("Создаю прокси...");
return bean;
}
}
Теперь готовые бины кладутся в контекст
Когда контекст закрывается, у всех бинов вызывается метод, помеченный @PreDestroy
или destroy-method="..."
в xml