Spring Boot - darmoise/wiki GitHub Wiki

IoC Container

Центральной частью Spring является подход Inversion of Control, который позволяет конфигурировать и управлять объектами Java с помощью рефлексии. Вместо ручного внедрения зависимостей, фреймворк забирает ответственность за это посредством контейнера. Контейнер отвечает за управление жизненным циклом объекта: создание объектов, вызов методов инициализации и конфигурирование объектов путём связывания их между собой.

Объекты, создаваемые контейнером, также называются управляемыми объектами (beans).

Плюсы

  1. отделение выполнения задачи от ее реализации;
  2. легкое переключение между различными реализациями;
  3. большая модульность программы;
  4. более легкое тестирование программы путем изоляции компонента или проверки его зависимостей и обеспечения взаимодействия компонентов через контракты.

Подробнее можно прочитать тут.

Жизненный цикл context

  1. Парсирование кофигурации (XML, JavaConfig и тд). После парсирования конфигурации создается BeanDefenition - мета информация, описывающая будущий бин;
  2. Настройка уже созданных BeanDefinition - на этом этапе мы можем повлиять на то, какими будут наши бины еще до их создания;
  3. Создание кастомных FactoryBean - можно самостоятельно создать фабрику, которая будет создавать бины определенного типа;
  4. Создание экземпляров бинов - тут происходит создание классов или проксей. На этом этапе можно организовать инжект поля через конструктор;
  5. Настройка созданных бинов - BeanPostProcessor - именно то, что нам нужно. На этом этапе конструктор бина уже выполнился и бин создан, но бин еще не попал в контекст.

Основные этапы поднятия ApplicationContext

1. Парсирование конфигурации и создание BeanDefinition

Цель первого этапа создание всех BeanDefinition — специального интерфейса, через который можно получить доступ к метаданным будущего бина.

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

XML-конфигурации

Для Xml конфигурации используется класс — XmlBeanDefinitionReader, который реализует интерфейс BeanDefinitionReader. Тут все достаточно прозрачно. XmlBeanDefinitionReader получает InputStream и загружает Document через DefaultDocumentLoader. Далее обрабатывается каждый элемент документа и если он является бином, то создается BeanDefinition на основе заполненных данных (id, name, class, alias, init-method, destroy-method и др.).

Контекст можно получить следующим образом:

ClassPathXmlApplicationContext(“context.xml”);

Каждый BeanDefinition помещается в Map. Map хранится в классе DefaultListableBeanFactory. В коде Map выглядит вот так.

/** Map of bean definition objects, keyed by bean name */
private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<String, BeanDefinition>(64);

Пример XML:

<beans....>
 <bean class="com.inwhite.spring.compare.CoolDaoImpl" id="coolDao"/>

    <bean id ="coolService"  class="com.inwhite.spring.compare.CoolServiceImpl"
                             init-method="init" 
                             destroy-method="closeResources"
                             scope="prototype">
           <property name="dao" ref="coolDao"/>
    </bean>

</beans>

Конфигурация через аннотации с указанием пакета для сканирования

Контекст можно получить следующим образом:

new AnnotationConfigApplicationContext(“package.name”);

Конфигурация через аннотации с указанием класса с @Configuration или с указанием пакета сканирования

Используется класс AnnotationConfigApplicationContext:

new AnnotationConfigApplicationContext(JavaConfig.class); // JavaConfig
new AnnotationConfigApplicationContext(“package.name”); // указание пакета для сканирования

Внутри AnnotationConfigApplicationContext есть следующие поля:

/*
 * Работает в два этапа
 * 1. Регистрацияя всех @Configuration для парсирования. 
 * Если в конфигурации используется @Conditional, 
 * то будут задействованы только те конфигурации, 
 * для которых проверка вернет true;
 * 2. Регистрация специального BeanFactoryPostProcessor, 
 * а именно BeanDefinitionRegistryPostProcessor, который (при помощи класса ConfigurationClassParser) 
 * парсирует JavaConfig и создает BeanDefinition.
 */
private final AnnotatedBeanDefinitionReader reader; 
/* 
 * сканирует указанный пакет на наличие классов, 
 * помеченных аннотацией @Component, 
 * помеченные классы парсируются и для них создается BeanDefinition.
 */
private final ClassPathBeanDefinitionScanner scanner; 

Groovy конфигурация

По стилю похожа на XML, за исключением, что используется Groovy. Контекст получается следующим образом:

new GenericGroovyApplicationContext(“context.groovy”).

Внутри GenericGroovyApplicationContext находится поле:

private final GroovyBeanDefinitionReader reader; // занимается чтением конфигуарции

2. Настройка созданных BeanDefinition

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

public interface BeanFactoryPostProcessor {
        /*
         * ConfigurableListableBeanFactory - фабрика, которая содержит много полезных методов, например,
         * getBeanDefinitionNames, который позволяет получить все имена BeanDefinition, а уже по имени получить
         * BeanDefinition для обработки метаданных.
         */
	void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException;
}

Одна из родных реализаций интерфейса BeanFactoryPostProcessor - PropertySourcesPlaceholderConfigurer. Например, из application.property считывается данные для подключения к БД этого конфигуратора и inject'ятся в нужное поле. Так как inject делается по ключу, то до создания экземпляра бина нужно заменить ключ на заначение из файла.

Пример класса для inject'а:

/* 
 * Если PropertySourcesPlaceholderConfigurer не обработает этот BeanDefinition, 
 * то после создания экземпляра ClassName, 
 * в поле host проинжектится значение — "${host}" 
 * (в остальные поля проинжектятся соответствующие значения).
 */
@Component
public class ClassName {

    @Value("${host}")
    private String host;

    @Value("${user}")
    private String user;

    @Value("${password}")
    private String password;

    @Value("${port}")
    private Integer port;
} 

Настройка созданных BeanDefinition

Чтобы конфигуратор был добавлен в цикл настройки созданных BeanDefinition, создать для него бин:

@Configuration
@PropertySource("classpath:property.properties")
public class DevConfig {
	@Bean
	public static PropertySourcesPlaceholderConfigurer configurer() {
	    return new PropertySourcesPlaceholderConfigurer();
	}
}

3. Создание кастомных FactoryBean

FactoryBean — это generic интерфейс, которому можно делегировать процесс создания бинов типа . В те времена, когда конфигурация была исключительно в xml, разработчикам был необходим механизм с помощью которого они бы могли управлять процессом создания бинов. Именно для этого и был сделан этот интерфейс. Для тех кто пользуется JavaConfig, этот интерфейс будет абсолютно бесполезен.

<!-- На первый взгляд, тут все нормально и нет никаких проблем -->
<!-- А что если нужен еще один цвет, или вообще случайный, вот тут на помощь приходит FactoryBean -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <bean id="redColor" scope="prototype" class="java.awt.Color">
        <constructor-arg name="r" value="255" />
        <constructor-arg name="g" value="0" />
        <constructor-arg name="b" value="0" />
    </bean>
    
</beans>

Создадим фабрику для создания бинов типа Color:

package com.malahov.factorybean;

import org.springframework.beans.factory.FactoryBean;
import org.springframework.stereotype.Component;

import java.awt.*;
import java.util.Random;

public class ColorFactory implements FactoryBean<Color> {
    @Override
    public Color getObject() throws Exception {
        Random random = new Random();
        Color color = new Color(random.nextInt(255), random.nextInt(255), random.nextInt(255));
        return color;
    }

    @Override 
    public Class<?> getObjectType() { return Color.class; }

    @Override
    public boolean isSingleton() { return false; }
}

Теперь добавим эту фабрику в 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"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
	<bean id="colorFactory" class="com.malahov.temp.ColorFactory"/>
</beans>

Теперь создание бина типа Color.class будет делегироваться ColorFactory, у которого при каждом создании нового бина будет вызываться метод getObject.

4. Создание экземпляров бинов

Созданием экземпляров бинов занимается BeanFactory при этом, если нужно, делегирует это кастомным FactoryBean. Экземпляры бинов создаются на основе ранее созданных BeanDefinition.

Создание экземпляров бинов

5. Настройка созданных бинов

Интерфейс BeanPostProcessor позволяет вклиниться в процесс настройки ваших бинов до того, как они попадут в контейнер. Интерфейс несет в себе несколько методов.

public interface BeanPostProcessor {
	Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException;
	Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException;
}

Оба метода вызываются для каждого бина. На данном этапе экземпляр бина уже создан и идет его донастройка, соответственно:

  1. Оба метода в итоге должны вернуть бин. Если в методе вы вернете null, то при получении этого бина из контекста вы получите null, а поскольку через бинпостпроцессор проходят все бины, после поднятия контекста, при запросе любого бина вы будете получать фиг, в смысле null.
  2. Если вы хотите сделать прокси над вашим объектом, то имейте ввиду, что это принято делать после вызова init метода, иначе говоря это нужно делать в методе postProcessAfterInitialization.

Процесс донастройки бина

Полезной может быть эта статья.

JDK dynamic proxy и BeanPostProcessor

В некоторых задачах можно создать прокси-класс для логирования:

  1. Создадим аннотацию @Logging, которая будет вешаться над классом, методы которого нужно логировать Пример:
@Retention(RetentionPolicy.RUNTIME)
public @interface Logging {

}

Создаем кастомный BeanPostProcessor:

@Slf4j
@Component
public class LoggingBeanPostProcessor implements BeanPostProcessor {
    private final Map<String, Class<?>> map = new HashMap<>(); // Мапа, в которую сохраняются классы, помеченные аннотацией Logging

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        Class<?> cls = bean.getClass();
        if (cls.isAnnotationPresent(Logging.class)) {
            map.put(beanName, cls);
        }
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        val cls = map.get(beanName);

        if (cls != null) {
            return Proxy.newProxyInstance(
                bean.getClass().getClassLoader(),
                bean.getClass().getInterfaces(),
                (proxy, method, args) -> {
                    val methodName = method.getName();
                    val argsAsString = Arrays.stream(args)
                        .map(Object::toString)
                        .collect(Collectors.joining(", "));

                    log.info("Start " + methodName + "(" + argsAsString + ")");
                    val retVal = method.invoke(bean, args);
                    log.info("Finish " + methodName);

                    return retVal;
                }
            );
        }

        return bean;
    }
}

Bean

Это самый обычный объект, разница в том, что бинами принято называть те объекты, которые управляются Spring и живут внутри DI-контейне ра.

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

Свойства

  1. name - уникальный идентификатор бина;
  2. initialization method - метод инициализации бина;
  3. destroy method - метод уничтожения бина, который будет использоваться при уничтожении контейнера, содержащего бин;
  4. autowireCandidate - является ли этот компонент кандидатом на автоматическое подключение к какому-либо другому компоненту.

Scope

  1. singleton - один единственный bean для каждого контейнера Spring IoC (используется по умолчанию).
  2. prototype - создает новый экземляр бина на каждое обращение к объекту (т.е. будет иметь неограниченное количество экземпляров bean).
  3. request - создается один экземпляр бина на каждый HTTP-запрос.
  4. session - создается один экземпляр бина на каждую HTTP-сессию.
  5. web socket - создается определенный экземпляр для определенного сокета.
  6. application - создается один экземпляр бина для жизненного цикла бина. Похоже на singleton, но когда бины ограничены областью приложения.

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

  1. Загрузка описаний бинов, создание графа зависимостей(между бинами);
  2. Создание и запуск BeanFactoryPostProcessors;
  3. Создание бинов;
  4. Spring внедряет значения и зависимости в свойства бина;
  5. Если бин реализует метод setBeanName() из интерфейса NameBeanAware, то ID бина передается в метод
  6. Если бин реализует BeanFactoryAware, то Spring устанавливает ссылку на bean factory через setBeanFactory() из этого интерфейса;
  7. Если бин реализует интерфейс ApplicationContextAware, то Spring устанавливает ссылку на ApplicationContext через setApplicationContext();
  8. BeanPostProcessor это специальный интерфейс, и Spring позволяет бинам имплементировать этот интерфейс. Реализуя метод postProcessBeforeInitialization(), можно изменить экземпляр бина перед его(бина) инициализацией(установка свойств и т.п.);
  9. Если определены методы обратного вызова, то Spring вызывает их. Например, это метод, аннотированный @PostConstruct или метод initMethod из аннотации @Bean;
  10. Теперь бин готов к использованию. Его можно получить с помощью метода ApplicationContext#getBean();
  11. После того как контекст будет закрыт(метод close() из ApplicationContext), бин уничтожается;
  12. Если в бине есть метод, аннотированный @PreDestroy, то перед уничтожением вызовется этот метод; Если бин имплементирует DisposibleBean, то Spring вызовет метод destroy(), чтобы очистить ресурсы или убить процессы в приложении. Если в аннотации @Bean определен метод destroyMethod, то вызовется и он.

Статическое внедрение зависимости

Если в классе будет статический метод, то при инициализации впервую очередь создастся статический метод (из-за особенностей статических полей), а потом уже Bean, который "навешивается" на статический метод. При этом Spring не позволяет внедрять бины напрямую в статические поля, нужно создать нестатический сеттер-метод

MVC (Model-View-Controller)

  • Model - блок объединяет данные приложения (на практике POJO классы);
  • View - блок отвечает за возвращения клиенту наполнения в виде текста или изображений.;
  • Controller - включен между Model и View. Управляет процессом преобразования входящих запросов в адекватные ответы. Действует как ворота, направляющие всю поступающую информацию. Переключает поток информации из модели в представление и обратно.

Некоторые аннотации

ConditionalOn* (ConditionalOnProperty, ConditionalOnMissingBean и т.д.)

Создает условие бин, если выполняется условие.

@Controller

Фронт-контролер в проекте Spring MVC.

@Service

Указывает, что класс осуществляет функции сервиса. Можно указать название сервиса в параметрах.

@Scheduled

Используется, чтобы отметить, что метод будет выполняться по расписанию. Принимает в себя один атрибут из списка: cron, fixedDelay (задача будет выполнена в первый раз после значения initialDelay, и она будет продолжать выполняться в соответствии с fixedDelay), fixedRate (следующая задача не будет вызвана до тех пор, пока не будет выполнена предыдущая).

@RequestMapping

Используется для связывания с URL.

@RequestBody

Позволяет отправлять Object в запросе.

@RequestParam

Позволяет отправлять query-параметр в запросе.

@PathVariable

Позволяет отправлять path variable в запросе.

@Qualifier

@Qualifier - используется совместно с @Autowired для уточнения данных связывания, когда возможны коллизии (например одинаковых имен\типов).

@Scope

Указывает scope для бина.

@Lazy

Указывает на то, что бин должен быть инициализирован лениво. Для того, чтобы бин создавался в момент обращения к нему, инжектить надо через setter'ы:

@Configuration
public class ChildConfig {
    @Bean
    @Lazy
    public Child1 child1() {
        return new Child1();
    }
}

public class Child1 {
    private String value = "2";

    @PostConstruct
    void onInit() {
        System.out.println("child1");
    }

    public void setValue(String value) {
        this.value = value;
    }
}

@RestController
@RequestMapping(value = "/api/v1/")
public class Controller {
    private final Child1 child1;

    public Controller(@Lazy final Child1 child1) {
        this.child1 = child1;
    }
}
// or
@RestController
@RequestMapping(value = "/api/v1/")
public class Controller {
    private Child1 child1;

    @Autowired
    public void setChild1(@Lazy Child1 child1) {
        this.child1 = child1;
    }

}

Аннотацию @Lazy надо повесить над бином и над сеттером/конструктором, в который зависимость внедряется.

Циклические зависимости

Если вы используете преимущественно внедрение через конструктор, можно создать неразрешимый сценарий циклической зависимости. Например: Класс A требует экземпляр класса B через внедрение на основе конструктора, а класс B требует экземпляр класса A через внедрение на основе конструктора. Если сконфигурировать бины классов A и B на внедрение друг в друга, IoC-контейнер Spring обнаружит эту циклическую ссылку во время выполнения и сгенерирует исключение BeanCurrentlyInCreationException.

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

@Service
public class AService {
    private final BService service;

    @Autowired
    public AService(@Lazy BService service) {
        this.service = service;
    }

    public String get() {
        return String.valueOf(service.hashCode());
    }
}

@Service
public class BService {
    private final AService service;

    @Autowired
    public BService(@Lazy AService service) {
        this.service = service;
    }

    public String get() {
        return String.valueOf(service.hashCode());
    }
}

@Component vs @Service vs @Repository vs @Controller

Они все служат для обозначения класса как бин:

  1. @Component - кандидата для создания bean;
  2. @Service - класс содержит бизнес-логику и вызывает методы на уровне хранилища. Ничем не отличается от классов с @Component;
  3. @Repository - указывает, что класс выполняет роль хранилища (объект доступа к DAO). При этом отлавливает определенные исключения персистентности и пробрасывает их как одно непроверенное исключение Spring Framework. Для этого Spring оборачивает эти классы в прокси, и в контекст должен быть добавлен класс PersistenceExceptionTranslationPostProcessor;
  4. @Controller - указывает, что класс выполняет роль контроллера MVC. Диспетчер сервлетов просматривает такие классы для поиска @RequestMapping; 4.1. @RestController Автоматически добавляются аннотации @Controller, а так же @ResponseBody (позволяет отправлять Object в ответе) применяется ко всем методам;

Как работать с Redis в Spring

  1. @RedisHash - аналог @Entity для Redis. Указываю hash и время жизни;
  2. @Id - отмечает ключ;
  3. @Indexed - отмечает индекс (по нему можно искать).

Какие есть репозитории

CrudRepository

Представляет функции CRUD (Create, Read, Update, Delete).

Вопросы

  1. Можно ли сменить логику существующих методов в репозитории? Например есть findAll() и нужно, не переименовывая метод, сделать, например, чтобы данные сортировались. Да, можно, просто переопределяем метод, @Override не добавляем, добавляем @Query.

Методы

  1. save(S entity);
  2. Iterable saveAll(Iterable entities);
  3. findById(ID id);
  4. existsById(ID id);
  5. findAll();
  6. findAllById(Iterable ids);
  7. count();
  8. deleteById(ID id);
  9. delete(T entity);
  10. deleteAllById(Iterable<? extends ID> ids);
  11. void deleteAll(Iterable<? extends T> entities);
  12. void deleteAll().

PagingAndSortingRepository

Этот интерфейс предоставляет метод findAll(Pageable pageable), который позволяет разбивать на страницы (Pageable). Pageable - page size, current page number, sorting (ASC, DESC).

Методы

  1. findAll(Sort sort);
  2. findAll(Pageable pageable);

JpaRepository

Методы

  1. findAll();
  2. findAll(Example example, Sort sort);
  3. saveAll(Iterable entities);
  4. flush() – сбросить все ожидающие задачи в БД;
  5. saveAndFlush(S entity) – сохранить объект и немедленно flush изменения, используется, когда нашей бизнес-логике необходимо прочитать сохраненные изменения на более позднем этапе той же транзакции до commit;
  6. deleteAllInBatch(Iterable entities) – удалить итерируемый объект, можем передать несколько объектов, чтобы удалить их в batch режиме.

Работа с Entity

@Entity

Говорит Hibernate (ORM), что класс является сущностью.

@Table

Указывает схему и таблицу в БД. Если имя класса и имя таблицы совпадают, то аннотацию можно не указывать.

@Column(name, unique, nullable, length)

Указывает колонку таблицы.

@Id

Указывает на primary key.

@GeneratedValue

Указывает на то, что Hibernate должен сгенерировать ID объекта, в соответствии с одной из стратегий: AUTO, IDENTITY, SEQUENCE, TABLE. Эти четыре типа генерации приведут к созданию одинаковых значений, но с использованием разных механизмов базы данных.

AUTO

Будет применен генератор на основе типа ключа (UUID, Integer и т.д.).

IDENTITY

Идентификатор будет автоматически увеличиваться, но будут отключены batch-обновления.

SEQUENCE

Использует последовательности (если БД поддерживает, если не поддерживает, то переключается на table-генерацию).

@Entity
public class User {
    @Id
    @GeneratedValue(generator = "sequence-generator")
    @GenericGenerator(
      name = "sequence-generator",
      strategy = "org.hibernate.id.enhanced.SequenceStyleGenerator",
      parameters = {
        @Parameter(name = "sequence_name", value = "user_sequence"),
        @Parameter(name = "initial_value", value = "4"),
        @Parameter(name = "increment_size", value = "1")
        }
    )
    private long userId;
    
    // ...
}

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

TABLE

Этот генератор использует сегменты генерации значений идентификаторов базовой таблицы БД.

Аннотация @GenericGenerator

При помощи этой аннотация можно указать кастомный генератор id.

Связь на уровне таблиц

Некоторые аргументы аннотаций:

  1. cascade - операции могут быть каскадными, по умолчанию отключено;
  2. fetch type - тип выборки данных, может быть ленивым (LAZY) и не терпеливым (eager).

@OneToOne

Одна запись в таблице A ссылается на одну запись в таблице B.

@OneToMany

Одна запись в таблице A ссылается на много записей в таблице B.

@ManyToOne

Много записей в таблице B, ссылаются на одну запись в таблице A.

@ManyToMany

Много записей в таблице A (сотрудники) ссылается на много записей в таблице B (задачи).

@JoinColumn

Аннотация @JoinColumn определяет столбец, который объединит два объекта. Он определяет столбец foreign key сущности и связанное с ним поле primary key. Эта аннотация позволяет нам создавать связи между сущностями, быстро сохранять данные и получать к ним доступ. Обычно он используется с аннотациями @ManyToOne или @OneToOne для определения ассоциации.

Проблема n+1

Проблема N + 1 возникает, когда фреймворк доступа к данным выполняет N дополнительных SQL-запросов для получения тех же данных, которые можно получить при выполнении одного SQL-запроса.

Есть две сущности: Сущности

Мы получем проблему N + 1, когда вместо одного запроса выполняем 5:

SELECT
    pc.id AS id,
    pc.review AS review,
    pc.post_id AS postId
FROM post_comment pc
 
SELECT p.title FROM post p WHERE p.id = 1
SELECT p.title FROM post p WHERE p.id = 2
SELECT p.title FROM post p WHERE p.id = 3
SELECT p.title FROM post p WHERE p.id = 4

SQL

Для решения проблемы можно поступить следующим образом (извлечь все данные одним запросом):

SELECT
    pc.id AS id,
    pc.review AS review,
    p.title AS postTitle
    FROM post_comment pc
    JOIN post p ON pc.post_id = p.id

JPA и Hibernate

Использование FetchType.EAGER для JPA - плохая идея, тем более он подвержен проблеме N + 1. Поэтому стоит использовать FetchType.LAZY. Либо сделать запрос JOIN FETCH через EntityManager:

List<PostComment> comments = entityManager.createQuery("""
    select pc
    from PostComment pc
    join fetch pc.post p
    """, PostComment.class)
.getResultList();
 
for(PostComment comment : comments) {
    LOGGER.info(
        "The Post '{}' got this review '{}'",
        comment.getPost().getTitle(),
        comment.getReview()
    );
}

Кэширование в Spring

@Cacheable

  1. value/cacheNames - имя (имена) кэшей, в которых хранятся результаты вызова методов;
  2. key - выражение в SpEL (Spring Expression Language) для динамического вычисления ключа;
  3. conditional - выражение в SpEL для условия кэширования.

@CacheEvict

Проблема кэша в том, что он не может быть бесконечным. Аннотация позволяет удалить одно или несколько (все) значений. allEntries = true говорит о том, что нужно полностью очистить кэш.

@CachePut

Позволяет обновлять содержание кэша во время выполнения метода. Отличие этой аннотации от @Cachable в том, что @CachePut сначала запустит метод, а потом поместит его результаты в кэш.

Spring Cloud

Spring Cloud Context

Клиент для загрузки properties файлов с Config Server и загрузки в DI-контейнер бинов, специфичных для Spring Cloud компонентов. Поддерживает

Spring Cloud Security

Расширение Spring Security, реализует:

  • Единый вход OAuth2, с ретрансляцией токенов (проброс токенов извне, через Gateway).
  • Защиту ресурсов токенами OAuth2

Пример: Создаем конфигурацию для web security:

@Configuration
public class WebSecurityConfiguration {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(AbstractHttpConfigurer::disable)
            .authorizeHttpRequests(ahr -> {
                ahr.requestMatchers(
                        // путь, доступ к которому возможен только после авторизации
                        "/api/v1/private/**" 
                    ).fullyAuthenticated()
                    .anyRequest().permitAll(); // любой запрос, разрешить всем
            })
            .httpBasic(Customizer.withDefaults()) // basic auth используем
            .build();
    }

    // использщуется для хранения пароля в хэшированном виде
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(8);
    }
}

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

@Service
@RequiredArgsConstructor
public class UserService implements UserDetailsService {
    private final PasswordEncoder passwordEncoder;
    private final UserHelperService userHelperService;
    private final UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        /* 
         * Получаем данные пользователя из таблицы
         * Модель реализует интерфейс UserDetails
         */
        return userMapper.toModel(
            userHelperService.findByUsername(username)
        );
    }
    
    // ...
    // ТУТ ДРУГОЙ КОД
    // ...

    // создаем пользователя, пароль хэшируем
    private UserEntity createUserEntity(final RegUser model) {
        val account = new Account();
        val encodedPassword = passwordEncoder.encode(model.password());
        val spk = Base58.encode(account.getSecretKey());

        return userMapper.toEntity(model)
            .setPassword(encodedPassword)
            .setAddress(account.getPublicKey().toBase58())
            .setPk(spk);
    }
}

Создаем authorization provider для basic auth:

@Slf4j
@Component
@RequiredArgsConstructor
public class UserAuthenticationProvider implements AuthenticationProvider {
    private final PasswordEncoder passwordEncoder;
    private final UserService userService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        val username = authentication.getName(); // получаем имя пользователя из запроса
        val rawPassword = authentication.getCredentials().toString(); // получаем пароль
        
        // получаем данные для пользователя из таблицы
        UserDetails userDetails = userService.loadUserByUsername(username); 
        val encodedPassword = userDetails.getPassword();

        // проверяем хэши паролей
        if (passwordEncoder.matches(rawPassword, encodedPassword)) {
            return new UsernamePasswordAuthenticationToken(
                username,
                encodedPassword,
                userDetails.getAuthorities()
            );
        }
        else {
            // выбрасываем исключение, если данные для авторизации некорректные
            throw new BadCredentialsException(ErrorCode.CREDENTIALS.getMessage());
        }
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}

Spring Cloud OpenFeign

Декларативный REST клиент.

Spring Validation

Используется для валидации полей в запросе (пример, аннотации @NotNull, @Size, @NotEmpty, @Positive). Можно создать кастомную аннотацию, для примера создадим аннотацию для проверки password и confirmPassword на эквивалетность.

Создадим саму аннотацию:

@Documented
 // указываем класс, который используется для валидации
@Constraint(validatedBy = PasswordsEqualsValidator.class)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
@Retention(RUNTIME)
public @interface PasswordsEquals {
    String password(); // название поля пароля с паролем
    String confirmPassword(); // название поля с подтверждением пароля

    String message() default "Invalid password";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    @Target({ ElementType.TYPE })
    @Retention(RetentionPolicy.RUNTIME)
    @interface List {
        PasswordsEquals[] value();
    }
}

Создадим сам валидатор (должен реализовывать ConstraintValidator):

public class PasswordsEqualsValidator implements ConstraintValidator<PasswordsEquals, Object> {
    private String password;
    private String confirmPassword;

    @Override
    public void initialize(PasswordsEquals constraintAnnotation) { // принимает аннотацию на вход
        this.password = constraintAnnotation.password();
        this.confirmPassword = constraintAnnotation.confirmPassword();
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) { // 
        String passwordValue = (String) new BeanWrapperImpl(value).getPropertyValue(password);
        String confirmPasswordValue = (String) new BeanWrapperImpl(value).getPropertyValue(confirmPassword);

        return Objects.equals(passwordValue, confirmPasswordValue);
    }
}

Создадим объект, в котором будем проверять поля:

@PasswordsEquals(
    password = "password",
    confirmPassword = "confirmPassword"
)
public record RegUserDto(
    @NotBlank(message = "Invalid username")
    String username,
    @NotBlank(message = "Password is empty")
    String password,
    @NotBlank(message = "Confirm password is empty")
    String confirmPassword
) {
}

AOP

Основная идея в выделении так называемой сковозной функциональности.

  1. Join point - точка наблюдения, присеодинения к коду, где планируется внедрнение функциональности.
  2. Pointcut - это срез, запрос точек присоединения
  3. Advice - набор инструментов, выполняемых на точках среза (Pointcut). Инструкции можно выполнять по событиям разных типов:
  • Before — перед вызовом метода
  • After — после вызова метода
  • After returning — после возврата значения из функции
  • After throwing — в случае exception
  • After finally — в случае выполнения блока finally
  • Around — можно сделать пред., пост., обработку перед вызовом метода, а также вообще обойти вызов метода.

На один Pointcut можно повесить несколько Advice разного типа. 4. Aspect - модуль, в котором собраны описания Pointcut и Advice.

Пример:

ДОБАВИТЬ

Транзакции в Spring

Transactional

Ищет есть ли бин TransactionManager. Если он есть Spring его автоматически подключает. Если его не удалось найти, тогда мы должны указать его явно (value, transactionManager).

У аннотации также есть следующие аргументы:

  1. propagation - тип propagation
  • REQUIRED/DEFAULT - если запущена транзакция — выполнять внутри нее, иначе создает новую транзакцию. Если ошибка в запросе, то в базу ничего на запишется.;
  • SUPPORTS - методу не важно, будет транзакция или нет, он в любом случае выполнится, но если будет транзакция, то он выполнится внутри нее.;
  • MANDATORY - использует существующую транзакцию. Если ее нет — бросает exception.;
  • REQUIRES_NEW - создает в любом случае новую транзакцию. Если запущена существующая транзакция — она останавливается на время выполнения метода, новый метод выполняется в новой транзакции, и дальше выполняется внешняя транзакция, если она есть;
  • NOT_SUPPORTED - означает не выполнять в текущей транзакции. Если транзакция запущена — она останавливается на время выполнения метода. Метод выполняется вне транзакции. Когда метод выполнился — транзакция запускается.;
  • NEVER - означает, что данный метод не должен выполняться в транзакции. Если транзакция запущена — бросает exception.;
  • NESTED - вложенная транзакция (подтранзакция). Подтвержается вместе с внешней транзакцией.Если нет существующей транзакции — работает как REQUIRED..
  1. timeout - таймаут для транзакции в секундах;
  2. isolation - уровень изоляции (READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE);
  3. rollbackFor - указываем роллбэк для определенного exception;
  4. noRollbackFor — Указывает, что откат не должен происходить, если целевой метод вызывает исключение, которое вы укажете.

TransactionManager

Он создает EntityManager, если он необходим, и осуществляет старт новой транзакции. В зависимости от того, выполняется ли хоть одна транзакция в текущий момент или нет и параметра “propagation” у метода, аннотированного @Transactional, создается новая транзакция.

Алгоритм создания новой транзакции:

  1. создается новый EntityManager;
  2. EntityManager привязывается к “текущему потоку Thread”;
  3. берется соединение из пула соединений БД;
  4. это соединение привязывается к “текущему потоку Thread” при помощи ThreadLocal (Класс ThreadLocal предоставляет локальные переменные потока. Каждый поток имеет свою собственную инициализированную копию переменной).

EntityManager

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

EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("jpa-example");
EntityManager entityManager = entityManagerFactory.createEntityManager();
try {
    entityManager.getTransaction().begin();
    entityManager.persist(firstEntity);
    entityManager.persist(secondEntity);
    entityManager.getTransaction().commit();
} catch (Exception e) {
    entityManager.getTransaction().rollback();
}

open-in-view

Если true (по умолчанию), то сессия Hibernate держится открытой все время обработки HTTP-запроса, включая этап создания представления (View) - JSON-ресурса и HTML-странице. Это делает возможным ленивую загрузку данных в слое представления после коммита транзакции в слое бизнес-логики. Например, мы запрашиваем из БД сущность Article. Статья должна быть отображена вместе с комментариями. OSIV (Open session in View) позволяет при рендеринге HTML просто вызвать метод сущности getComments(), и комментарии будут загружены отдельным запросом. При отключенном режиме OSIV мы получим LazyInitializationException, так как сессия уже закрыта, и сущность Article больше не управляется Hibernate.

Плюсы:

  1. Обратная совместимость;
  2. Если отключить OSIV, новичкам может быть непонятно, почему не работает такая интуитивно ожидаемая вещь, как получение коллекции связанных элементов при обращении к методу сущности. Вместо этого пользователь получит LazyInitializationException, это замедлит его путь к работающему приложению;
  3. OSIV позволяет увеличить простоту кода, удобство и скорость разработки.

Минусы:

  1. Запросы к БД без транзакции работают в режиме авто-коммита, сильно ее нагружая;
  2. Долгие соединения с БД опять же увеличивают нагрузку на нее и уменьшают пропускную способность.

Источники

  1. https://spring-projects.ru/guides/lessons/lesson-2/
  2. https://habr.com/ru/articles/222579/
  3. https://github.com/Shell26/Java-Developer/blob/master/spring.md#%D0%9E%D1%81%D0%BE%D0%B1%D0%B5%D0%BD%D0%BD%D0%BE%D1%81%D1%82%D0%B8-%D0%B8-%D0%BF%D1%80%D0%B5%D0%B8%D0%BC%D1%83%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0-spring-framework
  4. https://javarush.com/quests/lectures/questspring.level06.lecture22
  5. https://www.baeldung.com/hibernate-identifiers
  6. https://javarush.com/quests/lectures/questhibernate.level13.lecture02
  7. https://habr.com/ru/articles/674882/
  8. https://medium.com/@kirill.sereda/%D1%82%D1%80%D0%B0%D0%BD%D0%B7%D0%B0%D0%BA%D1%86%D0%B8%D0%B8-%D0%B2-spring-framework-a7ec509df6d2
  9. https://habr.com/ru/companies/otus/articles/764244/
  10. https://habr.com/ru/companies/jugru/articles/218203/
  11. https://habr.com/ru/companies/otus/articles/529692/
⚠️ **GitHub.com Fallback** ⚠️