Перейти к основному содержимому

5.03. Spring

Разработчику Архитектору

Spring

Что такое Spring и зачем он нужен?

Spring — это экосистема фреймворков и инструментов, предназначенных для построения надёжных, гибких и легко тестируемых корпоративных приложений на платформе Java SE и Java EE (ныне Jakarta EE). Изначально созданный в 2002 году Родом Джонсоном как альтернатива тяжеловесной и сложной в сопровождении модели Enterprise JavaBeans (EJB 2.x), Spring быстро стал де-факто стандартом для разработки серверных Java-приложений — от монолитов до распределённых микросервисов.

Главное предназначение Spring — упорядочить и упростить уже существующие возможности Java. Он оборачивает, координирует и делегирует Java API. Spring не требует наследования от его базовых классов и не навязывает жёсткой архитектуры; вместо этого он предлагает парадигмы проектирования, реализованные через инверсию управления и аспектно-ориентированное программирование. Это делает приложения, построенные на Spring, слабосвязанными, модульными и поддающимися тестированию вне контейнера — качества, критически важные в промышленной разработке.

Следует сразу провести терминологическое различие:

  • Spring Framework — это ядро экосистемы: набор модулей, реализующих базовые механизмы: управление объектами, внедрение зависимостей, интеграция с БД, транзакции, веб-обработка. Он полностью независим от Servlet API, хотя и умеет с ним работать.
  • Spring Boot, Spring Security, Spring Data и другие — это проекты, построенные на основе Spring Framework, решающие конкретные задачи (ускорение старта, безопасность, работа с данными и пр.). Они не являются частью ядра, но тесно с ним интегрированы и часто используются совместно.

Таким образом, «Spring» в широком смысле — это целая платформа, а в узком — её фундаментальный компонент: Spring Framework.


Исторический контекст и мотивация дизайна

Для понимания логики Spring важно вспомнить состояние Java-разработки в начале 2000-х. Платформа Java EE (тогда J2EE) предоставляла мощный, но чрезвычайно комплексный и вербозный стек: Enterprise JavaBeans (EJB), Java Naming and Directory Interface (JNDI), Java Transaction API (JTA), Java Message Service (JMS) и др. Простое приложение требовало десятков XML-файлов, наследования от фреймворковых классов и запуска внутри полноценного application server’а (WebLogic, WebSphere). Тестирование вне контейнера было затруднено, а изменения конфигурации — трудоёмки.

Spring предложил радикальный поворот: отказ от требований к среде выполнения. Spring-приложение можно запустить как обыкновенное Java-приложение (main()), без application server’а. Он заменил необходимость наследования — композицией и внедрением зависимостей, а жёсткую связку с инфраструктурой — абстракциями (например, JdbcTemplate вместо «голого» java.sql.Connection). Spring реализовал идеи Java EE проще и легче — и, в конечном итоге, сильно повлиял на эволюцию самой Jakarta EE (например, внедрение CDI — Contexts and Dependency Injection — во многом вдохновлено Spring DI).

Ключевая философская установка Spring — «non-invasive» (ненавязчивость): ваши классы не обязаны знать о Spring. Они могут быть обычными POJO (Plain Old Java Objects). Вся «магия» — внешняя, управляется фреймворком.


Архитектура Spring Framework

Spring Framework спроектирован как модульная иерархия. Он состоит из более чем 20 модулей, объединённых в несколько слоёв. Каждый модуль — независимая JAR-библиотека, которую можно подключить отдельно, без остальных. Это позволяет собирать приложение «по частям», избегая избыточных зависимостей.

Ниже приведена структура по слоям, от ядра к периферии, с пояснением роли каждого компонента:

1. Core Container (Ядро контейнера)

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

  • spring-core — базовые утилиты, используемые во всём фреймворке: Resource (унифицированный доступ к файлам, URL, classpath), TypeConverter, аннотация @Nullable, механизм обработки свойств (PropertyEditor, Environment). Ядро, на котором строится остальное.
  • spring-beans — реализация IoC-контейнера. Содержит интерфейсы BeanFactory и ApplicationContext, а также всю логику создания, настройки, связывания и уничтожения бинов (beans) — объектов, управляемых Spring.
  • spring-context — расширение контейнера, добавляющее поддержку международизации (MessageSource), событийной модели (ApplicationEventPublisher), планировщиков (TaskExecutor, TaskScheduler), валидации (Validator). Именно ApplicationContext (а не BeanFactory) используется в подавляющем большинстве приложений.
  • spring-expression (SpEL) — мощный язык выражений, встроенный во время выполнения. Позволяет динамически вычислять значения в конфигурации: @Value("#{systemProperties['user.region']}"), @Value("#{T(java.lang.Math).random() * 100}"), вызов методов, доступ к коллекциям. Используется в XML/Java-конфигурации, Spring Security, Spring Data и др.

2. Data Access / Integration (Доступ к данным и интеграция)

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

  • spring-jdbc — абстракция над JDBC. Устраняет шаблонный код: открытие/закрытие соединений, обработка SQLException. JdbcTemplate позволяет писать запросы одной строкой, не заботясь о ручном управлении ресурсами.
  • spring-orm — интеграция с ORM-фреймворками: Hibernate, JPA, JDO, iBatis/MyBatis. Spring управляет жизненным циклом ORM (сессии, фабрики), обёртывает исключения (HibernateException → DataAccessException) и координирует транзакции.
  • spring-oxm — Object/XML Mapping. Упрощает сериализацию/десериализацию Java-объектов в XML и обратно с помощью JAXB, Castor, XMLBeans, JiBX, XStream.
  • spring-jms — абстракция над Java Message Service. Позволяет отправлять и получать сообщения через JmsTemplate, а также реализовывать слушателей (MessageListener) без работы с Connection, Session и MessageConsumer вручную.
  • spring-txдекларативное и программное управление транзакциями. Позволяет объявить метод как транзакционный (@Transactional) и не думать о begin(), commit(), rollback(). Поддерживает как локальные (DataSourceTransactionManager), так и распределённые (JtaTransactionManager) транзакции, вложенные транзакции и точки сохранения (savepoints).

3. Web (Веб-слой)

Предназначен для построения веб-приложений и RESTful API.

  • spring-web — базовая инфраструктура: клиенты HTTP (RestTemplate, WebClient), кодеки, обработка multipart-запросов, поддержка WebSocket.
  • spring-webmvc — реализация Model-View-Controller на основе Servlet API. Содержит DispatcherServlet — центральный контроллер, распределяющий запросы по обработчикам, а также всё, что связано с маршрутизацией (@Controller, @RequestMapping), преобразованием данных (HttpMessageConverter), резолверами представлений (ViewResolver).
  • spring-webfluxреактивный стек для построения асинхронных, неблокирующих веб-приложений (альтернатива MVC). Использует Project Reactor (Mono, Flux) и Netty в качестве встроенного сервера.

4. AOP и инструментирование

  • spring-aop — поддержка аспектно-ориентированного программирования. Позволяет выносить сквозную функциональность (логгирование, безопасность, кэширование, транзакции) в отдельные аспекты, не засоряя бизнес-код. Реализован через прокси-объекты (JDK Dynamic Proxies или CGLIB).
  • spring-aspects — интеграция с AspectJ — более мощным, но более сложным AOP-фреймворком, способным модифицировать байткод.
  • spring-instrument — поддержка класс-лоадеров для инструментирования (например, для отслеживания времени выполнения методов во время загрузки классов).

5. Тестирование

  • spring-test — инструменты для интеграционного тестирования: загрузка контекста в JUnit (@SpringBootTest), мокинг зависимостей (@MockBean), транзакционные тесты (@Transactional), тестирование веб-слоя (@WebMvcTest). Позволяет проверять взаимодействие компонентов в условиях, максимально приближенных к production.

Основные концепции

Чтобы понять Spring, необходимо глубоко освоить четыре взаимосвязанных понятия.

Inversion of Control (IoC) — Инверсия управления

Это принцип проектирования. Суть: вместо того чтобы объект сам создавал или искал свои зависимости, их ему предоставляет внешний субъект — «контейнер». Традиционно объект контролирует поток выполнения: он решает, когда и какие другие объекты создавать. При IoC этот контроль инвертируется: поток управления передаётся контейнеру. Объект становится пассивным участником: он объявляет, что ему нужно, а контейнер заботится о том, как это получить.

IoC снижает связность: класс зависит не от конкретной реализации PaymentService, а от интерфейса PaymentService. Конкретная реализация (CreditCardPaymentService, PayPalPaymentService) поставляется извне — и может меняться без изменения кода класса.

Dependency Injection (DI) — Внедрение зависимостей

Это механизм реализации IoC. DI — это процесс, при котором зависимости передаются объекту, а не запрашиваются им самим. В Spring существует три способа DI:

  1. Внедрение через конструктор — наиболее рекомендуемый. Гарантирует, что объект создаётся в полностью инициализированном состоянии. Неизменяемые зависимости.

    @Service
    public class OrderService {
    private final PaymentService paymentService;

    public OrderService(PaymentService paymentService) {
    this.paymentService = paymentService;
    }
    }
  2. Внедрение через setter-метод — позволяет изменять зависимости после создания объекта (редко нужно).

    @Service
    public class OrderService {
    private PaymentService paymentService;

    @Autowired
    public void setPaymentService(PaymentService paymentService) {
    this.paymentService = paymentService;
    }
    }
  3. Внедрение через поле — самый компактный, но нарушает инкапсуляцию и затрудняет unit-тестирование (нельзя передать зависимость вручную без рефлексии). Не рекомендуется, кроме тривиальных случаев.

    @Service
    public class OrderService {
    @Autowired
    private PaymentService paymentService;
    }

Spring использует аннотацию @Autowired (или JSR-330 @Inject) для автоматического связывания. По умолчанию связывание происходит по типу; при конфликте — по имени бина или с помощью @Qualifier.

Bean — управляемый объект

В терминологии Spring бин — это любой Java-объект, жизненный цикл которого управляется Spring IoC-контейнером. Это не специальный тип объекта — это роль. Любой POJO может стать бином, если Spring знает о нём и берёт на себя его создание, настройку и уничтожение.

Бины описываются метаданными конфигурации. Ранее — в XML:

<bean id="orderService" class="com.example.OrderService" />
<bean id="paymentService" class="com.example.CreditCardPaymentService" />

Сегодня — в основном через аннотации и Java-конфигурацию:

@Configuration
public class AppConfig {
@Bean
public PaymentService paymentService() {
return new CreditCardPaymentService();
}

@Bean
public OrderService orderService(PaymentService paymentService) {
return new OrderService(paymentService);
}
}

Или автоматически — через компонентное сканирование:

@ComponentScan("com.example")
public class AppConfig { }

@Service
public class OrderService { ... }

Каждый бин имеет:

  • Имя (bean name) — уникальный идентификатор в контексте (по умолчанию — имя класса с маленькой буквы).
  • Область видимости (scope)singleton (по умолчанию, один экземпляр на контекст), prototype (новый экземпляр при каждом запросе), request, session, websocket.
  • Жизненный цикл — Spring вызывает @PostConstruct после инициализации и @PreDestroy перед уничтожением. Также можно реализовать InitializingBean / DisposableBean.

ApplicationContext — контекст приложения

Это самый важный интерфейс в Spring. ApplicationContext — это расширенная реализация BeanFactory, представляющая собой полную конфигурацию приложения в runtime. Его можно рассматривать как реестр бинов плюс инфраструктурные сервисы.

Когда приложение запускается, Spring:

  1. Читает конфигурацию (XML, @Configuration, @Component-классы).
  2. Создаёт метамодель — описание всех бинов, их зависимостей, настроек.
  3. Выполняет разрешение зависимостей (dependency resolution) — строит граф объектов.
  4. Создаёт бины в правильном порядке (уважая зависимости), применяет пост-процессоры (BeanPostProcessor), внедряет зависимости.
  5. Вызывает методы инициализации.
  6. Складывает готовые бины в ApplicationContext.

После этого любой компонент может получить доступ к любому другому бину через ApplicationContext (хотя прямой вызов context.getBean() считается плохой практикой — зависимости должны внедряться, а не извлекаться вручную).

Именно ApplicationContext обеспечивает:

  • Единое пространство имён для бинов.
  • Централизованное управление жизненным циклом.
  • Доступ к инфраструктурным сервисам: событиям, интернационализации, окружению.
  • Интеграцию с другими слоями (транзакции, AOP, веб).

Способы конфигурации

Spring прошёл путь от XML-диктатуры к полностью автоматизированной настройке. Сегодня существует три основных подхода — и они могут сосуществовать.

1. XML-конфигурация (устаревшая, но важная для понимания)

Исторически первый способ. Конфигурация описывается в отдельных XML-файлах (например, applicationContext.xml). Бины объявляются тегами <bean>, зависимости — через <property> или <constructor-arg>.

Преимущества: полная независимость от Java-кода, централизованное управление, возможность изменения без перекомпиляции.
Недостатки: многословность, отсутствие проверки типов на этапе компиляции, сложность поддержки при росте приложения.

<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
https://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource">
<property name="url" value="${db.url}" />
<property name="username" value="${db.user}" />
<property name="password" value="${db.password}" />
</bean>

<bean id="userRepository" class="com.example.UserRepositoryImpl">
<constructor-arg ref="dataSource" />
</bean>
</beans>

2. Java-конфигурация (@Configuration, @Bean)

Введена в Spring 3.0 как более типобезопасная и выразительная альтернатива XML. Конфигурация — это обычный Java-класс с аннотацией @Configuration, а методы, помеченные @Bean, возвращают экземпляры бинов.

Преимущества: проверка типов, автодополнение в IDE, рефакторинг, условная конфигурация (@Profile, @Conditional), возможность использовать всю мощь Java (циклы, логика).
Недостатки: необходимость компиляции при изменении.

@Configuration
@PropertySource("classpath:db.properties")
public class DatabaseConfig {

@Value("${db.url}")
private String url;

@Bean
public DataSource dataSource() {
BasicDataSource ds = new BasicDataSource();
ds.setUrl(url);
ds.setUsername("user");
ds.setPassword("pass");
return ds;
}

@Bean
public UserRepository userRepository(DataSource dataSource) {
return new UserRepositoryImpl(dataSource);
}
}

3. Автоматическое конфигурирование (Component Scanning + Spring Boot)

Максимально декларативный подход. Разработчик помечает классы аннотациями (@Component, @Service, @Repository, @Controller), а Spring автоматически обнаруживает их в указанном пакете (@ComponentScan) и регистрирует как бины. Зависимости внедряются по типу.

Spring Boot радикально развивает эту идею: он анализирует classpath, находит зависимости (H2, MySQL, Redis), и автоматически создаёт и настраивает нужные бины (DataSource, RedisConnectionFactory, JpaTransactionManager и т.д.). Поведение можно кастомизировать через application.properties/application.yml.

Это позволяет писать приложения с нулевой конфигурацией:

@SpringBootApplication // = @Configuration + @EnableAutoConfiguration + @ComponentScan
public class MyApp {
public static void main(String[] args) {
SpringApplication.run(MyApp.class, args);
}
}

Важно: автоматическая конфигурация не отменяет понимания. Она лишь скрывает рутину. Чтобы эффективно использовать Spring Boot, необходимо чётко представлять, какие бины создаются, как они связаны и как их можно переопределить.


Spring MVC

Spring MVC — это реализация шаблона проектирования Model-View-Controller, построенная поверх Java Servlet API. Его цель — чётко разделить три ответственности:

  • Model — данные и бизнес-логика (обычно представлены POJO, сервисами, репозиториями).
  • View — представление результатов (JSP, Thymeleaf, Freemarker, или просто JSON/XML для REST).
  • Controller — посредник: принимает HTTP-запрос, вызывает модель, выбирает представление.

В отличие от «классического» MVC (например, в Swing), веб-MVC не предполагает прямого обновления View моделью. Веб — это stateless-протокол: каждый запрос независим. Поэтому Controller собирает данные из модели, помещает их в контекст представления и передаёт управление View.

Ядро архитектуры: DispatcherServlet

Все запросы в Spring MVC проходят через один сервлет — DispatcherServlet. Это фронт-контроллер, централизующий обработку. Его работа — координировать взаимодействие компонентов:

  1. Получение запроса от контейнера (Tomcat, Jetty).
  2. Определение обработчика (handler) через HandlerMapping. По умолчанию используется RequestMappingHandlerMapping, который сопоставляет URL/HTTP-метод с методом класса, помеченного @Controller или @RestController.
  3. Подготовка аргументов вызова метода через HandlerAdapter (обычно RequestMappingHandlerAdapter). Здесь происходит:
    • привязка параметров запроса (@RequestParam, @PathVariable);
    • десериализация тела (@RequestBody);
    • валидация (@Valid);
    • внедрение специальных объектов (HttpServletRequest, Model).
  4. Вызов метода контроллера.
  5. Обработка возвращаемого значения:
    • строка → имя представления (для @Controller);
    • объект → сериализация в JSON/XML (для @RestController);
    • ResponseEntity → полный контроль над HTTP-ответом.
  6. Рендеринг View (если применимо) через ViewResolver, который по имени (например, "home") находит шаблон (home.html) и заполняет его данными из Model.

Этот процесс полностью настраиваем: любой этап можно заменить своей реализацией — и это ключевое преимущество Spring MVC перед фреймворками с «жёсткой» архитектурой.

Контроллеры: @Controller и @RestController

Класс, помеченный @Controller, сообщает Spring, что он содержит методы для обработки веб-запросов. Методы аннотируются метками маршрутизации:

  • @GetMapping("/users/{id}") — GET-запрос к /users/123;
  • @PostMapping("/orders") — POST-запрос для создания заказа;
  • @PutMapping, @DeleteMapping — для остальных HTTP-глаголов.

Аргументы метода могут включать:

  • @PathVariable("id") Long id — извлечение из URL (/users/{id});
  • @RequestParam("page") int page — из query-параметров (?page=2);
  • @RequestBody User user — десериализация JSON/XML в объект (через Jackson или JAXB);
  • Model model — контейнер для передачи данных во View;
  • HttpServletResponse response — прямой доступ к ответу (редко нужно, нарушает абстракцию).

Возвращаемое значение:

  • String → имя представления (например, "user-profile"user-profile.html);
  • ModelAndView → явное указание View и Model;
  • void → имя View выводится из URL (редко);
  • Любой POJO + @ResponseBody (или класс с @RestController) → автоматическая сериализация в JSON/XML.

@RestController — это просто @Controller + @ResponseBody на уровне класса. Он предназначен исключительно для построения RESTful API, где ответ всегда — данные (не HTML).

Обработка исключений

Spring MVC предоставляет централизованный механизм обработки ошибок через @ControllerAdvice. Это компонент, который «ловит» исключения, выброшенные любым контроллером, и преобразует их в структурированный HTTP-ответ.

@ControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<ErrorResponse> handleUserNotFound(UserNotFoundException ex) {
ErrorResponse error = new ErrorResponse("USER_NOT_FOUND", ex.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
}

Такой подход избавляет от дублирования try-catch в каждом методе и гарантирует единообразный формат ошибок — критически важно для клиентских приложений.


Spring Data

Одна из самых успешных абстракций Spring — Spring Data. Его задача — устранить рутину CRUD-операций и повторяющегося SQL/JPA-кода, сохранив при этом гибкость.

Основная идея: Repository как интерфейс

Разработчик объявляет интерфейс, наследуемый от JpaRepository<T, ID> (для JPA) или MongoRepository<T, ID> (для MongoDB), и Spring динамически создаёт реализацию этого интерфейса во время выполнения.

public interface UserRepository extends JpaRepository<User, Long> {
List<User> findByEmail(String email);
List<User> findByLastNameStartingWith(String prefix);
@Query("SELECT u FROM User u WHERE u.age > :minAge")
List<User> findAdults(@Param("minAge") int minAge);
}

Spring Data анализирует:

  • Имя метода: findByEmail → генерирует JPQL/HQL-запрос WHERE email = ?;
  • Параметры: StartingWithLIKE ?%, BetweenBETWEEN ? AND ?;
  • Аннотацию @Query: позволяет писать произвольные запросы на JPQL, SQL или (для NoSQL) нативные запросы.

Все операции (save(), findById(), delete()) уже реализованы в базовом интерфейсе. Нет необходимости писать классы UserRepositoryImpl.

Поддержка различных хранилищ

Spring Data — это фасад. За ним скрываются специализированные модули:

  • spring-data-jpa — для реляционных БД через Hibernate, EclipseLink;
  • spring-data-mongodb — для документных БД;
  • spring-data-redis — для in-memory хранилищ;
  • spring-data-elasticsearch — для поисковых движков;
  • spring-data-jdbc — упрощённая альтернатива JPA без lazy loading и кэширования.

Все они используют единый подход: интерфейс Repository → автоматическая реализация → расширение через имена методов или аннотации.

Транзакционность и пагинация

Любой метод репозитория по умолчанию выполняется в транзакции (благодаря @Transactional, который Spring Data добавляет в реализацию). Пагинация поддерживается через типы Page<T> и Pageable:

Page<User> findByLastName(String name, Pageable pageable);
// вызов: userRepository.findByLastName("Smith", PageRequest.of(0, 20));

Фреймворк автоматически добавляет LIMIT/OFFSET (или эквивалент) и выполняет отдельный запрос COUNT(*) для расчёта общего числа страниц.


Spring Security

Spring Security — это независимый фреймворк, но настолько тесно интегрированный с Spring, что воспринимается как его часть. Он решает две задачи:

  1. Аутентификация — «Кто вы?» (проверка учётных данных: логин/пароль, JWT, OAuth2-токен).
  2. Авторизация — «Что вы можете?» (проверка прав доступа к ресурсу: URL, методу, данным).

Архитектура: цепочка фильтров

Все запросы к защищённому приложению проходят через цепочку сервлет-фильтров (FilterChain). Spring Security регистрирует свой главный фильтр — FilterChainProxy, который делегирует работу конкретным SecurityFilter’ам:

  • UsernamePasswordAuthenticationFilter — обрабатывает форму входа;
  • BearerTokenAuthenticationFilter — извлекает JWT из заголовка Authorization;
  • ExceptionTranslationFilter — перехватывает AccessDeniedException и AuthenticationException;
  • FilterSecurityInterceptor — финальная проверка: имеет ли аутентифицированный пользователь право на доступ к данному URL.

Каждый фильтр может разрешить запрос (передача дальше по цепочке), отклонить (отправить 401/403) или инициировать аутентификацию (перенаправить на страницу логина).

Конфигурация: Java DSL

Современный подход — декларативная настройка через SecurityFilterChain:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
)
.logout(logout -> logout.permitAll());
return http.build();
}
}

Здесь:

  • authorizeHttpRequests — правила доступа по URL;
  • hasRole("ADMIN") — проверка наличия роли ROLE_ADMIN (префикс ROLE_ добавляется автоматически);
  • formLogin — настройка стандартной формы аутентификации;
  • logout — обработка выхода.

Для REST API вместо formLogin используется httpBasic() или настройка OAuth2/JWT.

Защита на уровне методов

Помимо URL, Spring Security позволяет ограничивать доступ к методам с помощью аннотаций:

  • @PreAuthorize("hasRole('USER')") — проверка до вызова метода;
  • @PostAuthorize("returnObject.owner == principal.username") — проверка после вызова (редко, дорого);
  • @Secured("ROLE_ADMIN") — упрощённый вариант @PreAuthorize.

Это особенно полезно в сервисном слое, где один и тот же метод может вызываться из разных точек входа (веб, CLI, scheduler).

Поддержка стандартов

Spring Security не изобретает протоколы — он реализует их:

  • OAuth2 / OpenID Connect — для делегированной аутентификации (Google, GitHub);
  • JWT — для stateless-аутентификации в микросервисах;
  • LDAP / Active Directory — для интеграции с корпоративными каталогами;
  • CSRF-защита — включена по умолчанию для stateful-приложений (форм), отключается для REST.

Управление транзакциями

Транзакции — критически важный механизм для обеспечения целостности данных. Spring предоставляет единый API для работы с транзакциями, независимо от底层 технологии (JDBC, JPA, JTA).

@Transactional: простота и мощь

Аннотация @Transactional — основной инструмент. Её можно ставить на класс (все публичные методы) или на отдельный метод.

@Service
public class OrderService {

@Transactional
public Order createOrder(OrderRequest request) {
Order order = orderRepository.save(new Order());
paymentService.processPayment(order.getId());
inventoryService.reserveItems(order.getItems());
return order;
}
}

Spring создаёт прокси вокруг OrderService. При вызове createOrder():

  1. Открывается транзакция (через PlatformTransactionManager);
  2. Выполняется метод;
  3. При успехе — commit();
  4. При исключении — rollback() (если исключение непроверяемое или явно указано в rollbackFor).

Ключевые параметры

  • propagation — как вести себя при вложенном вызове:
    • REQUIRED (по умолчанию) — использовать существующую транзакцию или создать новую;
    • REQUIRES_NEW — всегда новая транзакция (приостанавливает текущую);
    • NESTED — вложенная транзакция с точкой сохранения (поддерживается не всеми БД).
  • isolation — уровень изоляции (READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE);
  • readOnly = true — подсказка СУБД для оптимизации (только для операций чтения);
  • timeout = 30 — максимальное время выполнения в секундах.

Программное управление

Для сложных сценариев (например, частичный rollback) доступен TransactionTemplate:

transactionTemplate.execute(status -> {
try {
orderRepository.save(order);
paymentService.process(payment);
} catch (PaymentException ex) {
status.setRollbackOnly(); // пометить как rollback, но продолжить выполнение
handlePartialFailure();
}
return null;
});

Тестирование

Spring уделяет тестированию особое внимание. Он различает:

  • Unit-тесты: проверяют один класс, все зависимости замоканы (Mockito). Spring здесь не участвует.
  • Интеграционные тесты: проверяют взаимодействие компонентов, включая Spring-контекст.

Инструменты Spring Test

  • @SpringBootTest — загружает полный контекст приложения. Тяжеловесно, но необходимо для end-to-end проверки.
  • @DataJpaTest — загружает только слой данных: репозитории, DataSource, транзакции. Использует in-memory БД (H2) по умолчанию. Отлично для тестирования запросов.
  • @WebMvcTest — загружает только веб-слой: контроллеры, ObjectMapper, фильтры безопасности. Сервисы мокаются. Идеален для проверки маршрутов и статус-кодов.
  • @TestConfiguration — добавляет временные бины только для тестов (например, мок-реализацию внешнего API).

Пример: тестирование контроллера

@WebMvcTest(UserController.class)
class UserControllerTest {

@Autowired
private MockMvc mockMvc;

@MockBean
private UserService userService;

@Test
void shouldReturnUserWhenFound() throws Exception {
User user = new User(1L, "test@example.com");
when(userService.findById(1L)).thenReturn(user);

mockMvc.perform(get("/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.email").value("test@example.com"));
}
}

MockMvc эмулирует HTTP-запрос без запуска сервера — быстро и надёжно.


Spring Boot: концепция «convention over configuration»

Если Spring Framework — это конструктор, то Spring Boot — это готовый автомобиль с предустановленными опциями. Его три кита:

  1. Auto-configuration — анализ classpath и автоматическое создание бинов. Наличие spring-boot-starter-data-jpa и HikariCP → Spring Boot создаёт DataSource, EntityManagerFactory, JpaTransactionManager.
  2. Starter-зависимости — предопределённые наборы библиотек для конкретных задач:
    • spring-boot-starter-web — веб (Tomcat, Spring MVC, Jackson);
    • spring-boot-starter-data-jpa — JPA + Hibernate + транзакции;
    • spring-boot-starter-security — Spring Security.
  3. Встроенные серверы — приложение — это executable JAR с Tomcat/Jetty/Undertow внутри. Не требует развёртывания в application server.

Механизм автонастройки

Каждая автонастройка — это @Configuration-класс с аннотацией @Conditional*. Например, DataSourceAutoConfiguration активируется только если:

  • В classpath есть DataSource;
  • Пользователь не предоставил свой DataSource (@ConditionalOnMissingBean);
  • Заданы свойства spring.datasource.url.

Это позволяет гибко переопределять поведение: достаточно объявить свой DataSource — и автонастройка отключится.

Внешнее конфигурирование

Spring Boot поддерживает 17+ источников конфигурации (в порядке приоритета):

  1. Параметры командной строки (--server.port=8081);
  2. Переменные окружения (SERVER_PORT=8081);
  3. application.properties / application.yml в classpath;
  4. Профиль-специфичные файлы (application-prod.yml).

Это позволяет использовать один и тот же JAR в dev, test, prod — меняя только окружение.


Наблюдаемость

Для промышленного применения недостаточно, чтобы приложение работало — необходимо понимать, как оно работает. Spring Boot предоставляет встроенные механизмы наблюдаемости через Spring Boot Actuator — модуль, exposing операционные конечные точки (endpoints) через HTTP или JMX.

Что такое endpoint?

Endpoint — это программный интерфейс, предоставляющий информацию о состоянии приложения:

  • /actuator/health — общее состояние здоровья (UP, DOWN, OUT_OF_SERVICE);
  • /actuator/metrics — список доступных метрик (CPU, память, HTTP-запросы, пул БД);
  • /actuator/env — все свойства окружения и конфигурации;
  • /actuator/beans — полный список бинов в контексте;
  • /actuator/mappings — зарегистрированные URL-маршруты;
  • /actuator/threaddump — дамп потоков выполнения;
  • /actuator/heapdump — дамп кучи (требует явного включения).

По умолчанию многие endpoint’ы отключены в production (/env, /heapdump) из соображений безопасности. Их можно включить выборочно, настроив права доступа через Spring Security.

Здоровье (Health) как контракт

Endpoint /health — это не просто «сервер жив / мёртв». Он интегрируется с внешними зависимостями: БД, Redis, RabbitMQ, внешние API. Для каждой зависимости Spring Boot регистрирует HealthIndicator:

@Component
public class ExternalApiHealthIndicator implements HealthIndicator {
@Override
public Health health() {
try {
ResponseEntity<String> res = restTemplate.getForEntity("https://api.example.com/health", String.class);
if (res.getStatusCode().is2xxSuccessful()) {
return Health.up().withDetail("status", "reachable").build();
} else {
return Health.down().withDetail("status", res.getStatusCode()).build();
}
} catch (Exception e) {
return Health.down(e).build();
}
}
}

Если хотя бы один HealthIndicator возвращает DOWN, общий статус становится DOWN. Это позволяет оркестраторам (Kubernetes, Docker Swarm) автоматически перезапускать нездоровые экземпляры.

Метрики и интеграция с системами мониторинга

Spring Boot использует Micrometer — фасад над системами метрик (Prometheus, Graphite, Datadog, New Relic). Он предоставляет типизированные метрики:

  • Counter — монотонно возрастающее значение (число запросов);
  • Gauge — мгновенное значение (размер пула соединений);
  • Timer — гистограмма времени выполнения (латентность запросов);
  • DistributionSummary — гистограмма объёмов (размер ответа в байтах).

Каждый HTTP-запрос автоматически генерирует метрики:

  • http.server.requests — таймер по методу, URI, статусу;
  • jdbc.connections.active — активные соединения с БД;
  • jvm.memory.used — использование памяти.

Эти метрики экспортируются в Prometheus в формате текста по /actuator/prometheus, после чего визуализируются в Grafana. Таким образом, Spring Boot обеспечивает сквозную трассировку от запроса до ресурсов JVM без ручного кода.


Spring Cloud

Когда приложение растёт, оно перестаёт быть монолитом и превращается в систему сервисов. Spring Cloud — это набор инструментов для управления сложностью распределённых систем: обнаружение сервисов, конфигурация, отказоустойчивость, маршрутизация.

Service Discovery: как найти нужный сервис?

В микросервисной архитектуре экземпляры сервисов динамически появляются и исчезают. Жёстко прописывать IP-адреса в коде невозможно. Решение — центральный реестр сервисов.

Spring Cloud интегрируется с:

  • Eureka (Netflix) — сервер регистрации и discovery;
  • Consul (HashiCorp) — multi-purpose tool (discovery, config, KV-store);
  • Zookeeper (Apache) — координация распределённых систем.

Сервис при старте регистрируется в реестре, передавая:

  • имя (например, user-service);
  • IP и порт;
  • метаданные (версия, зона доступности).

Клиенты (другие сервисы) запрашивают у реестра список экземпляров user-service и выбирают один (обычно через round-robin). Spring Cloud автоматизирует это через @LoadBalanced RestTemplate или WebClient:

@Bean
@LoadBalanced
public WebClient.Builder webClientBuilder() {
return WebClient.builder();
}

// Вызов: webClient.get().uri("http://user-service/users/1").retrieve()...

Здесь user-service — логическое имя, а не хост. Spring Cloud под капотом преобразует его в реальный URL через discovery-клиент.

Centralized Configuration

Когда сервисов десятки, изменение конфигурации в каждом application.yml — кошмар. Spring Cloud Config Server решает это:

  1. Создаётся отдельный сервис (config-server), который читает конфигурации из Git-репозитория (application.yml, user-service-prod.yml);
  2. Клиентские сервисы при старте запрашивают у Config Server свои настройки;
  3. Изменения в Git → автоматическая перезагрузка (@RefreshScope) без перезапуска.

Конфигурация становится версионируемой, аудируемой и централизованной.

Отказоустойчивость

Сети ненадёжны. Вызов внешнего сервиса может зависнуть на секунды. Без защиты это приведёт к исчерпанию потоков и полному падению системы. Spring Cloud Circuit Breaker (на базе Resilience4j или Sentinel) реализует паттерн аварийного выключателя:

  • Закрытое состояние — запросы проходят нормально;
  • При превышении порога ошибок/таймаутов → открытое состояние — все вызовы сразу падают с исключением;
  • Через заданное время → половинчатое состояние — пробные запросы; при успехе — возврат в закрытое.

Пример:

@CircuitBreaker(name = "paymentService", fallbackMethod = "fallbackPayment")
public Payment processPayment(Order order) {
return paymentClient.process(order);
}

private Payment fallbackPayment(Order order, Exception ex) {
return new Payment(order.getId(), "PENDING", "Service unavailable");
}

Дополняет это Spring Retry — автоматические повторные попытки при временных сбоях (например, сетевой таймаут).

API Gateway

Клиенты (браузеры, мобильные приложения) не должны знать о внутренней топологии сервисов. Spring Cloud Gateway — это реактивный reverse proxy, который:

  • маршрутизирует запросы (/users/**user-service);
  • применяет фильтры (аутентификация, rate limiting, логгирование);
  • агрегирует ответы от нескольких сервисов (GraphQL-like);
  • преобразует протоколы (HTTP → gRPC).

Фильтры — это цепочка обработчиков, где каждый может модифицировать запрос/ответ до передачи дальше.


Реактивное программирование

Традиционный Spring MVC — блокирующий: каждый запрос занимает один поток. При высокой нагрузке это приводит к исчерпанию пула потоков. Spring WebFlux предлагает неблокирующую, асинхронную модель на основе Reactive Streams.

Основы Reactive Streams

Reactive Streams — это стандарт (JSR-379) для обработки асинхронных потоков данных с обратным давлением (backpressure). Его реализует Project Reactor — основа WebFlux:

  • Mono<T> — поток, который эмитит 0 или 1 элемент (аналог CompletableFuture<T>);
  • Flux<T> — поток, который эмитит 0..N элементов (аналог Stream<T> + асинхронность).

Пример контроллера:

@RestController
public class UserController {

private final UserRepository userRepository;

@GetMapping("/users/{id}")
public Mono<User> getUser(@PathVariable Long id) {
return userRepository.findById(id); // возвращает Mono<User>
}

@GetMapping("/users")
public Flux<User> getAllUsers() {
return userRepository.findAll(); // возвращает Flux<User>
}
}

Важно: реактивность распространяется по цепочке. Если репозиторий возвращает Mono, а сервис вызывает блокирующий метод — вы получите ложную реактивность. Для полного эффекта нужны:

  • неблокирующий драйвер БД (R2DBC вместо JDBC);
  • реактивные клиенты (WebClient вместо RestTemplate);
  • сервер, поддерживающий реактивность (Netty, Undertow — не Tomcat).

Когда использовать WebFlux?

  • Высокая конкуренция, низкая задержка (тысячи соединений на одном сервере);
  • Долгие операции ввода-вывода (streaming, SSE, WebSocket);
  • Интеграция с реактивными системами (Kafka, Cassandra).

Для типичных CRUD-приложений с умеренной нагрузкой MVC остаётся проще и производительнее — потоки JVM оптимизированы для коротких блокирующих операций.


Продвинутая безопасность

Spring Security предоставляет базовые механизмы, но реальные системы требуют глубокой настройки.

Валидация JWT: собственный JwtDecoder

По умолчанию Spring Security использует NimbusJwtDecoder, но его можно кастомизировать для:

  • валидации подписи через JWKS (JSON Web Key Set) удалённого IdP;
  • проверки кастомных claim’ов ("scope": ["read:users", "write:orders"]);
  • интеграции с внутренними системами (например, проверка статуса пользователя в БД по sub).
@Bean
public JwtDecoder jwtDecoder() {
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwksUri).build();
jwtDecoder.setJwtValidator(new DelegatingOAuth2TokenValidator<>(
JwtValidators.createDefault(),
new CustomScopeValidator() // проверяет scope
));
return jwtDecoder;
}

OAuth2 Resource Server: защита API

В микросервисной архитектуре клиенты получают токен от Authorization Server (Auth0, Keycloak), а сервисы выступают как Resource Server — проверяют токен, но не выдают его.

Конфигурация:

@Configuration
@EnableWebSecurity
public class ResourceServerConfig {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.decoder(jwtDecoder()))
)
.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
);
return http.build();
}
}

После валидации токена Spring создаёт Authentication объект с:

  • principalsub из токена;
  • authorities — преобразованные scope в GrantedAuthority (например, SCOPE_read:usersread:users);
  • details — полезная нагрузка токена.

Аудит и логгирование безопасности

Spring Security позволяет регистрировать события безопасности:

  • AuthenticationSuccessEvent — успешный вход;
  • AuthenticationFailureBadCredentialsEvent — ошибка логина;
  • AuthorizationDeniedEvent — отказ в доступе.

Эти события можно перехватывать через @EventListener и отправлять в SIEM-систему (Splunk, ELK) для анализа атак.

@Component
public class SecurityAuditListener {

@EventListener
public void onAuthenticationSuccess(AuthenticationSuccessEvent event) {
String username = event.getAuthentication().getName();
auditLog.info("User {} authenticated successfully", username);
}

@EventListener
public void onAccessDenied(AuthorizationDeniedEvent event) {
String username = event.getAuthentication().getName();
String resource = event.getSource().toString();
auditLog.warn("Access denied for user {} to {}", username, resource);
}
}

Производительность

Безопасность и надёжность важны, но не в ущерб скорости. Spring предоставляет инструменты для оптимизации.

Декларативное кэширование

Аннотации @Cacheable, @CachePut, @CacheEvict позволяют кэшировать результаты методов:

@Service
public class UserService {

@Cacheable(value = "users", key = "#id")
public User findById(Long id) {
// тяжёлый запрос в БД
return userRepository.findById(id);
}

@CachePut(value = "users", key = "#user.id")
public User update(User user) {
return userRepository.save(user);
}

@CacheEvict(value = "users", key = "#id")
public void delete(Long id) {
userRepository.deleteById(id);
}
}

Поддерживаемые провайдеры: Caffeine (in-memory), Redis, Hazelcast, Ehcache. Spring абстрагирует API — смена кэша требует только изменения конфигурации.

Пул соединений с БД

JDBC-соединения — дорогостоящий ресурс. Всегда используйте пул:

  • HikariCP — самый быстрый и лёгкий (используется по умолчанию в Spring Boot);
  • Tomcat JDBC Pool, Commons DBCP2 — альтернативы.

Ключевые настройки:

  • maximum-pool-size — максимум одновременных соединений (обычно 10–20);
  • connection-timeout — время ожидания свободного соединения (30 сек);
  • idle-timeout, max-lifetime — управление «устаревшими» соединениями.

Неправильная настройка пула — частая причина отказов под нагрузкой.

Lazy Loading и N+1 проблема

JPA по умолчанию загружает связи @OneToMany лениво (LAZY). Это экономит ресурсы, но может привести к N+1 запросам: один запрос на сущность + N запросов на каждую связанную коллекцию.

Решения:

  • JOIN FETCH в JPQL — загрузка связанных данных одним запросом;
  • @EntityGraph — декларативное указание графа загрузки;
  • DTO-проекции через @Query — возврат плоских объектов.

Spring Data JPA поддерживает EntityGraph:

public interface OrderRepository extends JpaRepository<Order, Long> {
@EntityGraph(attributePaths = {"items", "customer"})
Optional<Order> findWithDetailsById(Long id);
}

Тонкая настройка жизненного цикла

Понимание того, как именно Spring создаёт и управляет бинами, необходимо при диагностике сложных проблем: циклических зависимостей, неожиданного поведения при инициализации, кастомной обработки после создания. Процесс инициализации бина — это строго определённая последовательность шагов, каждый из которых может быть расширен.

Этапы жизненного цикла бина (singleton scope)

  1. Инстантация (Instantiation)
    Spring вызывает конструктор бина (через рефлексию или Supplier). На этом этапе бин — «сырой» объект, без внедрённых зависимостей.

  2. Заполнение свойств (Population of properties)
    Выполняется AutowiredAnnotationBeanPostProcessor:

    • разрешаются зависимости (@Autowired, @Value, @Resource);
    • внедряются значения из Environment (${db.url});
    • применяются PropertyEditor или Converter.
  3. Пост-обработка до инициализации (Post-processing before initialization)
    Вызываются все BeanPostProcessor.postProcessBeforeInitialization().
    Именно здесь работают:

    • @PostConstruct (через InitDestroyAnnotationBeanPostProcessor);
    • InitializingBean.afterPropertiesSet() (устаревший, но поддерживаемый);
    • кастомные аннотации (например, @Validated через MethodValidationPostProcessor).
  4. Инициализация (Initialization)
    Выполняется метод, указанный в init-method (XML) или @Bean(initMethod = "...").

  5. Пост-обработка после инициализации (Post-processing after initialization)
    Вызываются BeanPostProcessor.postProcessAfterInitialization().
    Здесь создаются:

    • AOP-прокси (AnnotationAwareAspectJAutoProxyCreator оборачивает бин, если он целевой для аспекта);
    • @Transactional-прокси (BeanFactoryTransactionAttributeSourceAdvisor).
  6. Использование
    Бин готов к внедрению в другие компоненты или к прямому вызову через ApplicationContext.

  7. Уничтожение (Destruction)
    При закрытии контекста:

    • @PreDestroy;
    • DisposableBean.destroy();
    • destroy-method.

Расширение через BeanPostProcessor и BeanFactoryPostProcessor

Эти интерфейсы — точки расширения самого низкого уровня.

  • BeanPostProcessor работает с экземпляром бина. Пример: логгирование всех вызовов методов через динамический прокси:

    @Component
    public class LoggingBeanPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
    if (bean.getClass().isAnnotationPresent(Loggable.class)) {
    return Proxy.newProxyInstance(
    bean.getClass().getClassLoader(),
    bean.getClass().getInterfaces(),
    (proxy, method, args) -> {
    log.info("Calling {} with {}", method.getName(), args);
    return method.invoke(bean, args);
    }
    );
    }
    return bean;
    }
    }
  • BeanFactoryPostProcessor работает с метаданными бинов до создания любых экземпляров. Позволяет динамически менять определения бинов:

    @Component
    public class ConditionalBeanRegistrar implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
    if (isFeatureFlagEnabled()) {
    BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
    GenericBeanDefinition def = new GenericBeanDefinition();
    def.setBeanClass(FeatureService.class);
    registry.registerBeanDefinition("featureService", def);
    }
    }
    }

Эти механизмы лежат в основе @ConfigurationProperties, @Conditional, @Profile — они все реализованы через пост-процессоры.

Разрешение циклических зависимостей

Spring допускает циклы только при внедрении через setter или поле (не через конструктор). Механизм:

  1. Создаётся «сырой» экземпляр A (без зависимостей);
  2. Помещается в «early singleton cache»;
  3. При создании B, который зависит от A, Spring извлекает неполностью инициализированный A из early-cache и внедряет его;
  4. Завершается инициализация A.

Это небезопасно: B может вызвать метод A, пока A ещё не проинициализирован. Spring выдаёт предупреждение BeanCurrentlyInCreationException, если цикл обнаружен при конструкторном DI.


Миграция legacy-конфигураций

Многие корпоративные системы до сих пор используют XML-конфигурацию. Миграция — архитектурное обновление.

Шаг 1: Гибридная конфигурация

Spring позволяет смешивать XML и Java Config:

@Configuration
@ImportResource("classpath:legacy-dao.xml") // подключает XML
public class HybridConfig {
@Bean
public NewService newService() {
return new NewService();
}
}

Это позволяет постепенно переводить компоненты, не переписывая всё сразу.

Шаг 2: Замена @Autowired field injection

Field injection (@Autowired на поле) мешает unit-тестированию. Лучшая практика — конструкторная инъекция. Современные IDE (IntelliJ IDEA) позволяют автоматически рефакторить:

// Было:
@Service
public class OrderService {
@Autowired private PaymentService paymentService;
}

// Стало:
@Service
public class OrderService {
private final PaymentService paymentService;
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
}

Если зависимостей много (>3), стоит пересмотреть SRP (Single Responsibility Principle) — возможно, класс делает слишком много.

Шаг 3: Устранение ApplicationContextAware

Прямые вызовы context.getBean() нарушают DI. Вместо:

@Component
public class LegacyService implements ApplicationContextAware {
private ApplicationContext context;
public void doWork() {
DataService data = context.getBean(DataService.class);
}
}

Используется lookup method injection или ObjectProvider:

@Component
public class ModernService {
private final ObjectProvider<DataService> dataServiceProvider;
public void doWork() {
DataService data = dataServiceProvider.getIfAvailable();
}
}

ObjectProvider — ленивый, безопасный способ получения бина по требованию, без жёсткой связи с контекстом.

Шаг 4: Замена PropertyPlaceholderConfigurer на @ConfigurationProperties

XML-стиль:

<context:property-placeholder location="classpath:app.properties"/>
<bean id="dataSource" class="...">
<property name="url" value="${db.url}"/>
</bean>

Java-стиль:

@ConfigurationProperties(prefix = "db")
@Component
public class DatabaseProperties {
private String url;
private String username;
// геттеры/сеттеры
}

@Configuration
@EnableConfigurationProperties(DatabaseProperties.class)
public class DataSourceConfig {
@Bean
public DataSource dataSource(DatabaseProperties props) {
return DataSourceBuilder.create()
.url(props.getUrl())
.username(props.getUsername())
.build();
}
}

Преимущества: типобезопасность, IDE-поддержка, автоматическая валидация через @Validated.


Spring и Java Platform Module System (JPMS)

С появлением модулей в Java 9 возникла проблема: Spring — reflection-heavy фреймворк, а JPMS ограничивает доступ к внутренностям модулей. Совместимость требует явных открытых пакетов.

Ключевые требования к module-info.java

  1. Открытие пакетов для рефлексии
    Spring читает аннотации и создаёт бины через рефлексию. Пакеты с компонентами должны быть open:

    open module com.example.app {
    requires spring.context;
    requires spring.beans;
    // ...
    }

    open — разрешает deep reflection для всего модуля. Альтернатива — opens com.example.service to spring.core, но это многословно.

  2. Экспорт API-пакетов
    Если модуль предоставляет интерфейсы для других, их пакеты должны быть exported:

    exports com.example.api;
  3. Обработка автоматических модулей
    Большинство библиотек (включая Spring до 5.3) — automatic modules (имя модуля = имя JAR). Их можно require по имени, но без строгих гарантий. Spring Boot 2.4+ и Spring Framework 5.3+ поставляются как явные модули, что упрощает сборку.

Проблемы и решения

  • InaccessibleObjectException — при попытке доступа к private-полям. Решение: флаги JVM (--add-opens java.base/java.lang=ALL-UNNAMED), но это обход, а не решение. Лучше — явное open в module-info.java.
  • Spring AOP и CGLIB — генерация подклассов в runtime. Требует --add-opens для пакетов с целевыми классами.
  • Spring Boot и Fat JAR — JPMS не поддерживает «fat» JAR’ы. Решение: использовать jlink для сборки custom runtime или оставить приложение как classpath-приложение (стандартный путь для Spring Boot).

На практике большинство Spring-приложений продолжают собираться как classpath-приложения, а не модульные — из-за сложности и ограниченной выгоды. JPMS оправдан в библиотеках, но не в бизнес-приложениях.


Native Image: Spring Boot и GraalVM

Традиционные JVM-приложения страдают от долгого старта и высокого потребления памяти. GraalVM Native Image компилирует JVM-байткод в standalone native binary, устраняя overhead JIT и garbage collection.

Как это работает с Spring?

GraalVM требует статического анализа: всё, что может быть вызвано через рефлексию, должно быть явно зарегистрировано. Spring Boot 3+ и Spring Native добавляют поддержку:

  1. Конфигурация времени сборки (@Configuration(proxyBeanMethods = false))
    Упрощает анализ: Spring не создаёт CGLIB-прокси для @Configuration-классов.

  2. Hint’ы для рефлексии
    Spring Native генерирует reflect-config.json, resource-config.json автоматически через @NativeHint:

    @NativeHint(
    types = {User.class, Order.class},
    options = "--enable-url-protocols=http"
    )
    @SpringBootApplication
    public class MyApp { }
  3. Ограничения

    • Thread.stop(), Object.finalize() — не поддерживаются;
    • Динамическая загрузка классов (Class.forName()) — только для заранее объявленных;
    • Прокси через java.lang.reflect.Proxy — работают, CGLIB — нет (используется JDK Proxy);
    • @Scheduled — работает, @Async — требует явной настройки пула.

Преимущества и издержки

  • Плюсы:

    • Старт за 20–100 мс (вместо 2–5 сек);
    • Потребление памяти в 2–5 раз меньше;
    • Более предсказуемый runtime (нет JIT warm-up).
  • Минусы:

    • Время сборки — 2–10 минут;
    • Бинарь привязан к ОС и архитектуре;
    • Отладка — сложнее (нет hot reload, профилирование через perf);
    • Не все библиотеки совместимы (например, некоторые JDBC-драйверы).

Native Image оправдан для:

  • serverless-функций (AWS Lambda, Azure Functions);
  • edge-сервисов с ограничениями по памяти;
  • CLI-инструментов.

Для обычных серверных приложений JVM остаётся оптимальным выбором.


Архитектурные паттерны

Spring не навязывает архитектуру, но предоставляет инструменты для реализации сложных паттернов.

CQRS (Command Query Responsibility Segregation)

Разделение операций на:

  • Commands — изменяют состояние (мутирующие, идемпотентные);
  • Queries — читают состояние (идемпотентные, без побочных эффектов).

Реализация:

  • Разные интерфейсы/классы для команд и запросов:
    // Команда
    public record CreateUserCommand(String email, String name) {}
    @Service
    public class UserCommandService {
    public void handle(CreateUserCommand cmd) {
    userRepository.save(new User(cmd.email(), cmd.name()));
    }
    }

    // Запрос
    public record GetUserQuery(Long id) {}
    @Service
    public class UserQueryService {
    public UserDto handle(GetUserQuery query) {
    return userRepository.findById(query.id())
    .map(u -> new UserDto(u.id(), u.email()))
    .orElseThrow();
    }
    }
  • Разные модели данных:
    • Write model — оптимизирована под запись (нормализованная, с транзакциями);
    • Read model — оптимизирована под чтение (денормализованная, возможно, в отдельной БД — например, Elasticsearch).

Spring Data позволяет легко поддерживать обе модели: JpaRepository для записи, ElasticsearchRepository для чтения.

Event Sourcing

Вместо хранения текущего состояния — хранение последовательности событий, приведших к этому состоянию.

Пример:

  • Событие UserCreated(id=1, email="a@b.com");
  • Событие UserEmailChanged(id=1, old="a@b.com", new="x@y.com").

Преимущества:

  • Аудит «из коробки»;
  • Возможность восстановить состояние на любой момент времени (time travel);
  • Лёгкое построение read-моделей через обработку событий.

Spring поддерживает через Spring Cloud Stream и Eventuate Tram:

@StreamListener("userEvents")
public void onUserCreated(UserCreated event) {
// Обновить read-model в Elasticsearch
elasticsearchClient.index(event.id(), event.email());
}

Saga Pattern

В микросервисах нет глобальных транзакций (XA). Saga — это последовательность локальных транзакций, где каждая сопровождается компенсирующей операцией на случай сбоя.

Пример оформления заказа:

  1. ReserveInventory → если успех, идти дальше; иначе — стоп.
  2. ProcessPayment → если успех, идти дальше; иначе — вызвать CancelInventoryReservation.
  3. ShipOrder → если успех — завершить; иначе — вызвать RefundPayment, затем CancelInventoryReservation.

Реализация:

  • Orchestrated Saga — центральный оркестратор (OrderSagaOrchestrator) управляет шагами через очереди (RabbitMQ, Kafka).
  • Choreographed Saga — сервисы обмениваются событиями (InventoryReserved, PaymentFailed), без центрального узла.

Spring Cloud Stream + Kafka — естественный выбор для Choreographed Saga:

@StreamListener("inventory")
public void onInventoryReserved(InventoryReserved event) {
try {
paymentService.process(event.orderId());
streamBridge.send("payment-out", new PaymentProcessed(event.orderId()));
} catch (Exception e) {
streamBridge.send("inventory-out", new CancelInventory(event.orderId()));
}
}

Практический кейс: обработка заказов через Kafka

Рассмотрим типичную задачу: сервис оформления заказов (order-service) должен уведомлять другие сервисы — резервирование товаров (inventory-service), обработку платежей (payment-service) и аналитику (analytics-service) — без жёсткой связки. Цель — обеспечить асинхронность, отказоустойчивость и сохранность данных при сбоях.

Почему Kafka, а не RabbitMQ или REST?

Выбор Kafka обусловлен требованиями:

  • Сохранность сообщений — Kafka хранит сообщения на диске, обеспечивая durability;
  • Повторное чтение — потребители могут перечитывать историю (например, для восстановления после бага);
  • Высокая пропускная способность — тысячи сообщений в секунду на один партицион;
  • Гарантии порядка в партиции — критично для событий одного заказа.

RabbitMQ подошёл бы для простых fire-and-forget задач, но не для событийной архитектуры. REST — для синхронных, а не асинхронных взаимодействий.


Архитектура системы

[order-service]

▼ (публикация)
[Kafka: topic "orders"]

├───▶ [inventory-service] (группа потребителей "inventory-group")
├───▶ [payment-service] (группа "payment-group")
└───▶ [analytics-service] (группа "analytics-group")

Ключевые решения:

  • Один топик orders, а не отдельные топики на сервис — упрощает аудит и мониторинг;
  • Разные consumer group’ы — каждый сервис читает все сообщения независимо (fan-out);
  • События в формате Avro + Schema Registry — строгая типизация, совместимость версий.

Шаг 1: Производитель (order-service)

Сервис создаёт заказ и публикует событие только после коммита транзакции с БД. Это исключает рассогласование: сообщение не уйдёт, если заказ не сохранился.

Проблема: Dual Write

Наивный подход:

@Transactional
public Order createOrder(OrderRequest request) {
Order order = orderRepository.save(request.toOrder()); // 1. Запись в БД
kafkaTemplate.send("orders", new OrderCreated(order.id(), order.items())); // 2. Отправка в Kafka
return order;
}

Риск: если падение после шага 1, но до шага 2 — заказ есть в БД, но сообщения нет. Другие сервисы не узнают о нём.

Решение: Transactional Outbox Pattern

  1. В той же транзакции записываем событие в специальную таблицу outbox:
    INSERT INTO orders (id, items) VALUES (1001, '[...]');
    INSERT INTO outbox (event_id, aggregate_id, event_type, payload, created_at)
    VALUES (uuid(), 1001, 'OrderCreated', '{"orderId":1001,...}', now());
  2. Отдельный реляционный триггер или асинхронный воркер читает outbox и отправляет события в Kafka.

Spring предоставляет Spring Integration и Spring for Apache Kafka для реализации воркера:

@Component
public class OutboxPoller {
private final JdbcTemplate jdbcTemplate;
private final KafkaTemplate<String, String> kafkaTemplate;

@Scheduled(fixedDelay = 100) // опрос каждые 100 мс
@Transactional
public void pollAndPublish() {
List<OutboxEvent> events = jdbcTemplate.query(
"SELECT * FROM outbox WHERE published = false ORDER BY created_at LIMIT 100 FOR UPDATE SKIP LOCKED",
new OutboxRowMapper()
);

events.forEach(event -> {
try {
kafkaTemplate.send("orders", event.getPayload());
jdbcTemplate.update("UPDATE outbox SET published = true WHERE id = ?", event.getId());
} catch (Exception e) {
// логгирование, но НЕ rollback транзакции — иначе циклическая ошибка
log.error("Failed to publish event {}", event.getId(), e);
}
});
}
}

Ключевые моменты:

  • FOR UPDATE SKIP LOCKED — позволяет нескольким инстансам параллельно обрабатывать outbox без блокировок;
  • published = true проставляется только после успешной отправки;
  • Исключение в kafkaTemplate.send() не прерывает транзакцию — событие останется в outbox и будет повторно обработано.

Шаг 2: Потребитель (inventory-service)

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

Проблема: At-Least-Once Delivery

Kafka гарантирует доставку минимум один раз. При сбое потребителя между обработкой сообщения и коммитом оффсета сообщение будет перечитано.

Решение: Idempotency Key

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

{
"eventId": "a1b2c3d4",
"eventType": "OrderCreated",
"payload": { "orderId": 1001, "items": [...] }
}

Потребитель ведёт журнал обработанных eventId:

@Service
public class InventoryEventHandler {
private final JdbcTemplate jdbcTemplate;
private final InventoryService inventoryService;

@KafkaListener(topics = "orders", groupId = "inventory-group")
public void handleOrderEvent(OrderEvent event) {
// 1. Проверка идемпотентности
if (isEventProcessed(event.getEventId())) {
return; // уже обработано — игнорировать
}

// 2. Бизнес-логика
try {
inventoryService.reserveItems(event.getPayload().getItems());
// 3. Фиксация обработки в той же транзакции
markEventAsProcessed(event.getEventId());
} catch (InsufficientStockException e) {
// компенсирующее действие: публикация события неудачи
kafkaTemplate.send("orders-failed", new OrderFailed(event.getOrderId(), "INSUFFICIENT_STOCK"));
}
}

private boolean isEventProcessed(String eventId) {
return jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM processed_events WHERE event_id = ?",
Integer.class, eventId
) > 0;
}

private void markEventAsProcessed(String eventId) {
jdbcTemplate.update(
"INSERT INTO processed_events (event_id, processed_at) VALUES (?, now())",
eventId
);
}
}

Обработка ошибок: Dead Letter Topic (DLT)

Если ошибка не поддаётся обработке (например, невалидный формат сообщения), сообщение отправляется в специальный топик orders.DLT для ручного анализа:

@Bean
public DefaultErrorHandler errorHandler(KafkaTemplate<String, String> template) {
var recoverer = new DeadLetterPublishingRecoverer(template);
var errorHandler = new DefaultErrorHandler(recoverer, new FixedBackOff(1000L, 3)); // 3 попытки с задержкой 1с
errorHandler.addNotRetryableExceptions(DeserializationException.class); // не повторять при ошибках парсинга
return errorHandler;
}

Шаг 3: Наблюдаемость и отладка

Без мониторинга асинхронная система превращается в «чёрный ящик».

Распределённая трассировка (Distributed Tracing)

Spring Cloud Sleuth интегрируется с Kafka:

  • При публикации сообщения в заголовки Kafka (tracing_header) добавляется traceId и spanId;
  • Потребитель извлекает их и продолжает трассировку.

Результат — сквозной трейс в Jaeger или Zipkin:

[order-service] ──(traceId=abc)──▶ Kafka

[inventory-service] ──(traceId=abc)──▶ БД

[payment-service] ──(traceId=abc)──▶ Внешний API

Мониторинг отставания (Lag)

Критический метрик — consumer.lag: сколько сообщений непрочитано в партиции.
Spring Boot Actuator + Micrometer экспортирует:

  • kafka_consumer_fetch_manager_records_lag — лаг по топику/группе;
  • kafka_producer_record_send_total — количество отправленных сообщений.

Алерт при lag > 1000 — признак перегрузки потребителя.

Отладка через kafka-console-consumer

Для диагностики:

# Чтение всех сообщений в топике
kafka-console-consumer --bootstrap-server localhost:9092 \
--topic orders \
--from-beginning \
--property print.key=true \
--property key.separator=:

Позволяет увидеть «сырые» события, проверить порядок, найти дубликаты.


Гарантии и компромиссы

ГарантияКак достигаетсяКомпромисс
Durabilityacks=all, репликация = 3Задержка записи ~10–50 мс
OrderingВсе события заказа — в одной партиции по orderIdМасштабируемость по заказам
At-Least-Onceenable.auto.commit=false, ручной коммит после обработкиСложность идемпотентности
Exactly-Once (идемпотентность)Журнал processed_events + уникальный eventIdДополнительные запросы в БД
AvailabilityMulti-AZ Kafka cluster, consumer group rebalanceСложность настройки кластера

Exactly-Once Semantics (EOS) на уровне Kafka (через idempotent producer и транзакции) дополняет, но не заменяет идемпотентность на уровне приложения — сбой может произойти после коммита в Kafka, но до применения бизнес-логики.