itmo_conspects

Лекция 6

Spring Framework (или коротко Spring) — универсальный фреймворк с открытым исходным кодом для Java-платформы.

Spring является собой свободной альтернативной Java EE (или Jakarta EE), предоставляющая функционал для enterprise-разработки. Spring имеет множество расширений (MVC, Data и т.д.) и активной поддерживается сообществом

Spring IoC

Центральной частью 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 понял, какие классы должны стать бинами и участвовать в инверсии управления, их нужно

Рассмотрим способ, включающий в себя 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
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 достает все нужные ему сущности.

Теперь все считанные классы и интерфейсы запаковываются в объекты 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

Spring Beans