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:
-
Внедрение через конструктор — наиболее рекомендуемый. Гарантирует, что объект создаётся в полностью инициализированном состоянии. Неизменяемые зависимости.
@Service
public class OrderService {
private final PaymentService paymentService;
public OrderService(PaymentService paymentService) {
this.paymentService = paymentService;
}
} -
Внедрение через setter-метод — позволяет изменять зависимости после создания объекта (редко нужно).
@Service
public class OrderService {
private PaymentService paymentService;
@Autowired
public void setPaymentService(PaymentService paymentService) {
this.paymentService = paymentService;
}
} -
Внедрение через поле — самый компактный, но нарушает инкапсуляцию и затрудняет 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:
- Читает конфигурацию (XML,
@Configuration,@Component-классы). - Создаёт метамодель — описание всех бинов, их зависимостей, настроек.
- Выполняет разрешение зависимостей (dependency resolution) — строит граф объектов.
- Создаёт бины в правильном порядке (уважая зависимости), применяет пост-процессоры (
BeanPostProcessor), внедряет зависимости. - Вызывает методы инициализации.
- Складывает готовые бины в
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. Это фронт-контроллер, централизующий обработку. Его работа — координировать взаимодействие компонентов:
- Получение запроса от контейнера (Tomcat, Jetty).
- Определение обработчика (handler) через
HandlerMapping. По умолчанию используетсяRequestMappingHandlerMapping, который сопоставляет URL/HTTP-метод с методом класса, помеченного@Controllerили@RestController. - Подготовка аргументов вызова метода через
HandlerAdapter(обычноRequestMappingHandlerAdapter). Здесь происходит:- привязка параметров запроса (
@RequestParam,@PathVariable); - десериализация тела (
@RequestBody); - валидация (
@Valid); - внедрение специальных объектов (
HttpServletRequest,Model).
- привязка параметров запроса (
- Вызов метода контроллера.
- Обработка возвращаемого значения:
- строка → имя представления (для
@Controller); - объект → сериализация в JSON/XML (для
@RestController); ResponseEntity→ полный контроль над HTTP-ответом.
- строка → имя представления (для
- Рендеринг 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 = ?; - Параметры:
StartingWith→LIKE ?%,Between→BETWEEN ? 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, что воспринимается как его часть. Он решает две задачи:
- Аутентификация — «Кто вы?» (проверка учётных данных: логин/пароль, JWT, OAuth2-токен).
- Авторизация — «Что вы можете?» (проверка прав доступа к ресурсу: 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():
- Открывается транзакция (через
PlatformTransactionManager); - Выполняется метод;
- При успехе —
commit(); - При исключении —
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 — это готовый автомобиль с предустановленными опциями. Его три кита:
- Auto-configuration — анализ classpath и автоматическое создание бинов. Наличие
spring-boot-starter-data-jpaиHikariCP→ Spring Boot создаётDataSource,EntityManagerFactory,JpaTransactionManager. - Starter-зависимости — предопределённые наборы библиотек для конкретных задач:
spring-boot-starter-web— веб (Tomcat, Spring MVC, Jackson);spring-boot-starter-data-jpa— JPA + Hibernate + транзакции;spring-boot-starter-security— Spring Security.
- Встроенные серверы — приложение — это executable JAR с Tomcat/Jetty/Undertow внутри. Не требует развёртывания в application server.
Механизм автонастройки
Каждая автонастройка — это @Configuration-класс с аннотацией @Conditional*. Например, DataSourceAutoConfiguration активируется только если:
- В classpath есть
DataSource; - Пользователь не предоставил свой
DataSource(@ConditionalOnMissingBean); - Заданы свойства
spring.datasource.url.
Это позволяет гибко переопределять поведение: достаточно объявить свой DataSource — и автонастройка отключится.
Внешнее конфигурирование
Spring Boot поддерживает 17+ источников конфигурации (в порядке приоритета):
- Параметры командной строки (
--server.port=8081); - Переменные окружения (
SERVER_PORT=8081); application.properties/application.ymlв classpath;- Профиль-специфичные файлы (
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 решает это:
- Создаётся отдельный сервис (
config-server), который читает конфигурации из Git-репозитория (application.yml,user-service-prod.yml); - Клиентские сервисы при старте запрашивают у Config Server свои настройки;
- Изменения в 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 объект с:
principal—subиз токена;authorities— преобразованныеscopeвGrantedAuthority(например,SCOPE_read:users→read: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)
-
Инстантация (Instantiation)
Spring вызывает конструктор бина (через рефлексию илиSupplier). На этом этапе бин — «сырой» объект, без внедрённых зависимостей. -
Заполнение свойств (Population of properties)
ВыполняетсяAutowiredAnnotationBeanPostProcessor:- разрешаются зависимости (
@Autowired,@Value,@Resource); - внедряются значения из
Environment(${db.url}); - применяются
PropertyEditorилиConverter.
- разрешаются зависимости (
-
Пост-обработка до инициализации (Post-processing before initialization)
Вызываются всеBeanPostProcessor.postProcessBeforeInitialization().
Именно здесь работают:@PostConstruct(черезInitDestroyAnnotationBeanPostProcessor);InitializingBean.afterPropertiesSet()(устаревший, но поддерживаемый);- кастомные аннотации (например,
@ValidatedчерезMethodValidationPostProcessor).
-
Инициализация (Initialization)
Выполняется метод, указанный вinit-method(XML) или@Bean(initMethod = "..."). -
Пост-обработка после инициализации (Post-processing after initialization)
ВызываютсяBeanPostProcessor.postProcessAfterInitialization().
Здесь создаются:- AOP-прокси (
AnnotationAwareAspectJAutoProxyCreatorоборачивает бин, если он целевой для аспекта); @Transactional-прокси (BeanFactoryTransactionAttributeSourceAdvisor).
- AOP-прокси (
-
Использование
Бин готов к внедрению в другие компоненты или к прямому вызову черезApplicationContext. -
Уничтожение (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 или поле (не через конструктор). Механизм:
- Создаётся «сырой» экземпляр A (без зависимостей);
- Помещается в «early singleton cache»;
- При создании B, который зависит от A, Spring извлекает неполностью инициализированный A из early-cache и внедряет его;
- Завершается инициализация 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
-
Открытие пакетов для рефлексии
Spring читает аннотации и создаёт бины через рефлексию. Пакеты с компонентами должны бытьopen:open module com.example.app {
requires spring.context;
requires spring.beans;
// ...
}open— разрешает deep reflection для всего модуля. Альтернатива —opens com.example.service to spring.core, но это многословно. -
Экспорт API-пакетов
Если модуль предоставляет интерфейсы для других, их пакеты должны бытьexported:exports com.example.api; -
Обработка автоматических модулей
Большинство библиотек (включая 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 добавляют поддержку:
-
Конфигурация времени сборки (
@Configuration(proxyBeanMethods = false))
Упрощает анализ: Spring не создаёт CGLIB-прокси для@Configuration-классов. -
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 { } -
Ограничения
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 — это последовательность локальных транзакций, где каждая сопровождается компенсирующей операцией на случай сбоя.
Пример оформления заказа:
ReserveInventory→ если успех, идти дальше; иначе — стоп.ProcessPayment→ если успех, идти дальше; иначе — вызватьCancelInventoryReservation.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
- В той же транзакции записываем событие в специальную таблицу
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()); - Отдельный реляционный триггер или асинхронный воркер читает
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=:
Позволяет увидеть «сырые» события, проверить порядок, найти дубликаты.
Гарантии и компромиссы
| Гарантия | Как достигается | Компромисс |
|---|---|---|
| Durability | acks=all, репликация = 3 | Задержка записи ~10–50 мс |
| Ordering | Все события заказа — в одной партиции по orderId | Масштабируемость по заказам |
| At-Least-Once | enable.auto.commit=false, ручной коммит после обработки | Сложность идемпотентности |
| Exactly-Once (идемпотентность) | Журнал processed_events + уникальный eventId | Дополнительные запросы в БД |
| Availability | Multi-AZ Kafka cluster, consumer group rebalance | Сложность настройки кластера |
Exactly-Once Semantics (EOS) на уровне Kafka (через idempotent producer и транзакции) дополняет, но не заменяет идемпотентность на уровне приложения — сбой может произойти после коммита в Kafka, но до применения бизнес-логики.