В этой статье:
(нажмите для перехода)
➤ Что такое Saga Pattern
➤ Что такое Rollback Pattern
➤ В чем отличие между Rollback Pattern и Saga Pattern
➤ Что такое Концепции потоковой передачи событий (Event Streaming)
➤ Что такое Eventual Consistency (конечная согласованность)
➤ Что такое SOLID
➤ Как использовать подход Чистой архитектуры (Clean Architecture) в Spring
➤ Какие параметры у RESTful сервиса
➤ Какие существуют REST методы
➤ Что такое Model, ModelAndView и ViewResolver
➤ Что такое принципы Inversion of Control (IoC) и Dependency Injection (DI)
➤ Что такое Domain-Driven Design (DDD)
➤ Что такое CQRS (Command Query Responsibility Segregation)
➤ Что такое Event Sourcing
➤ Что такое Circuit Breaker (Переключатель цепи)
➤ Что такое JSON-RPC (Remote Procedure Call)
➤ Что такое Spring MVC
➤ Какие могут быть слои в Spring
➤ Как обычно структурируется Spring приложение
➤ Что лучше, аннотации или XML в Spring
➤ В чем разница между REST, RPC, GraphQL и SOAP
➤ Какие части могут быть в Spring приложении
➤ Чем Spring отличается от Java EE
➤ Что такое аспектно-ориентированное программирование (AOP)
➤ Как происходит обработка запроса и выдача ответа в Spring
➤ Что такое Project Reactor
➤ Что такое Mono и Flux
➤ Что такое сервлет (Servlet)
➤ Что такое Инфраструктура открытых ключей (Public Key Infrastructure, PKI)
➤ Что такое CSRF (Cross-Site Request Forgery)
➤ Что такое формат Prometheus
➤ Что такое Структурированная конкуренция (Structured concurrency)
➤ Что такое Принцип Happens-Before (событийное предшествование)
➤ Чем WAR (Web Application Archive) отличается от JAR (Java Archive)
➤ Какой жизненный цикл у бинов в Spring
➤ Что такое Git Registry, GitHub Packages, GitHub Actions, Changelog, Release Notes
➤ Какие основные протоколы обмена данными в Spring
➤ Что такое Protobuf
➤ Что такое Javadoc
➤ Какие основные отличия Maven от Gradle
➤ Из чего состоит pom.xml
➤ Из чего состоит build.gradle
➤ Что такое Supplier
➤ В чем отличие record в Java и data class в Kotlin
➤ Что такое Java JWT (Java JSON Web Token)
➤ Как использовать Lombok в Java
➤ Что такое Swagger / OpenAPI
➤ Как настроить доступ через OpenVPN
➤ Что такое Keycloak
➤ Что такое Testcontainers
➤ Чем отличаются Jetty, Netty и Tomcat
➤ Что такое Netty
➤ Что такое Nginx
➤ Пример простого веб сервера
➤ Что такое Saga Pattern
Saga Pattern — это шаблон проектирования, используемый для управления распределенными транзакциями в микросервисных архитектурах. Saga — это последовательность транзакций, каждая из которых обновляет данные в одном сервисе и публикует событие или сообщение. Если какая-либо транзакция в последовательности неудачна, Saga вызывает серию компенсационных транзакций для отмены изменений, внесенных ранее.
Ключевые особенности Saga Pattern:
Распределенная транзакция:
В отличие от традиционных транзакций, которые контролируются с помощью системы управления базами данных (DBMS) и поддерживают свойства ACID, распределенные транзакции Saga включают несколько шагов, каждый из которых может вызывать разные сервисы.
Компенсационные транзакции:
Для каждого шага, который может изменить состояние системы, существует соответствующая компенсационная транзакция, которая может отменить его эффекты. Это необходимо, если последующие шаги в Saga не могут быть завершены успешно.
Управление последовательностью:
Saga может быть реализована в двух вариантах — последовательном и параллельном. Последовательное исполнение требует, чтобы каждый шаг был завершен, прежде чем начался следующий. Параллельное исполнение позволяет выполнять некоторые шаги одновременно, но все же требует, чтобы некоторые операции зависели от результатов предыдущих.
Примеры использования Saga Pattern:
Обработка заказов:
При обработке заказа могут быть задействованы различные сервисы — сервис заказов, сервис платежей и сервис доставки. Если платеж не проходит, необходимо отменить заказ и, возможно, освободить зарезервированный товар.
Бронирование путешествий:
При бронировании полета, гостиницы и аренды автомобиля как части одного путешествия, каждое бронирование представляет собой отдельный шаг Saga. Если один из шагов не удается (например, невозможно забронировать автомобиль), другие успешные бронирования должны быть отменены, чтобы избежать ненужных расходов для пользователя.
Виды Saga:
Оркестраторная Saga (Orchestrated Saga):
Центральный оркестратор управляет выполнением и координацией каждой транзакции и компенсационной транзакции. В подходе оркестрации центральный координатор (называемый оркестратором) управляет логикой Saga, указывая каждому сервису, какие действия выполнять. Оркестратор отвечает за обработку ответов от сервисов и принятие решения о следующих шагах, включая инициацию компенсационных действий в случае сбоев.
Преимущества:
Легче управлять и мониторить выполнение Saga, так как вся логика управления сосредоточена в одном месте.
Проще обеспечить согласованность и восстановление после ошибок.
Недостатки:
Оркестратор становится точкой отказа, что потенциально уменьшает отказоустойчивость системы.
Может возникнуть “бутылочное горлышко” при увеличении масштаба системы из-за централизации управления.
Хореографическая Saga (Choreographed Saga):
Каждая служба в SAGA прослушивает события и выполняет свою работу автономно, публикуя события для других служб. В этом подходе нет централизованного управляющего компонента. Вместо этого каждый сервис, участвующий в Saga, знает, какие шаги нужно предпринять дальше и когда инициировать следующий шаг или компенсационное действие. Сервисы общаются друг с другом, отправляя события, которые могут быть пойманы другими сервисами, что приводит к выполнению следующих шагов.
Преимущества:
Меньше зависимости от одного управляющего компонента, что увеличивает отказоустойчивость системы.
Более гибкая и масштабируемая система, так как новые сервисы могут быть добавлены с минимальными изменениями в других сервисах.
Недостатки:
Может быть труднее отслеживать ход выполнения Saga, так как нет централизованной точки управления.
Сложнее обеспечить согласованность данных из-за асинхронной природы коммуникаций.
Схема работы Saga Pattern:
Клиент инициирует создание заказа.
Order Service создает заказ и публикует событие OrderCreatedEvent.
Payment Service обрабатывает событие OrderCreatedEvent и пытается выполнить платеж.
Если платеж успешен, Payment Service публикует событие PaymentCompletedEvent.
Если платеж неудачен, Payment Service публикует событие PaymentFailedEvent.
Inventory Service обрабатывает событие OrderCreatedEvent и резервирует товары.
Если резервирование успешно, Inventory Service публикует событие InventoryReservedEvent.
Если резервирование неудачно, Inventory Service публикует событие InventoryReservationFailedEvent.
Order Service обрабатывает события PaymentCompletedEvent, PaymentFailedEvent, InventoryReservedEvent, InventoryReservationFailedEvent и завершает или отменяет заказ.
import org.springframework.stereotype.Service @Service class Service1 { fun performTransaction1() { // Логика для T1 } fun compensateTransaction1() { // Логика компенсации для T1 } }
import org.springframework.stereotype.Service @Service class SagaOrchestrator( private val service1: Service1, private val service2: Service2, private val service3: Service3, private val service4: Service4 ) { fun executeSaga() { try { service1.performTransaction1() service2.performTransaction2() service3.performTransaction3() service4.performTransaction4() } catch (e: Exception) { service4.compensateTransaction4() service3.compensateTransaction3() service2.compensateTransaction2() service1.compensateTransaction1() } } }
➤ Что такое Rollback Pattern
Rollback Pattern (Шаблон отката) — это подход к обработке отказов в распределенных системах, при котором, если одна из операций в последовательности транзакций не удается, все выполненные операции откатываются, возвращая систему в исходное состояние. В отличие от Saga Pattern, где используются компенсирующие транзакции, в Rollback Pattern изменения отменяются с помощью явного отката.
Принципы работы Rollback Pattern:
Компенсационные действия:
Для каждой операции, которая изменяет состояние системы, определяется компенсационное действие. Это действие должно отменить изменения, внесенные исходной операцией.
Идентификация неудач:
Система должна уметь обнаруживать, когда операция не удалась, и инициировать компенсационные действия.
Изолированные операции:
Операции должны быть независимыми и изолированными, чтобы компенсационные действия не повлияли на другие операции.
Транзакционность:
Применяется в системах, где необходимо обеспечить атомарность операций. Это значит, что все операции в рамках одной транзакции либо выполняются полностью, либо не выполняются вовсе.
Журналирование состояний:
Часто требует, чтобы система вела журнал изменений или состояний до начала транзакции, чтобы можно было вернуться к предыдущему состоянию.
Схема работы Rollback Pattern:
Клиент инициирует создание заказа.
Order Service создает заказ и публикует событие OrderCreatedEvent.
Payment Service обрабатывает событие OrderCreatedEvent и пытается выполнить платеж.
Если платеж успешен, Payment Service публикует событие PaymentCompletedEvent.
Если платеж неудачен, Payment Service публикует событие PaymentFailedEvent.
Order Service обрабатывает событие PaymentFailedEvent и отменяет заказ, публикуя событие OrderCancelledEvent.
import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @Service class Service1 { @Transactional fun performTransaction1() { // Логика для T1 } @Transactional fun rollbackTransaction1() { // Логика отката для T1 } } // Аналогично для Service3 и Service4
import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @Service class TransactionManager( private val service1: Service1, private val service2: Service2, private val service3: Service3, private val service4: Service4 ) { @Transactional fun executeTransactions() { try { service1.performTransaction1() service2.performTransaction2() service3.performTransaction3() service4.performTransaction4() } catch (e: Exception) { service4.rollbackTransaction4() service3.rollbackTransaction3() service2.rollbackTransaction2() service1.rollbackTransaction1() throw e } } }
➤ В чем отличие между Rollback Pattern и Saga Pattern
Saga Pattern:
Компенсирующие транзакции:
В случае ошибки выполняются компенсирующие транзакции, чтобы отменить эффекты уже выполненных операций.
Асинхронность:
Обычно используется в асинхронных распределенных системах, например, в микросервисах.
Продолжение работы:
Частичные сбои могут быть обработаны, и система продолжает работу.
Rollback Pattern:
Полный откат:
В случае ошибки выполняется полный откат всех выполненных операций, возвращая систему в исходное состояние.
Синхронность:
Чаще используется в синхронных системах и требует глобальной транзакции.
Строгая согласованность:
Обеспечивает строгую согласованность данных, но может быть менее гибким и менее производительным.
Область применения:
Rollback Pattern обычно используется в контексте одиночной базы данных или в рамках одного сервиса, где можно откатить все изменения до начального состояния, если операция не удалась.
Saga Pattern применяется в распределенных системах и микросервисных архитектурах, где транзакции затрагивают несколько сервисов, и откат должен выполняться через серию компенсационных операций между разными сервисами.
Управление состоянием:
В Rollback Pattern состояние управляется локально в рамках одной системы или компонента, и обычно есть возможность использовать механизмы управления транзакциями (например, в СУБД) для возврата к исходному состоянию.
Saga Pattern требует координации между множеством сервисов, каждый из которых управляет своим состоянием. Откат в Saga организуется через компенсационные транзакции, которые должны быть явно определены и реализованы для каждого шага в процессе.
Сложность и управление:
Rollback Pattern является относительно простым в реализации в рамках одной системы или приложения, где поддерживаются атомарные транзакции.
Saga Pattern требует более сложной логики координации и обработки ошибок, так как каждый шаг Saga может потребовать выполнения компенсационных действий в других сервисах.
Отказоустойчивость и масштабируемость:
Rollback Pattern может быть ограничен в рамках одной системы, что уменьшает его применимость в масштабируемых распределенных системах.
Saga Pattern разработан специально для распределенных и масштабируемых систем, предоставляя возможности для управления сложными бизнес-процессами и обеспечивая отказоустойчивость на уровне архитектуры.
➤ Что такое Концепции потоковой передачи событий (Event Streaming)
Концепции потоковой передачи событий (Event Streaming) являются основополагающими в построении современных распределенных систем и микросервисных архитектур. Эти концепции позволяют приложениям передавать, получать и обрабатывать потоки событий в реальном времени. Вот основные концепции и компоненты потоковой передачи событий:
Основные концепции потоковой передачи событий:
Событие (Event):
запись значимого изменения состояния или действия, которое произошло в системе. Например, добавление нового пользователя, обновление заказа или завершение транзакции.
Событие содержит информацию о произошедшем, такую как тип события, временную метку и данные, связанные с этим событием.
Поток событий (Event Stream):
представляет собой непрерывную последовательность событий, поступающих в реальном времени.
Поток может быть упорядочен по временным меткам или другим критериям.
Производитель (Producer):
компонент или сервис, который генерирует события и отправляет их в поток.
Примером может быть сервис, который регистрирует события при добавлении новых пользователей.
Потребитель (Consumer):
компонент или сервис, который получает события из потока и обрабатывает их.
Примером может быть аналитическая система, которая обрабатывает и анализирует события из потока.
Топик (Topic):
логическая группа событий в системе потоковой передачи событий.
Производители отправляют события в топик, а потребители подписываются на топик для получения событий.
Брокер сообщений (Message Broker):
система, которая управляет топиками и маршрутизацией событий от производителей к потребителям.
Примеры брокеров сообщений: Apache Kafka, RabbitMQ, Amazon Kinesis.
Обработка событий (Event Processing):
включает в себя фильтрацию, преобразование, агрегацию и другие операции над событиями в реальном времени.
Часто используется для создания потоковых аналитических приложений.
Примеры использования потоковой передачи событий
Мониторинг и оповещения:
Потоковая передача событий используется для мониторинга систем в реальном времени и отправки оповещений при возникновении аномалий или событий.
Аналитика в реальном времени:
Обработка и анализ потоков событий для получения аналитических данных в реальном времени. Примеры: обработка транзакций, отслеживание кликов на сайте.
Микросервисные архитектуры:
Микросервисы могут использовать потоковую передачу событий для асинхронного общения и координации. Это помогает уменьшить связанность между микросервисами и улучшить масштабируемость системы.
➤ Что такое Eventual Consistency (конечная согласованность)
Eventual Consistency (конечная согласованность) — это модель согласованности данных, применяемая в распределенных системах и базах данных. Эта модель гарантирует, что если не будут происходить новые обновления данных, то все копии данных в системе в конечном итоге станут согласованными. Иными словами, все изменения данных в конечном итоге будут реплицированы во все узлы системы.
Основные аспекты конечной согласованности:
Асинхронное обновление данных:
В распределенной системе обновления данных могут происходить асинхронно. Это означает, что после внесения изменений данные не будут сразу согласованы между всеми узлами системы.
Конечное согласование:
Если новые обновления не будут происходить, система гарантирует, что все узлы в конечном итоге придут к одному и тому же состоянию.
Допустимость временной несогласованности:
Временная несогласованность данных возможна и допустима. Система может временно показывать старые или неконсистентные данные до тех пор, пока все обновления не будут реплицированы и применены на всех узлах.
Применимость:
Конечная согласованность часто используется в распределенных системах, таких как распределенные базы данных, системы кэширования и системы репликации, где важно поддерживать высокую доступность и производительность.
Примеры систем с конечной согласованностью
NoSQL базы данных:
Многие NoSQL базы данных, такие как Cassandra, DynamoDB, Couchbase, MongoDB (в режиме репликации), используют модель конечной согласованности для достижения высокой доступности и масштабируемости.
Системы кэширования:
Системы кэширования, такие как Redis (в режиме кластера), могут использовать конечную согласованность для обеспечения высокой производительности при доступе к данным.
Системы обмена сообщениями:
Системы обмена сообщениями, такие как Apache Kafka, также могут использовать модель конечной согласованности для обеспечения высокой производительности и доступности.
➤ Что такое SOLID
SOLID — это аббревиатура, обозначающая пять принципов объектно-ориентированного программирования и дизайна, которые помогают создавать гибкие, масштабируемые и легко поддерживаемые программные системы. Вот краткое описание каждого принципа и примеры их применения в контексте Spring:
Single Responsibility Principle (Принцип единственной ответственности):
Класс должен иметь только одну причину для изменения, то есть выполнять только одну задачу.
@Service public class UserService { @Autowired private UserRepository userRepository; public User findUserById(Long id) { return userRepository.findById(id).orElse(null); } public void saveUser(User user) { userRepository.save(user); } } @Service public class EmailService { public void sendWelcomeEmail(User user) { // Логика отправки письма } }
Здесь UserService отвечает только за операции с пользователем, а EmailService — за отправку писем.
Open/Closed Principle (Принцип открытости/закрытости):
Классы должны быть открыты для расширения, но закрыты для модификации.
public interface DiscountPolicy { double applyDiscount(double price); } @Service public class RegularDiscountPolicy implements DiscountPolicy { @Override public double applyDiscount(double price) { return price * 0.9; // 10% скидка } } @Service public class SpecialDiscountPolicy implements DiscountPolicy { @Override public double applyDiscount(double price) { return price * 0.8; // 20% скидка } }
Классы RegularDiscountPolicy и SpecialDiscountPolicy могут расширять поведение без изменения существующего кода.
Liskov Substitution Principle (Принцип подстановки Барбары Лисков):
Объекты подклассов должны заменять объекты базового класса без изменения правильности программы.
public interface NotificationSender { void sendNotification(String message); } @Service public class EmailNotificationSender implements NotificationSender { @Override public void sendNotification(String message) { // Логика отправки email } } @Service public class SmsNotificationSender implements NotificationSender { @Override public void sendNotification(String message) { // Логика отправки SMS } }
Оба класса EmailNotificationSender и SmsNotificationSender могут использоваться в контексте интерфейса NotificationSender.
Interface Segregation Principle (Принцип разделения интерфейса):
Клиенты не должны зависеть от интерфейсов, которые они не используют.
public interface UserRepository { void save(User user); User findById(Long id); } public interface UserDeletionRepository { void delete(User user); } @Service public class UserServiceImpl implements UserRepository, UserDeletionRepository { @Override public void save(User user) { // Логика сохранения пользователя } @Override public User findById(Long id) { // Логика поиска пользователя return null; } @Override public void delete(User user) { // Логика удаления пользователя } }
Здесь интерфейсы разделены, так что каждый из них представляет собой отдельный набор функциональностей.
Dependency Inversion Principle (Принцип инверсии зависимостей):
Модули верхнего уровня не должны зависеть от модулей нижнего уровня; оба должны зависеть от абстракций.
public interface MessageSender { void sendMessage(String message); } @Service public class SmsMessageSender implements MessageSender { @Override public void sendMessage(String message) { // Логика отправки SMS } } @Service public class NotificationService { private final MessageSender messageSender; @Autowired public NotificationService(MessageSender messageSender) { this.messageSender = messageSender; } public void sendNotification(String message) { messageSender.sendMessage(message); } }
Здесь NotificationService зависит от абстракции MessageSender, а не от конкретной реализации, что позволяет легко заменять реализацию при необходимости.
➤ Как использовать подход Чистой архитектуры (Clean Architecture) в Spring
Чистая архитектура (Clean Architecture) — это концепция разработки программного обеспечения, предложенная Робертом С. Мартином (Uncle Bob). Она разделяет систему на различные слои с четко определенными обязанностями и зависимостями, что позволяет создать гибкую, тестируемую и легко поддерживаемую систему.
Основные принципы чистой архитектуры:
Зависимости направлены внутрь:
Внешние слои могут зависеть от внутренних, но не наоборот.
Внешний слой:
Включает пользовательский интерфейс и инфраструктуру.
Внутренний слой:
Включает бизнес-логику и сущности.
Входные и выходные порты:
Определяют интерфейсы для взаимодействия между слоями.
Структура проекта:
Пример структуры проекта с использованием чистой архитектуры в Spring Boot:
src/main/java/com/example/demo ├── DemoApplication.java ├── config │ └── WebConfig.java ├── controller │ └── UserController.java ├── entity │ └── User.java ├── repository │ └── UserRepository.java ├── service │ └── UserService.java ├── usecase │ └── GetUserUseCase.java └── web ├── UserRequest.java └── UserResponse.java
➤ Какие параметры у RESTful сервиса
При разработке RESTful веб-сервиса необходимо учитывать несколько ключевых параметров и аспектов, чтобы обеспечить соответствие принципам REST и добиться максимальной эффективности, удобства использования и масштабируемости. Вот основные параметры, которые следует учитывать:
Ресурсы (Resources):
URL (Uniform Resource Locator): Ресурсы идентифицируются уникальными URL-адресами. Например, /users для пользователей или /products для продуктов.
Именование: Pесурсы обычно именуются в множественном числе (например, /users, /orders).
HTTP методы (HTTP Methods):
GET: Для получения ресурса или списка ресурсов.
POST: Для создания нового ресурса.
PUT: Для обновления существующего ресурса.
DELETE: Для удаления ресурса.
PATCH: Для частичного обновления ресурса.
Статусы HTTP (HTTP Status Codes):
200 OK: Успешный запрос.
201 Created: Успешное создание ресурса.
204 No Content: Успешное выполнение запроса без возвращаемого контента.
400 Bad Request: Некорректный запрос.
401 Unauthorized: Требуется аутентификация.
403 Forbidden: Доступ запрещен.
404 Not Found: Ресурс не найден.
500 Internal Server Error: Внутренняя ошибка сервера.
Заголовки HTTP (HTTP Headers):
Content-Type: Указывает тип контента (например, application/json).
Accept: Указывает, какой формат контента ожидает клиент.
Authorization: Используется для передачи информации об аутентификации.
Форматы данных (Data Formats):
JSON (JavaScript Object Notation): Широко используемый формат для передачи данных.
XML (eXtensible Markup Language): Используется в некоторых системах для передачи данных.
Обработка ошибок (Error Handling):
Сообщения об ошибках должны быть информативными и предоставлять достаточно информации для понимания проблемы.
Структурированные сообщения об ошибках в формате JSON или XML.
Кэширование (Caching):
Использование заголовков кэширования (Cache-Control, ETag, Last-Modified) для улучшения производительности и снижения нагрузки на сервер.
Аутентификация и авторизация (Authentication and Authorization):
Basic Auth: Простой метод аутентификации с использованием имени пользователя и пароля.
Token-based Auth (например, JWT): Более безопасный и гибкий метод аутентификации.
OAuth: Протокол для авторизации, позволяющий сторонним приложениям получать ограниченный доступ к ресурсам.
Версионирование API (API Versioning):
Включение версии в URL (например, /v1/users).
Использование заголовков для указания версии API.
Документация (Documentation):
Документация должна быть полной и понятной, включая описание всех конечных точек, параметров, форматов данных, статусов ошибок и примеров запросов/ответов.
Инструменты для документации: Swagger/OpenAPI.
➤ Какие существуют REST методы
REST (Representational State Transfer) — это архитектурный стиль для взаимодействия с веб-сервисами, который использует стандартные HTTP-методы для выполнения операций над ресурсами. В REST основные методы HTTP используются для работы с ресурсами (например, данными) и определяют, какое действие должно быть выполнено.
Вот основные HTTP-методы, которые используются в REST API:
GET:
Назначение:
Получение данных (чтение ресурса).
Описание:
Этот метод используется для запроса данных с сервера. Запросы с методом GET не изменяют состояние сервера и считаются идемпотентными (т.е. повторный запрос не приведет к изменениям).
Пример использования:
GET /users
Возвращает список всех пользователей.
GET /users/1
Возвращает данные пользователя с идентификатором 1.
POST:
Назначение:
Создание нового ресурса.
Описание:
Метод POST используется для отправки данных на сервер с целью создания нового ресурса. Этот метод не является идемпотентным — при повторных запросах может создаваться новый ресурс.
Пример использования:
POST /users Content-Type: application/json { "name": "Alice", "email": "[email protected]" }
Создает нового пользователя с именем «Alice».
PUT:
Назначение:
Полное обновление ресурса.
Описание:
Метод PUT используется для замены существующего ресурса новыми данными. Если ресурс с указанным идентификатором существует, он будет обновлен, если нет — может быть создан (в зависимости от реализации). Метод является идемпотентным — повторное выполнение запроса не изменит результат.
Пример использования:
PUT /users/1 Content-Type: application/json { "name": "Bob", "email": "[email protected]" }
Обновляет данные пользователя с идентификатором 1, заменяя все поля новыми значениями.
PATCH:
Назначение:
Частичное обновление ресурса.
Описание:
Метод PATCH используется для частичного обновления ресурса. В отличие от PUT, который обновляет ресурс полностью, PATCH обновляет только те поля, которые были переданы в запросе. Этот метод также может быть идемпотентным, если реализован соответствующим образом.
Пример использования:
PATCH /users/1 Content-Type: application/json { "email": "[email protected]" }
Обновляет только поле email у пользователя с идентификатором 1.
DELETE:
Назначение:
Удаление ресурса.
Описание:
Метод DELETE используется для удаления ресурса с сервера. Если ресурс удален успешно, сервер должен вернуть статус успешного выполнения (например, 200 OK или 204 No Content). Этот метод также является идемпотентным — повторное выполнение запроса не изменит результат.
Пример использования:
DELETE /users/1
Удаляет пользователя с идентификатором 1.
OPTIONS:
Назначение:
Запрос разрешенных методов для ресурса.
Описание:
Метод OPTIONS используется для запроса доступных HTTP-методов для определенного ресурса или всего сервиса. Ответ содержит информацию о том, какие методы разрешены на сервере для этого ресурса. Часто используется для проверки поддержки CORS (Cross-Origin Resource Sharing).
Пример использования:
OPTIONS /users
Возвращает методы, которые могут быть выполнены над /users (например, GET, POST, и т.д.).
HEAD:
Назначение:
Получение заголовков ответа без тела.
Описание:
Метод HEAD работает так же, как и GET, за исключением того, что в ответе возвращаются только заголовки, а тело ответа отсутствует. Этот метод полезен для проверки, существует ли ресурс, или для получения метаданных, таких как размер или последний раз изменённые данные.
Пример использования:
HEAD /users/1
Возвращает заголовки для запроса пользователя с идентификатором 1
TRACE:
Назначение:
Диагностика сетевого пути.
Описание:
Этот метод используется для тестирования и отладки сетевого соединения. Сервер должен вернуть запрос обратно в том виде, в котором он был получен. Используется для диагностики маршрутов между клиентом и сервером.
Пример использования:
TRACE /users
CONNECT:
Назначение:
Установка туннеля для защищенной связи.
Описание:
Этот метод используется для создания туннеля через сервер, обычно для безопасного соединения с помощью SSL/TLS. На практике этот метод чаще всего применяется при реализации прокси-серверов.
Пример использования:
CONNECT server.example.com:443 HTTP/1.1
Идемпотентность и безопасность методов:
Идемпотентные методы:
Эти методы могут быть выполнены несколько раз с тем же результатом, как если бы они были выполнены один раз. К идемпотентным методам относятся:
GET
PUT
DELETE
HEAD
OPTIONS
Неидемпотентные методы:
Выполнение этих методов несколько раз может привести к различным результатам. Например, каждый раз при выполнении POST может создаваться новый ресурс.
POST
PATCH
Безопасные методы:
Безопасными считаются методы, которые не изменяют состояние ресурса на сервере. Это:
GET
HEAD
OPTIONS
TRACE
➤ Что такое Model, ModelAndView и ViewResolver
В Spring MVC (Model-View-Controller) архитектуре используются несколько ключевых компонентов для управления данными, представлениями и их взаимодействием. Среди них Model, ModelAndView и ViewResolver играют важную роль. Давайте рассмотрим каждую из этих концепций подробнее.
Model:
интерфейс, который используется для передачи данных из контроллера в представление. Он позволяет добавить атрибуты, которые будут доступны в шаблоне представления. Model обычно используется в методах контроллера. В этом примере метод greeting добавляет атрибут message в Model, который затем будет доступен в представлении с именем greeting.
import org.springframework.stereotype.Controller import org.springframework.ui.Model import org.springframework.web.bind.annotation.GetMapping @Controller class MyController { @GetMapping("/greeting") fun greeting(model: Model): String { model.addAttribute("message", "Hello, World!") return "greeting" } }
ModelAndView:
класс, который объединяет модель и представление. Он используется для передачи как данных, так и информации о представлении в одном объекте. Это полезно, когда необходимо вернуть и модель, и представление из контроллера. В этом примере метод welcome создает экземпляр ModelAndView, устанавливает имя представления welcome и добавляет атрибут message.
import org.springframework.stereotype.Controller import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.servlet.ModelAndView @Controller class MyController { @GetMapping("/welcome") fun welcome(): ModelAndView { val modelAndView = ModelAndView("welcome") modelAndView.addObject("message", "Welcome to Spring MVC!") return modelAndView } }
ViewResolver:
интерфейс, который определяет объект представления (view) на основе его имени. Он используется для маппинга логических имен представлений на конкретные представления. В зависимости от настроек, ViewResolver может искать представления в файловой системе, в класспате или использовать другие механизмы. Этот конфигурационный класс настраивает InternalResourceViewResolver, который ищет JSP-файлы в указанной директории.
spring.mvc.view.prefix=/WEB-INF/views/ spring.mvc.view.suffix=.jsp
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.web.servlet.config.annotation.WebMvcConfigurer import org.springframework.web.servlet.view.InternalResourceViewResolver @Configuration class WebConfig : WebMvcConfigurer { @Bean fun viewResolver(): InternalResourceViewResolver { val resolver = InternalResourceViewResolver() resolver.setPrefix("/WEB-INF/views/") resolver.setSuffix(".jsp") return resolver } }
➤ Что такое принципы Inversion of Control (IoC) и Dependency Injection (DI)
Inversion of Control (IoC):
принцип программирования, при котором управление объектами и их зависимостями передается внешнему контейнеру или фреймворку. Это означает, что вместо создания и управления зависимостями вручную, программист позволяет контейнеру управлять этими задачами. IoC помогает отделить различные части системы и уменьшить зависимость между ними, что делает систему более гибкой и легко тестируемой.
Принципы IoC:
Контейнер управляет жизненным циклом объектов: Контейнер отвечает за создание, инициализацию и уничтожение объектов.
Контейнер управляет зависимостями объектов: Контейнер инъектирует необходимые зависимости в объекты.
Dependency Injection (DI):
процесс, при котором контейнер Spring автоматически предоставляет зависимости (объекты, от которых зависит ваш класс) в объект, чтобы минимизировать жесткие зависимости и улучшить тестируемость и поддерживаемость кода.
Основные концепции и виды DI в Spring:
IoC Container (Inversion of Control Container):
Контейнер, управляющий жизненным циклом бинов (компонентов) и их зависимостями. В Spring основными контейнерами являются ApplicationContext и BeanFactory.
Бины (Beans):
Это объекты, которые управляются контейнером Spring. Бины создаются, связываются и управляются контейнером на основе конфигурации метаданных.
Аннотации:
Аннотации используются для указания контейнеру Spring, как внедрять зависимости. Основные аннотации включают @Component, @Service, @Repository, @Controller, @Autowired, @Qualifier и @Inject.
Виды Dependency Injection:
Constructor Injection (Конструкторное внедрение):
Зависимости передаются через конструктор класса.
Предпочтительный способ DI, так как делает зависимости явно видимыми и способствует неизменности объектов.
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class UserService { private final UserRepository userRepository; @Autowired public UserService(UserRepository userRepository) { this.userRepository = userRepository; } public User findUserById(Long id) { return userRepository.findById(id).orElse(null); } }
import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Service @Service class UserService @Autowired constructor( private val userRepository: UserRepository ) { fun findUserById(id: Long): User? { return userRepository.findById(id).orElse(null) } }
Setter Injection (Внедрение через сеттеры):
Зависимости передаются через сеттеры или другие методы класса.
Полезно для опциональных зависимостей и позволяет легко изменять зависимости после создания объекта.
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class UserService { private UserRepository userRepository; @Autowired public void setUserRepository(UserRepository userRepository) { this.userRepository = userRepository; } public User findUserById(Long id) { return userRepository.findById(id).orElse(null); } }
import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Service @Service class UserService { private lateinit var userRepository: UserRepository @Autowired fun setUserRepository(userRepository: UserRepository) { this.userRepository = userRepository } fun findUserById(id: Long): User? { return userRepository.findById(id).orElse(null) } }
Field Injection (Внедрение через поля):
Зависимости передаются напрямую в поля класса.
Простой способ внедрения, но менее предпочтительный из-за недостатков в области тестируемости и управляемости.
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class UserService { @Autowired private UserRepository userRepository; public User findUserById(Long id) { return userRepository.findById(id).orElse(null); } }
import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Service @Service class UserService { @Autowired private lateinit var userRepository: UserRepository fun findUserById(id: Long): User? { return userRepository.findById(id).orElse(null) } }
Конфигурация Spring:
Использование аннотаций (Java-based Configuration):
import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @Configuration @ComponentScan(basePackages = "com.example") public class AppConfig {}
Использование XML-конфигурации:
<!-- applicationContext.xml --> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <context:component-scan base-package="com.example"/> <bean id="userService" class="com.example.UserService"> <property name="userRepository" ref="userRepository"/> </bean> <bean id="userRepository" class="com.example.UserRepository"/> </beans>
➤ Что такое Domain-Driven Design (DDD)
Domain-Driven Design (DDD) — это методология разработки программного обеспечения, предложенная Эриком Эвансом в его книге “Domain-Driven Design: Tackling Complexity in the Heart of Software”. DDD сосредоточена на создании программных систем, которые точно отражают сложные бизнес-домены.
Основные концепции Domain-Driven Design:
Доменные модели (Domain Models):
Модель домена представляет собой абстракцию реального мира, которая включает в себя основные сущности, их атрибуты и взаимосвязи. Модель домена разрабатывается в тесном сотрудничестве с экспертами предметной области.
Условия и ограничения (Ubiquitous Language):
Условия и ограничения — это общий язык, который используется всеми участниками проекта, включая разработчиков и экспертов предметной области. Этот язык помогает устранить неоднозначности и улучшить коммуникацию.
Контексты ограничений (Bounded Contexts):
Контекст ограничения — это граница, в пределах которой определенные термины и концепции имеют четкое значение. Это помогает управлять сложностью, разделяя систему на логические части.
Сущности (Entities):
Сущности — это объекты, которые имеют уникальный идентификатор и определяются своей идентичностью, а не атрибутами.
Объекты-значения (Value Objects):
Объекты-значения — это объекты, которые определяются своими атрибутами и не имеют уникального идентификатора. Они неизменяемы и могут использоваться повторно.
Агрегаты (Aggregates):
Агрегаты — это группы связанных сущностей и объектов-значений, которые рассматриваются как единое целое. Агрегаты обеспечивают целостность данных и управляют изменениями через корневую сущность (Aggregate Root).
Службы (Services):
Службы — это операции или действия, которые не принадлежат никакой конкретной сущности или объекту-значению, но имеют важное значение для домена.
Репозитории (Repositories):
Репозитории предоставляют интерфейсы для доступа к сущностям и агрегатам из хранилища данных, абстрагируя детали реализации хранилища.
Модули (Modules):
Модули помогают организовать доменные объекты и их взаимодействия, разделяя систему на логические части.
➤ Что такое CQRS (Command Query Responsibility Segregation)
CQRS (разделение ответственности команд и запросов) — это архитектурный паттерн, который разделяет операции чтения (запросы) и записи (команды) в систему на отдельные модели. Это позволяет оптимизировать каждую операцию отдельно, улучшая производительность, масштабируемость и гибкость системы.
Основные идеи CQRS:
Разделение моделей:
Командная модель (Command Model):
обрабатывает команды, изменяет состояние системы и применяет бизнес-логику.
Запросная модель (Query Model):
обрабатывает запросы на чтение данных, оптимизирована для выполнения быстрых и эффективных запросов.
Команды и запросы:
Команда (Command):
операция, которая изменяет состояние системы (например, создание заказа, обновление профиля пользователя). Команды обычно являются операциями записи.
Запрос (Query):
операция, которая извлекает данные из системы (например, получение списка заказов, получение профиля пользователя). Запросы обычно являются операциями чтения.
➤ Что такое Event Sourcing
Event Sourcing (событийное хранение) — это архитектурный паттерн, в котором состояние системы моделируется как последовательность событий. Вместо хранения текущего состояния объекта, система сохраняет все события, которые привели к этому состоянию. Это позволяет воспроизвести текущее состояние системы путем повторного применения всех событий.
Основные идеи Event Sourcing:
События как источник истины:
Все изменения состояния системы записываются как события. Каждое событие описывает изменение состояния системы в определенный момент времени.
Воспроизведение состояния:
Текущее состояние объекта можно получить, последовательно применяя все события, связанные с этим объектом.
Неизменяемость событий:
События являются неизменяемыми и не могут быть изменены после записи. Это обеспечивает надежный и точный журнал изменений системы.
➤ Что такое Circuit Breaker (Переключатель цепи)
Circuit Breaker — это шаблон проектирования, используемый для повышения устойчивости и надежности распределенных систем, особенно микросервисных архитектур. Целью данного паттерна является предотвращение повторных попыток выполнения операции, которые, вероятно, будут терпеть неудачу, и быстрого выявления отказов, чтобы минимизировать их влияние на систему.
Основные идеи Circuit Breaker:
Предотвращение каскадных отказов:
Когда один из сервисов системы не отвечает или работает неправильно, попытки взаимодействия с ним могут привести к замедлению или даже отказу всей системы. Circuit Breaker помогает предотвратить такие каскадные отказы.
Три состояния:
Closed (Закрыто):
Запросы проходят через Circuit Breaker к целевому сервису. Если количество ошибок превышает заданный порог, Circuit Breaker переходит в состояние Open.
Open (Открыто):
Запросы немедленно завершаются ошибкой, не доходя до целевого сервиса. Через некоторое время (timeout) Circuit Breaker переходит в состояние Half-Open.
Half-Open (Полуоткрыто):
Некоторое количество запросов разрешается пройти к целевому сервису для проверки его работоспособности. Если запросы успешны, Circuit Breaker возвращается в состояние Closed. Если снова происходят ошибки, Circuit Breaker возвращается в состояние Open.
Механизм возврата к нормальной работе:
Если целевой сервис начинает работать нормально, Circuit Breaker автоматически возвращается в состояние Closed, позволяя запросам снова проходить.
Пример с использованием библиотеки Resilience4j:
Resilience4j — это популярная библиотека для реализации паттернов устойчивости, включая Circuit Breaker, в Java-приложениях.
dependencies { implementation 'io.github.resilience4j:resilience4j-spring-boot2:1.7.1' }
resilience4j.circuitbreaker: instances: myService: registerHealthIndicator: true slidingWindowSize: 10 failureRateThreshold: 50 waitDurationInOpenState: 10000 permittedNumberOfCallsInHalfOpenState: 3
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker import org.springframework.stereotype.Service import org.springframework.web.client.RestTemplate @Service class MyService(private val restTemplate: RestTemplate) { @CircuitBreaker(name = "myService", fallbackMethod = "fallback") fun callExternalService(): String { return restTemplate.getForObject("http://example.com/api", String::class.java) ?: "No response" } fun fallback(throwable: Throwable): String { return "Fallback response" } }
@CircuitBreaker:
аннотация используется для обозначения метода, к которому применяется Circuit Breaker.
name = “myService”:
указывает на конфигурацию Circuit Breaker, заданную в application.yml.
fallbackMethod = “fallback”:
указывает на метод, который будет вызван в случае срабатывания Circuit Breaker.
➤ Что такое JSON-RPC (Remote Procedure Call)
JSON-RPC (Remote Procedure Call) — это протокол удаленного вызова процедур, который используется для выполнения вызовов методов на удаленном сервере. Он использует JSON (JavaScript Object Notation) для кодирования данных и обмена сообщениями между клиентом и сервером. JSON-RPC является легковесным и простым протоколом, который поддерживает как однонаправленные уведомления, так и двунаправленные вызовы с ответами.
Основные особенности JSON-RPC:
Формат JSON:
Сообщения закодированы в формате JSON, что делает их легкими для понимания и обработки.
Запросы и ответы:
Запросы содержат имя метода, который нужно вызвать, и параметры для этого метода.
Ответы содержат результат вызова метода или информацию об ошибке, если вызов не удался.
Поддержка уведомлений:
Уведомления — это однонаправленные сообщения, которые не требуют ответа от сервера.
Простота и легковесность:
Протокол JSON-RPC минималистичен и не содержит сложных конструкций, что упрощает его реализацию и использование.
Пример использования JSON-RPC в Spring Boot приложении:
dependencies { implementation("org.springframework.boot:spring-boot-starter-web") implementation("com.googlecode.jsonrpc4j:jsonrpc4j:1.5.0") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") testImplementation("org.springframework.boot:spring-boot-starter-test") }
import com.googlecode.jsonrpc4j.JsonRpcService @JsonRpcService("/api") interface CalculatorService { fun subtract(a: Int, b: Int): Int }
import org.springframework.stereotype.Service @Service class CalculatorServiceImpl : CalculatorService { override fun subtract(a: Int, b: Int): Int { return a - b } }
import com.googlecode.jsonrpc4j.spring.AutoJsonRpcServiceImplExporter import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @Configuration class JsonRpcConfig { @Bean fun autoJsonRpcServiceImplExporter(): AutoJsonRpcServiceImplExporter { return AutoJsonRpcServiceImplExporter() } }
import com.googlecode.jsonrpc4j.spring.AutoJsonRpcServiceImplExporter import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController @RestController @RequestMapping("/api") class CalculatorController( private val calculatorService: CalculatorService ) : CalculatorService by calculatorService
➤ Что такое Spring MVC
Архитектура Spring MVC (Model-View-Controller) основана на хорошо известном шаблоне проектирования MVC, который разделяет приложение на три основные компонента: модель, представление и контроллер. Этот шаблон помогает разделить бизнес-логику, логику представления и логику управления, что делает приложение более модульным, удобным для поддержки и расширяемым.
Основные компоненты архитектуры Spring MVC:
Model (Модель):
Модель представляет данные приложения. Она содержит бизнес-логику и взаимодействует с базой данных для получения или сохранения данных.
В Spring MVC модель обычно представлена объектами Java (POJOs), которые обрабатываются сервисами и репозиториями.
View (Представление):
Представление отвечает за отображение данных пользователю. Оно формирует пользовательский интерфейс на основе данных модели.
В Spring MVC представления могут быть представлены различными технологиями, такими как JSP, Thymeleaf, Freemarker и другие.
Controller (Контроллер):
Контроллер принимает входящие запросы от клиента, обрабатывает их и возвращает соответствующее представление.
Контроллеры в Spring MVC обычно аннотируются с помощью @Controller и используют методы, аннотированные @RequestMapping, @GetMapping, @PostMapping и т.д. для маппинга запросов.
Основные компоненты Spring MVC:
DispatcherServlet:
основной компонент Spring MVC, который действует как передний контроллер (front controller). Он принимает все входящие HTTP-запросы, направляет их на соответствующие обработчики и возвращает соответствующие HTTP-ответы.
Handler Mapping:
отвечает за определение, какой контроллер и метод должны обрабатывать входящий запрос. Он использует маппинг URL и аннотации для сопоставления запросов с методами контроллера.
Controller:
обрабатывают запросы, выполняют логику приложения и возвращают данные модели, которые будут использованы в представлении.
View Resolver:
определяет, какое представление должно быть использовано для отображения данных. Он преобразует логическое имя представления в физическое местоположение представления, например, преобразует имя представления в путь к JSP-файлу.
Model and View:
объект, который содержит данные модели и информацию о представлении, которое должно быть использовано для отображения данных.
Поток обработки запроса в Spring MVC:
Получение запроса:
DispatcherServlet получает HTTP-запрос.
Определение контроллера:
Handler Mapping определяет, какой контроллер и метод должны обработать запрос.
Обработка запроса:
Контроллер обрабатывает запрос, выполняет бизнес-логику и запрашивает данные из модели.
Формирование ответа:
Контроллер возвращает объект ModelAndView или данные модели вместе с логическим именем представления.
Определение представления:
View Resolver определяет, какое представление должно быть использовано на основе логического имени представления.
Рендеринг представления:
Представление рендерится с использованием данных модели и отправляется обратно клиенту как HTTP-ответ.
➤ Какие могут быть слои в Spring
В типичном Spring приложении слои разделены на несколько категорий: контроллеры, сервисы, репозитории и модели. Эти слои помогают структурировать код и улучшить его поддерживаемость. Вот примеры каждого из слоев:
Модель (Model):
Модель представляет данные и их структуру. В Spring JPA это часто сущности, которые отражают таблицы базы данных.
import javax.persistence.Entity import javax.persistence.GeneratedValue import javax.persistence.GenerationType import javax.persistence.Id @Entity data class User( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long = 0, val username: String, val password: String, val role: String )
Репозиторий (Repository):
Репозиторий отвечает за доступ к данным и выполнение операций CRUD (создание, чтение, обновление, удаление).
import org.springframework.data.jpa.repository.JpaRepository interface UserRepository : JpaRepository<User, Long> { fun findByUsername(username: String): User? }
Сервис (Service):
Сервис содержит бизнес-логику приложения. Он обрабатывает данные, полученные из репозиториев, и передает их контроллерам.
import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.stereotype.Service @Service class UserService( private val userRepository: UserRepository, private val passwordEncoder: PasswordEncoder ) { fun findUserByUsername(username: String): User? { return userRepository.findByUsername(username) } fun registerUser(username: String, password: String, role: String): User { val encodedPassword = passwordEncoder.encode(password) val user = User(username = username, password = encodedPassword, role = role) return userRepository.save(user) } }
Контроллер (Controller):
Контроллер обрабатывает HTTP-запросы и возвращает ответы. Он использует сервисы для выполнения бизнес-логики и подготовки данных для представлений.
import org.springframework.stereotype.Controller import org.springframework.ui.Model import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.ModelAttribute import org.springframework.web.bind.annotation.PostMapping @Controller class UserController( private val userService: UserService ) { @GetMapping("/register") fun showRegistrationForm(model: Model): String { model.addAttribute("user", User()) return "register" } @PostMapping("/register") fun registerUser(@ModelAttribute user: User): String { userService.registerUser(user.username, user.password, "ROLE_USER") return "redirect:/login" } }
Конфигурация безопасности (Security Configuration):
Конфигурация безопасности отвечает за настройку доступа и авторизации.
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.core.userdetails.UserDetailsService import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.security.web.SecurityFilterChain @Configuration @EnableWebSecurity class SecurityConfig( private val userDetailsService: UserDetailsService ) { @Bean fun securityFilterChain(http: HttpSecurity): SecurityFilterChain { http .authorizeRequests { requests -> requests .antMatchers("/", "/home", "/register").permitAll() .anyRequest().authenticated() } .formLogin { form -> form .loginPage("/login") .permitAll() } .logout { logout -> logout.permitAll() } return http.build() } @Bean fun passwordEncoder(): PasswordEncoder { return BCryptPasswordEncoder() } }
Представления (Views):
Представления отображают данные пользователю. Они часто создаются с использованием шаблонизаторов, таких как Thymeleaf.
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>Home Page</title> </head> <body> <h1>Welcome</h1> <div th:if="${#httpServletRequest.remoteUser != null}"> <p>Welcome, <span th:text="${#httpServletRequest.remoteUser}">User</span>!</p> <a href="/logout">Logout</a> </div> <div th:if="${#httpServletRequest.remoteUser == null}"> <a href="/login">Login</a> | <a href="/register">Register</a> </div> </body> </html>
Главный класс:
import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication @SpringBootApplication class DemoApplication fun main(args: Array<String>) { runApplication<DemoApplication>(*args) }
➤ Как обычно структурируется Spring приложение
Разделение приложения на модули и папки помогает структурировать код, улучшает читаемость и поддерживаемость. В Spring приложениях обычно используется структурирование по слоям и функциональным модулям. Вот несколько подходов к организации структуры проекта:
Структурирование по слоям:
Самый распространенный подход — разделение на слои: контроллеры, сервисы, репозитории и модели.
src/main/kotlin/com/example/demo ├── controller │ ├── HomeController.kt │ ├── UserController.kt ├── service │ ├── UserService.kt ├── repository │ ├── UserRepository.kt ├── model │ ├── User.kt ├── security │ ├── SecurityConfig.kt └── DemoApplication.kt
Структурирование по функциональным модулям:
Этот подход организует код по функциональным областям, что удобно для больших проектов с множеством различных функциональных областей.
src/main/kotlin/com/example/demo ├── user │ ├── controller │ │ └── UserController.kt │ ├── service │ │ └── UserService.kt │ ├── repository │ │ └── UserRepository.kt │ └── model │ └── User.kt ├── home │ └── controller │ └── HomeController.kt ├── security │ └── SecurityConfig.kt └── DemoApplication.kt
Структурирование по техническим слоям:
В этом подходе классы группируются по техническим слоям, что особенно полезно для более сложных архитектур, таких как многослойные или микросервисные архитектуры.
src/main/kotlin/com/example/demo ├── config │ ├── SecurityConfig.kt ├── controller │ ├── HomeController.kt │ ├── UserController.kt ├── dto │ ├── UserDto.kt ├── exception │ ├── CustomException.kt ├── model │ ├── User.kt ├── repository │ ├── UserRepository.kt ├── service │ ├── UserService.kt ├── util │ ├── PasswordEncoderUtil.kt └── DemoApplication.kt
➤ Что лучше, аннотации или XML в Spring
В Spring Framework существуют два основных подхода к конфигурации и управлению зависимостями: использование аннотаций и использование XML-конфигурации. Оба подхода имеют свои преимущества и недостатки, и выбор между ними зависит от конкретных требований проекта и предпочтений разработчика.
Аннотации в Spring:
Аннотации в Spring позволяют конфигурировать и управлять зависимостями непосредственно в коде, что делает конфигурацию более читаемой и удобной для поддержки.
XML-конфигурация в Spring:
XML-конфигурация позволяет описывать бины и их зависимости в отдельном XML-файле, отделяя конфигурацию от кода. Это делает конфигурацию более декларативной и удобной для изменений без изменения кода.
Основные элементы XML-конфигурации:
<bean>:
Определяет бин и его свойства.
<property>:
Определяет свойства бина.
<constructor-arg>:
Определяет аргументы конструктора бина.
context:component-scan:
Сканирует указанные пакеты на наличие аннотированных классов.
Пример использования XML-конфигурации:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <context:component-scan base-package="com.example.demo"/> <bean id="userService" class="com.example.demo.service.UserService"/> <bean id="userController" class="com.example.demo.controller.UserController"> <property name="userService" ref="userService"/> </bean> </beans>
➤ В чем разница между REST, RPC, GraphQL и SOAP
REST, RPC, GraphQL и SOAP — это различные архитектурные стили и протоколы для создания веб-сервисов. Рассмотрим основные различия между ними.
REST (Representational State Transfer):
Плюсы:
Простота: Использует стандартные HTTP методы, что упрощает реализацию и понимание.
Кэширование: Поддержка кэширования на уровне HTTP повышает производительность.
Масштабируемость: Легко масштабируется за счет независимости ресурсов.
Гибкость: Может использовать различные форматы данных, такие как JSON, XML, YAML и др.
Широкое распространение: Поддерживается большинством современных веб-приложений и фреймворков.
Минусы:
Ограниченная функциональность: Не поддерживает сложные операции по сравнению с другими протоколами.
Избыточность данных: Может передавать больше данных, чем требуется клиенту.
Безопасность: Требует дополнительных мер для обеспечения безопасности, таких как OAuth.
RPC (Remote Procedure Call):
Плюсы:
Прозрачность вызовов: Легкость в использовании, так как удаленные вызовы выглядят как локальные.
Удобство: Подходит для выполнения сложных операций с помощью одного вызова.
Эффективность: Передача данных осуществляется только при вызове метода, что минимизирует избыточность.
Минусы:
Тесная связка: Клиент и сервер тесно связаны, что затрудняет изменения в API.
Отсутствие стандартов: Нет единого стандарта для реализации, что может вызвать несовместимость между различными системами.
Масштабируемость: Менее масштабируемый по сравнению с REST из-за необходимости поддерживать состояние сессии.
GraphQL:
Плюсы:
Гибкость: Клиенты могут запрашивать только те данные, которые им нужны.
Единая точка входа: Все запросы проходят через одну конечную точку, упрощая архитектуру.
Эффективность: Меньшее количество запросов, так как можно получить все необходимые данные за один запрос.
Типизация: Строгая типизация позволяет легко документировать и валидировать запросы.
Минусы:
Сложность: Более сложный для реализации и поддержки по сравнению с REST.
Перегрузка сервера: Может привести к перегрузке сервера, если клиент запрашивает слишком много данных.
Инструментарий: Требуется специальный инструментарий и библиотек для работы с GraphQL.
SOAP (Simple Object Access Protocol):
Плюсы:
Надежность: Поддерживает WS-Security, WS-ReliableMessaging и другие расширения для обеспечения безопасности и надежности.
Стандарты: Имеет строгие стандарты и спецификации, что облегчает интеграцию между различными системами.
Функциональность: Поддерживает сложные операции и транзакции.
Кросс-платформенность: Хорошо поддерживается в корпоративных средах и часто используется в интеграционных решениях.
Минусы:
Сложность: Более сложный для реализации и понимания по сравнению с REST.
Избыточность: Использует XML для передачи данных, что приводит к большему объему данных по сравнению с JSON.
Производительность: Более медленный по сравнению с REST из-за использования XML и дополнительных накладных расходов.
➤ Какие части могут быть в Spring приложении
Основные компоненты Spring:
Spring Framework:
Основной фреймворк, включающий контейнер инверсии управления (IoC), аспектно-ориентированное программирование (AOP), транзакции и другие основные функции.
Spring Boot:
Упрощает создание автономных приложений Spring с минимальной конфигурацией. Включает в себя автоконфигурацию, встроенные веб-серверы и множество стартовых пакетов (starters).
Веб-разработка:
Spring MVC:
Фреймворк для разработки веб-приложений с шаблонизацией, поддержкой RESTful веб-сервисов.
Spring WebFlux:
Реактивный веб-фреймворк для создания реактивных веб-приложений и микросервисов.
Thymeleaf:
Серверный шаблонизатор для создания HTML-страниц.
Angular, React, Vue.JS, TypeScript:
Фронтенд-фреймворки для создания клиентских приложений, которые могут быть интегрированы с Spring Boot через RESTful API или GraphQL.
Доступ к данным:
Spring Data JPA:
Интеграция с JPA для упрощения работы с базами данных через ORM.
Spring Data MongoDB:
Поддержка MongoDB.
Spring Data Redis:
Поддержка Redis.
Spring JDBC:
Простое взаимодействие с базами данных через JDBC.
Spring Data REST:
Автоматическое создание REST API для репозиториев данных.
Интеграция и обмен сообщениями:
Spring Integration:
Фреймворк для построения интеграционных решений на основе Enterprise Integration Patterns.
Spring Batch:
Поддержка пакетной обработки данных.
Spring AMQP:
Поддержка RabbitMQ.
Spring Kafka:
Поддержка Apache Kafka для обмена сообщениями.
Безопасность
Spring Security:
Фреймворк для аутентификации и авторизации приложений.
Spring Security OAuth2:
Поддержка OAuth2 и OpenID Connect.
Микросервисы и облако:
Spring Cloud:
Набор инструментов для разработки микросервисов, включающий конфигурацию, обнаружение сервисов, маршрутизацию и многое другое.
Spring Cloud Netflix:
Интеграция с библиотеками Netflix OSS, такими как Eureka, Hystrix и Zuul.
Spring Cloud Gateway:
Реактивный API-шлюз для маршрутизации и обработки запросов.
Spring Cloud Config:
Внешняя централизованная конфигурация для микросервисов.
Docker:
Контейнеризация приложений для упрощения развертывания и управления.
Kubernetes:
Оркестрация контейнеров для масштабируемости и надежности.
CI/CD инструменты:
Jenkins, GitLab CI, CircleCI и другие.
DevOps и мониторинг:
Spring Boot Actuator:
Метрики, мониторинг и управление приложением.
Spring Cloud Sleuth:
Трассировка распределенных систем.
Spring Boot Admin:
Административный интерфейс для управления и мониторинга Spring Boot приложений.
API и документация:
Swagger/OpenAPI:
Инструменты для документирования и тестирования API.
Spring REST Docs:
Генерация документации для REST API на основе тестов.
Мониторинг и управление:
Spring Boot Actuator:
Набор инструментов для мониторинга и управления приложениями Spring Boot.
Prometheus, Grafana, Splunk, Dynatrace:
Мониторинг и визуализация метрик.
ELK Stack (Elasticsearch, Logstash, Kibana):
Сбор, хранение и анализ логов.
Другие утилиты и инструменты:
MapStruct:
Инструмент для маппинга DTO и сущностей.
Lombok:
Библиотека для уменьшения шаблонного кода (например, геттеры, сеттеры, конструкторы).
Spring Cache:
Абстракция кэширования с поддержкой различных провайдеров (EhCache, Hazelcast, Redis).
Тестирование:
Spring Test:
Поддержка модульного и интеграционного тестирования с помощью JUnit и TestNG.
Spring Boot Test:
Поддержка тестирования Spring Boot приложений, включая тестирование веб-слоя и доступа к данным.
Mockito:
Фреймворк для создания mock-объектов и написания тестов.
Selenium, Cucumber
➤ Чем Spring отличается от Java EE
Spring и Java EE (теперь Jakarta EE) — это две разные платформы для создания корпоративных приложений на языке Java. Обе платформы предлагают различные инструменты и фреймворки для разработки, но они имеют свои особенности, преимущества и недостатки.
Основные отличия между Spring и Java EE (Jakarta EE):
Архитектура и подходы:
Spring:
это фреймворк, который предоставляет комплексные решения для разработки приложений. Он модульный и позволяет использовать только необходимые компоненты.
Использует контейнер для инверсии управления (IoC) и внедрения зависимостей (DI).
Легковесный и гибкий, позволяет легко интегрироваться с другими фреймворками и библиотеками.
Не требует сервера приложений, можно использовать встроенные серверы (например, Tomcat, Jetty).
Java EE (Jakarta EE):
это стандарт платформы для разработки корпоративных приложений на языке Java, предоставляющий набор спецификаций и API.
Основывается на серверах приложений, таких как WildFly, GlassFish, WebLogic, и JBoss, которые предоставляют все необходимые компоненты.
Стандартизирован и управляется консорциумом (ранее Oracle, сейчас Eclipse Foundation).
Чаще используется в крупных корпоративных приложениях с высокими требованиями к надежности и безопасности.
Контейнеры и внедрение зависимостей:
Spring:
Основывается на контейнере IoC, который управляет жизненным циклом объектов и их зависимостями.
Поддерживает несколько способов конфигурации: XML, аннотации, JavaConfig.
Java EE (Jakarta EE):
Использует контейнеры для управления жизненным циклом компонентов (EJB, сервлеты).
Внедрение зависимостей осуществляется с помощью CDI (Contexts and Dependency Injection).
Компоненты и модули:
Spring:
Включает множество проектов и модулей, таких как Spring MVC, Spring Data, Spring Security, Spring Boot, Spring Cloud и другие.
Spring Boot значительно упрощает создание и настройку приложений, предоставляя готовые шаблоны и автонастройку.
Java EE (Jakarta EE):
Состоит из множества спецификаций, таких как Servlet, JSP, JSF, JPA, EJB, JAX-RS, JMS и другие.
Все спецификации взаимосвязаны и стандартизированы, что обеспечивает совместимость между различными реализациями.
Сообщество и поддержка:
Spring:
Активное сообщество и сильная поддержка со стороны Pivotal (ныне часть VMware).
Быстрое внедрение новых технологий и обновлений.
Java EE (Jakarta EE):
Управляется сообществом под эгидой Eclipse Foundation (ранее Oracle).
Обновления и новые версии выходят реже, но спецификации тщательно проверяются и стандартизируются.
Инструменты и интеграция:
Spring:
Легко интегрируется с различными инструментами и библиотеками, такими как Hibernate, MyBatis, Apache Kafka и другими.
Spring Boot предоставляет встроенные серверы, что упрощает разработку и тестирование.
Java EE (Jakarta EE):
Предполагает использование полнофункциональных серверов приложений, что может быть сложнее и тяжелее для начальной настройки.
Хорошо интегрируется с другими спецификациями Java и сторонними библиотеками.
➤ Что такое аспектно-ориентированное программирование (AOP)
Аспектно-ориентированное программирование (AOP) — это парадигма программирования, которая позволяет отделить кросс-срезные (cross-cutting) аспекты приложения от его основной бизнес-логики. Основная цель AOP — улучшить модульность приложения, предоставляя способ выделения и повторного использования кода, который пересекает несколько модулей или классов, таких как логирование, управление транзакциями, безопасность и другие.
Основные концепции AOP:
Aspect (Аспект):
Модуль, содержащий кросс-срезную функциональность. Аспект может содержать одну или несколько точек сопоставления (pointcuts) и советов (advice).
Advice (Совет):
Действие, выполняемое аспектом. Типы советов включают:
Before: выполняется перед выполнением метода.
After: выполняется после выполнения метода.
AfterReturning: выполняется после успешного завершения метода.
AfterThrowing: выполняется после того, как метод выбросил исключение.
Around: выполняется до и после выполнения метода.
Pointcut (Точка сопоставления):
Условие, определяющее, на какие методы и объекты будет применяться совет. Pointcut используется для определения, где должен быть выполнен совет.
Join Point (Точка соединения):
Определенное место в выполнении программы, например, вызов метода или обработка исключения, где может быть применен совет.
Weaving (Ткацкий станок):
Процесс связывания аспектов с целевыми объектами для создания прокси-объектов. В Spring AOP ткацкий станок происходит во время выполнения (runtime).
➤ Как происходит обработка запроса и выдача ответа в Spring
Обработка запроса и выдача ответа в Spring MVC (Model-View-Controller) происходит через цепочку взаимодействий между несколькими компонентами. Основные этапы обработки запроса в Spring включают:
Получение запроса:
Веб-сервер (например, Tomcat) получает HTTP-запрос.
Передача запроса:
Веб-сервер передает запрос диспетчеру сервлетов (DispatcherServlet) Spring.
Обработка запроса DispatcherServlet:
Обработка через фильтры:
Запрос может проходить через фильтры (Filter) для предварительной обработки.
Определение контроллера:
DispatcherServlet использует HandlerMapping для определения подходящего контроллера для обработки запроса.
Вызов метода контроллера:
Выбранный метод контроллера обрабатывает запрос и возвращает результат.
Обработка через интерсепторы:
Запрос и ответ могут проходить через интерсепторы (HandlerInterceptor) для дополнительной обработки.
Формирование ответа:
DispatcherServlet передает результат в ViewResolver для рендеринга представления (если необходимо) или возвращает данные напрямую (например, JSON).
Отправка ответа клиенту:
Веб-сервер отправляет сформированный HTTP-ответ клиенту.
Подробный процесс обработки запроса:
Получение запроса:
Когда клиент (например, браузер или Postman) отправляет HTTP-запрос на сервер, запрос попадает на веб-сервер (например, Apache Tomcat).
Передача запроса DispatcherServlet:
Веб-сервер передает запрос диспетчеру сервлетов Spring (DispatcherServlet). DispatcherServlet является фронт-контроллером (Front Controller) для всех входящих запросов в Spring MVC.
Обработка запроса DispatcherServlet:
Обработка через фильтры:
Фильтры (Filter) могут использоваться для выполнения предварительной обработки запросов, таких как логирование, аутентификация, авторизация и т.д.
Определение контроллера:
DispatcherServlet использует один или несколько HandlerMapping для определения, какой контроллер и какой метод контроллера должны обработать запрос. HandlerMapping определяет контроллеры на основе URL-шаблонов, аннотаций и других критериев.
Вызов метода контроллера:
После определения контроллера DispatcherServlet вызывает соответствующий метод контроллера. Метод контроллера обрабатывает запрос, выполняет необходимую бизнес-логику и формирует результат.
Обработка через интерсепторы:
Интерсепторы (HandlerInterceptor) могут использоваться для выполнения дополнительной обработки до и после вызова метода контроллера. Интерсепторы могут модифицировать запросы и ответы, а также выполнять дополнительные проверки и логирование.
Формирование ответа:
Возвращение данных напрямую:
Если метод контроллера аннотирован @ResponseBody или контроллер аннотирован @RestController, возвращаемые данные (например, объект или список объектов) автоматически преобразуются в формат JSON или XML с помощью HttpMessageConverter.
Рендеринг представления:
Если метод контроллера возвращает имя представления (например, шаблон HTML), DispatcherServlet использует ViewResolver для рендеринга представления. ViewResolver определяет, какой шаблон использовать для генерации HTML-ответа.
Отправка ответа клиенту:
После формирования ответа (будь то JSON, XML или HTML) веб-сервер отправляет HTTP-ответ обратно клиенту.
➤ Что такое Project Reactor
высокопроизводительный асинхронный библиотека реактивного программирования для JVM, разработанная компанией Pivotal (ныне VMware) как часть экосистемы Spring. Reactor является основной реализацией Reactive Streams спецификации, и он предоставляет мощные инструменты для создания асинхронных, событийно-управляемых программ и систем.
Основные особенности Project Reactor:
Реактивные типы:
Mono:
Представляет собой асинхронный поток, который может содержать 0 или 1 элемент. Аналог Future или CompletableFuture, но с дополнительными реактивными операторами.
Flux:
Представляет собой асинхронный поток, который может содержать 0, 1 или более элементов (поток данных).
Богатый набор операторов:
Поддержка множества операторов для работы с данными, таких как фильтрация, трансформация, объединение, агрегация и многое другое.
Асинхронность и неблокирующий ввод/вывод:
Reactor поддерживает асинхронное выполнение и неблокирующий ввод/вывод, что делает его идеальным для создания высокопроизводительных и масштабируемых приложений.
Поддержка спецификации Reactive Streams:
Reactor соответствует спецификации Reactive Streams, обеспечивая совместимость с другими библиотеками и фреймворками, поддерживающими Reactive Streams.
Интеграция с экосистемой Spring:
Reactor тесно интегрирован с Spring Framework и Spring Boot, предоставляя реактивные возможности для создания веб-приложений, взаимодействия с базами данных и других задач.
import reactor.core.publisher.Mono fun main() { val mono: Mono<String> = Mono.just("Hello, Reactor!") mono.subscribe( { value -> println(value) }, // onNext { error -> println("Error: $error") }, // onError { println("Completed") } // onComplete ) }
import reactor.core.publisher.Flux fun main() { val flux: Flux<String> = Flux.just("Hello", "Reactor", "World") flux.subscribe( { value -> println(value) }, // onNext { error -> println("Error: $error") }, // onError { println("Completed") } // onComplete ) }
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import reactor.core.publisher.Flux import reactor.core.publisher.Mono @RestController class GreetingController { @GetMapping("/hello") fun hello(): Mono<String> { return Mono.just("Hello, Spring WebFlux!") } @GetMapping("/greetings") fun greetings(): Flux<String> { return Flux.just("Hello", "Hi", "Hey", "Hola") } }
➤ Что такое Mono и Flux
Mono и Flux — это ключевые типы в проекте Reactor, который является основой для реактивного программирования в Spring WebFlux. Эти типы представляют собой реактивные потоки данных и используются для обработки асинхронных операций.
Mono:
Представляет собой поток, который может содержать ноль или одно значение. Он используется для представления асинхронной операции, которая возвращает одно значение или завершается без возвращаемого значения (например, Mono).
Основные методы Mono:
just(value: T):
Создает Mono, который возвращает указанное значение.
empty():
Создает пустой Mono.
error(throwable: Throwable):
Создает Mono, который завершится с ошибкой.
fromCallable(callable: Callable):
Создает Mono из функции, которая возвращает значение.
map(transform: Function):
Преобразует значение в Mono с использованием функции.
flatMap(transform: Function):
Преобразует значение в Mono, возвращаемый функцией, и подписывается на него.
then():
Игнорирует результат и возвращает Mono
import org.springframework.web.bind.annotation.* import reactor.core.publisher.Mono @RestController @RequestMapping("/users") class UserController(private val userService: UserService) { @GetMapping("/{id}") fun getUserById(@PathVariable id: String): Mono<User> { return userService.getUserById(id) } @PostMapping fun createUser(@RequestBody user: User): Mono<User> { return userService.createUser(user) } @DeleteMapping("/{id}") fun deleteUser(@PathVariable id: String): Mono<Void> { return userService.deleteUser(id) } }
Flux:
Представляет собой поток, который может содержать ноль или более значений. Он используется для представления асинхронного потока данных, который может генерировать несколько значений, такие как коллекции или непрерывные потоки данных.
Основные методы Flux:
just(values: T…):
Создает Flux, который возвращает указанные значения.
empty():
Создает пустой Flux.
error(throwable: Throwable):
Создает Flux, который завершится с ошибкой.
fromIterable(iterable: Iterable):
Создает Flux из коллекции.
range(start: Int, count: Int):
Создает Flux, который генерирует последовательность чисел.
map(transform: Function):
Преобразует каждое значение в Flux с использованием функции.
flatMap(transform: Function):
Преобразует каждое значение в Flux, возвращаемый функцией, и подписывается на него.
filter(predicate: Predicate):
Фильтрует значения в Flux с использованием предиката.
import org.springframework.web.bind.annotation.* import reactor.core.publisher.Flux @RestController @RequestMapping("/users") class UserController(private val userService: UserService) { @GetMapping fun getAllUsers(): Flux<User> { return userService.getAllUsers() } @GetMapping("/role/{role}") fun getUsersByRole(@PathVariable role: String): Flux<User> { return userService.getUsersByRole(role) } @GetMapping("/usernames") fun getUsernames(): Flux<String> { return userService.getUsernames() } }
Сравнение Mono и Flux:
Mono:
Используется, когда ожидается ноль или одно значение.
Пример: Получение пользователя по ID (один результат или его отсутствие).
Flux:
Используется, когда ожидается ноль или более значений.
Пример: Получение списка всех пользователей (может быть ни одного, одного или множества пользователей).
Пример с использованием операций map, flatMap и filter:
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import reactor.core.publisher.Mono import java.time.Duration @RestController class HomeController { @GetMapping("/") fun index(): Mono<String> { return Mono.just("Welcome to the home page!") .map { it.toUpperCase() } // Преобразуем строку в верхний регистр .flatMap { appendDate(it) } // Добавляем текущую дату .filter { it.length > 20 } // Фильтруем строки длиной более 20 символов .switchIfEmpty(Mono.just("The resulting string is too short!")) // Если строка короче 20 символов, возвращаем альтернативное сообщение } private fun appendDate(str: String): Mono<String> { return Mono.just("$str - ${java.time.LocalDate.now()}") } }
Пример с использованием doOnNext и delayElement:
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import reactor.core.publisher.Mono import java.time.Duration @RestController class HomeController { @GetMapping("/") fun index(): Mono<String> { return Mono.just("Welcome to the home page!") .doOnNext { println("Original String: $it") } // Логируем оригинальную строку .map { it.reversed() } // Переворачиваем строку .delayElement(Duration.ofSeconds(2)) // Задержка в 2 секунды .doOnNext { println("Reversed String: $it") } // Логируем перевернутую строку } }
Пример с использованием zipWith для объединения с другой Mono:
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import reactor.core.publisher.Mono @RestController class HomeController { @GetMapping("/") fun index(): Mono<String> { val additionalInfo = Mono.just("This is additional info") return Mono.just("Welcome to the home page!") .zipWith(additionalInfo) { original, additional -> "$original - $additional" } } }
Пример с использованием flatMapMany для преобразования в Flux и последующего объединения:
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController import reactor.core.publisher.Flux import reactor.core.publisher.Mono @RestController class HomeController { @GetMapping("/") fun index(): Mono<String> { return Mono.just("Welcome to the home page!") .flatMapMany { Flux.fromArray(it.split(" ").toTypedArray()) } // Преобразуем строку в Flux слов .map { it.reversed() } // Переворачиваем каждое слово .collectList() // Собираем обратно в список .map { it.joinToString(" ") } // Объединяем слова обратно в строку } }
➤ Что такое сервлет (Servlet)
Сервлеты — это серверные компоненты, написанные на языке Java, которые используются для обработки запросов и создания динамического контента для веб-приложений. Они работают на сервере и являются частью технологии Java EE (ныне Jakarta EE). Сервлеты позволяют создавать веб-приложения, которые могут взаимодействовать с клиентами через HTTP.
Основные концепции сервлетов:
Сервлет-контейнер:
Среда выполнения, в которой сервлеты выполняются. Контейнер управляет жизненным циклом сервлета, его инициализацией, обработкой запросов и уничтожением.
HttpServlet:
Класс, от которого наследуются все сервлеты для обработки HTTP-запросов.
Servlet API:
Набор классов и интерфейсов, предоставляющих функциональность для создания сервлетов.
Жизненный цикл сервлета:
Инициализация (init):
Метод init вызывается контейнером при создании сервлета. Этот метод инициализирует сервлет и выполняется один раз за весь жизненный цикл сервлета.
Обработка запросов (service):
Метод service вызывается каждый раз, когда сервлет получает запрос от клиента. Этот метод делегирует запросы методам doGet, doPost, doPut, doDelete и т.д., в зависимости от типа HTTP-запроса.
Уничтожение (destroy):
Метод destroy вызывается контейнером перед уничтожением сервлета. Этот метод выполняется один раз и используется для освобождения ресурсов.
Пример простого сервлета:
import javax.servlet.ServletException import javax.servlet.annotation.WebServlet import javax.servlet.http.HttpServlet import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse import java.io.IOException @WebServlet("/hello") class HelloServlet : HttpServlet() { @Throws(ServletException::class, IOException::class) override fun doGet(request: HttpServletRequest, response: HttpServletResponse) { response.contentType = "text/html" response.writer.use { out -> out.println("<html><body>") out.println("<h1>Hello, World!</h1>") out.println("</body></html>") } } }
Конфигурация сервлета в web.xml:
<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0"> <servlet> <servlet-name>HelloServlet</servlet-name> <servlet-class>com.example.HelloServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>HelloServlet</servlet-name> <url-pattern>/hello</url-pattern> </servlet-mapping> </web-app>
Развертывание приложения:
Поместите ваш сервлет и конфигурационный файл web.xml в соответствующие директории вашего веб-приложения (например, WEB-INF/classes и WEB-INF соответственно). Разверните ваше приложение на сервере приложений, таком как Tomcat, WildFly или GlassFish.
Обработка различных типов HTTP-запросов:
Сервлеты могут обрабатывать различные типы HTTP-запросов, такие как GET, POST, PUT, DELETE, и т.д. Для этого необходимо переопределить соответствующие методы в классе сервлета.
import javax.servlet.ServletException import javax.servlet.annotation.WebServlet import javax.servlet.http.HttpServlet import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse import java.io.IOException @WebServlet("/example") class ExampleServlet : HttpServlet() { @Throws(ServletException::class, IOException::class) override fun doGet(request: HttpServletRequest, response: HttpServletResponse) { } @Throws(ServletException::class, IOException::class) override fun doPost(request: HttpServletRequest, response: HttpServletResponse) { } @Throws(ServletException::class, IOException::class) override fun doPut(request: HttpServletRequest, response: HttpServletResponse) { } @Throws(ServletException::class, IOException::class) override fun doDelete(request: HttpServletRequest, response: HttpServletResponse) { } }
Использование параметров запроса и сессий:
Сервлеты могут извлекать параметры запроса и работать с сессиями для сохранения данных между запросами.
Извлечение параметров запроса
import javax.servlet.ServletException import javax.servlet.annotation.WebServlet import javax.servlet.http.HttpServlet import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse import java.io.IOException @WebServlet("/param") class ParamServlet : HttpServlet() { @Throws(ServletException::class, IOException::class) override fun doGet(request: HttpServletRequest, response: HttpServletResponse) { val param = request.getParameter("paramName") response.contentType = "text/html" response.writer.use { out -> out.println("<html><body>") out.println("<h1>Parameter: $param</h1>") out.println("</body></html>") } } }
Работа с сессиями:
import javax.servlet.ServletException import javax.servlet.annotation.WebServlet import javax.servlet.http.HttpServlet import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletResponse import javax.servlet.http.HttpSession import java.io.IOException @WebServlet("/session") class SessionServlet : HttpServlet() { @Throws(ServletException::class, IOException::class) override fun doGet(request: HttpServletRequest, response: HttpServletResponse) { val session: HttpSession = request.session session.setAttribute("username", "JohnDoe") response.contentType = "text/html" response.writer.use { out -> out.println("<html><body>") out.println("<h1>Session created for user: ${session.getAttribute("username")}</h1>") out.println("</body></html>") } } }
➤ Что такое Инфраструктура открытых ключей (Public Key Infrastructure, PKI)
Инфраструктура открытых ключей (Public Key Infrastructure, PKI) — это система, которая обеспечивает управление, создание, распределение, использование, хранение и отзыв цифровых сертификатов и открытых ключей. PKI является основой для безопасного обмена данными в интернете, обеспечивая подлинность, целостность и конфиденциальность информации.
Основные компоненты PKI:
Центр сертификации (Certificate Authority, CA):
это доверенная третья сторона, которая выпускает и управляет цифровыми сертификатами. CA подтверждает подлинность субъекта, которому выдается сертификат.
Регистрационный центр (Registration Authority, RA):
отвечает за проверку идентификационных данных субъекта перед выдачей сертификата CA. RA действует как посредник между пользователем и CA.
Цифровой сертификат (Digital Certificate):
электронный документ, который связывает открытый ключ с идентификатором владельца (субъекта). Сертификат содержит информацию о владельце, открытый ключ, срок действия и цифровую подпись CA.
Открытый и закрытый ключи (Public and Private Keys):
Открытый ключ используется для шифрования данных и проверки цифровых подписей. Он распространяется публично.
Закрытый ключ используется для расшифрования данных и создания цифровых подписей. Он хранится в секрете.
Хранилище сертификатов (Certificate Store):
база данных, в которой хранятся цифровые сертификаты, закрытые ключи и другие элементы PKI.
Список отозванных сертификатов (Certificate Revocation List, CRL):
список сертификатов, которые были отозваны до истечения срока их действия. CA регулярно обновляет и распространяет CRL.
Протоколы и стандарты:
PKI использует различные стандарты и протоколы, такие как X.509 для цифровых сертификатов, SSL/TLS для защищенных соединений и другие.
Как работает PKI:
Запрос сертификата:
Пользователь (или устройство) генерирует пару ключей (открытый и закрытый ключи).
Пользователь отправляет запрос на сертификат (Certificate Signing Request, CSR) в CA через RA. CSR содержит информацию о пользователе и открытый ключ.
Проверка и выдача сертификата:
RA проверяет идентификационные данные пользователя.
CA создает цифровой сертификат, подписывая его своим закрытым ключом, и выдает его пользователю.
Использование сертификата:
Пользователь использует свой цифровой сертификат и связанный с ним закрытый ключ для безопасного обмена данными и цифровой подписи.
Проверка подлинности сертификата:
Получатель данных использует открытый ключ CA для проверки цифровой подписи сертификата пользователя, подтверждая его подлинность.
Отзыв сертификата:
В случае компрометации закрытого ключа или изменения статуса пользователя сертификат может быть отозван. CA добавляет отозванный сертификат в CRL.
Пример использования PKI:
Рассмотрим простой пример использования PKI для обеспечения безопасности веб-сайта с помощью SSL/TLS-сертификата.
Запрос и выдача SSL-сертификата:
Владелец веб-сайта генерирует пару ключей (открытый и закрытый ключи).
Владелец веб-сайта отправляет CSR в CA, предоставляя информацию о домене и открытый ключ.
CA проверяет право собственности на домен и выдает SSL-сертификат, подписывая его своим закрытым ключом.
Настройка веб-сайта:
Владелец веб-сайта устанавливает SSL-сертификат и связанный с ним закрытый ключ на веб-сервере.
Веб-сервер настроен для использования SSL/TLS для установления защищенных соединений.
Безопасное соединение:
Когда клиент (например, браузер) подключается к веб-сайту, сервер отправляет клиенту свой SSL-сертификат.
Клиент проверяет подлинность сертификата, используя открытый ключ CA.
Если сертификат действителен, устанавливается защищенное соединение, и данные шифруются с использованием открытого ключа сервера.
➤ Что такое CSRF (Cross-Site Request Forgery)
CSRF (Cross-Site Request Forgery) — это тип атаки на веб-приложения, при котором злоумышленник заставляет пользователя выполнить нежелательное действие на сайте, на котором он аутентифицирован. Атака осуществляется путем отправки поддельных запросов от имени пользователя, используя его аутентификационные данные (например, куки сессии).
Как работает CSRF:
Пользователь аутентифицирован на веб-сайте и имеет активную сессию.
Злоумышленник создает поддельную форму или ссылку на своем сайте, которая отправляет запросы на целевой сайт от имени пользователя.
Пользователь, посещая сайт злоумышленника, случайно выполняет действие на целевом сайте (например, перевод денег или изменение пароля).
Как предотвратить CSRF в Spring Security:
Spring Security предоставляет встроенную защиту от CSRF-атак, которая по умолчанию включена. Защита работает путем использования CSRF-токенов, которые должны быть включены в каждый изменяющий состояние запрос (например, POST, PUT, DELETE).
Как работает защита CSRF в Spring Security:
При загрузке страницы с формой, Spring Security генерирует уникальный CSRF-токен и добавляет его в скрытое поле формы.
При отправке формы токен отправляется вместе с запросом.
Сервер проверяет наличие и правильность токена. Если токен отсутствует или неправильный, запрос отклоняется.
Пример настройки защиты CSRF в Spring Security:
Включение защиты CSRF (обычно включено по умолчанию)
Защита от CSRF по умолчанию включена в Spring Security. Чтобы убедиться в этом, можно явно указать конфигурацию в классе SecurityConfig.
import org.springframework.context.annotation.Configuration import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter @Configuration @EnableWebSecurity class SecurityConfig : WebSecurityConfigurerAdapter() { override fun configure(http: HttpSecurity) { http.authorizeRequests() .antMatchers("/public/**", "/login", "/register").permitAll() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login") .defaultSuccessUrl("/home", true) .permitAll() .and() .csrf() // Включение защиты CSRF (включено по умолчанию) } }
Добавление CSRF-токена в формы:
Spring Security автоматически добавляет CSRF-токен в форму, если используется Thymeleaf. Для других шаблонизаторов или ручной обработки нужно явно включить токен в форму.
Пример формы с использованием Thymeleaf
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>Login</title> </head> <body> <h1>Login</h1> <form th:action="@{/login}" method="post"> <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" /> <div> <label for="username">Username:</label> <input type="text" id="username" name="username" required /> </div> <div> <label for="password">Password:</label> <input type="password" id="password" name="password" required /> </div> <div> <button type="submit">Login</button> </div> </form> </body> </html>
В этом примере строка добавляет CSRF-токен в форму.
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
Обработка запросов AJAX с CSRF:
Для запросов AJAX необходимо включить CSRF-токен в заголовок запроса.
Пример запроса AJAX с CSRF-токеном
function sendPostRequest() { var token = $("meta[name='_csrf']").attr("content"); var header = $("meta[name='_csrf_header']").attr("content"); $.ajax({ url: '/your-endpoint', type: 'POST', contentType: 'application/json', data: JSON.stringify({ key: 'value' }), beforeSend: function(xhr) { xhr.setRequestHeader(header, token); }, success: function(response) { console.log('Success:', response); }, error: function(error) { console.log('Error:', error); } }); }
Добавление мета-тегов для CSRF-токена в HTML:
<meta name="_csrf" content="${_csrf.token}"/> <meta name="_csrf_header" content="${_csrf.headerName}"/>
➤ Что такое формат Prometheus
Формат Prometheus — это стандартный формат для сбора, хранения и представления метрик, который используется системой мониторинга Prometheus. Этот формат включает различные типы метрик, метки (labels) для дополнительной информации и используется для легкого анализа и агрегирования данных.
Основные элементы формата Prometheus:
Типы метрик:
Prometheus поддерживает несколько типов метрик:
Counter:
Монотонно увеличивающееся значение, которое используется для подсчета чего-либо (например, количество запросов).
Gauge:
Значение, которое может увеличиваться и уменьшаться (например, текущее использование памяти).
Histogram:
Сводит значения в фиксированные интервалы (buckets) и используется для измерения распределения значений (например, время ответа запросов).
Summary:
Похоже на гистограмму, но также хранит квантильные оценки (например, 95-й процентиль времени ответа запросов).
Метки (Labels):
Метки используются для добавления дополнительной информации к метрикам. Они помогают фильтровать и агрегировать метрики по различным измерениям.
Формат данных:
Метрики в формате Prometheus представляются в виде текстовых строк с определенной структурой.
# HELP http_requests_total The total number of HTTP requests. # TYPE http_requests_total counter http_requests_total{method="get",handler="/api"} 1027 http_requests_total{method="post",handler="/api"} 230 # HELP cpu_usage The current CPU usage. # TYPE cpu_usage gauge cpu_usage{core="0"} 0.75 cpu_usage{core="1"} 0.60 # HELP request_duration_seconds A histogram of the request duration. # TYPE request_duration_seconds histogram request_duration_seconds_bucket{le="0.1"} 24054 request_duration_seconds_bucket{le="0.2"} 33444 request_duration_seconds_bucket{le="0.5"} 100392 request_duration_seconds_bucket{le="1.0"} 129389 request_duration_seconds_bucket{le="+Inf"} 144320 request_duration_seconds_sum 53423 request_duration_seconds_count 144320 # HELP response_size_bytes The size of HTTP responses. # TYPE response_size_bytes summary response_size_bytes{quantile="0.5"} 512 response_size_bytes{quantile="0.9"} 1024 response_size_bytes{quantile="0.99"} 2048 response_size_bytes_sum 10554321 response_size_bytes_count 24530
Комментарии:
Комментарии начинаются с # и предоставляют дополнительную информацию о метриках, такие как описание (HELP) и тип метрики (TYPE).
Метрики:
Каждая строка метрики состоит из имени метрики, меток (если есть) и значения.
Типы метрик:
http_requests_total:
Счетчик, отслеживающий количество HTTP-запросов.
cpu_usage:
Гейдж, отслеживающий текущее использование CPU.
request_duration_seconds:
Гистограмма, отслеживающая продолжительность запросов.
response_size_bytes:
Сводка, отслеживающая размеры ответов HTTP.
Как использовать формат Prometheus:
dependencies { implementation 'io.micrometer:micrometer-registry-prometheus' }
import org.springframework.boot.SpringApplication import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.context.annotation.Bean import io.micrometer.core.instrument.MeterRegistry import io.micrometer.prometheus.PrometheusMeterRegistry import io.micrometer.prometheus.PrometheusConfig @SpringBootApplication class Application fun main(args: Array<String>) { SpringApplication.run(Application::class.java, *args) } @Bean fun prometheusMeterRegistry(): PrometheusMeterRegistry { return PrometheusMeterRegistry(PrometheusConfig.DEFAULT) } @Bean fun metricsCommonTags(): MeterRegistryCustomizer<MeterRegistry> { return MeterRegistryCustomizer { registry -> registry.config().commonTags("application", "my-app") } }
Метод для экспонирования метрик:
import io.micrometer.prometheus.PrometheusMeterRegistry import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController @RestController class MetricsController( private val prometheusMeterRegistry: PrometheusMeterRegistry ) { @GetMapping("/actuator/prometheus") fun scrape(): String { return prometheusMeterRegistry.scrape() } }
➤ Что такое Структурированная конкуренция (Structured concurrency)
Структурированная конкуренция (Structured concurrency) — это концепция программирования, направленная на упрощение работы с параллелизмом и конкурентностью. Основная идея заключается в том, что все параллельные задачи должны быть структурированы таким образом, чтобы их жизненные циклы и иерархии задач были четко определены и управляемы. Это позволяет упростить управление задачами, улучшить читаемость и надежность кода, а также облегчить отладку и обработку ошибок.
Основные принципы структурированной конкуренции:
Иерархия задач:
Параллельные задачи организуются в четкую иерархию, где каждая задача имеет родительскую и дочерние задачи.
Жизненный цикл дочерней задачи контролируется родительской задачей.
Автоматическое управление жизненным циклом задач:
Родительская задача отвечает за создание и завершение всех своих дочерних задач. Завершение родительской задачи автоматически приводит к завершению всех ее дочерних задач.
Явное управление областью видимости задач:
Область видимости (scope) параллельных задач ограничена блоками кода, что упрощает управление задачами и их ресурсами.
➤ Что такое Принцип Happens-Before (событийное предшествование)
Принцип Happens-Before (событийное предшествование) является ключевым понятием в теории многопоточности и параллелизма, описывающим упорядочение операций и событий в многопоточной среде. Этот принцип используется для определения корректности выполнения программ и обеспечения синхронизации между потоками.
Основные идеи принципа Happens-Before:
Последовательность выполнения в одном потоке:
Если одна операция предшествует другой в одном и том же потоке, то первая операция обязательно произойдет раньше второй. Это обеспечивает программную логику и упорядоченность выполнения операций в пределах одного потока.
Синхронизация с помощью блокировок (locks):
Если операция A разблокирует монитор (lock), а операция B захватывает этот же монитор, то операция A происходит перед операцией B. Это гарантирует, что изменения, сделанные потоком до разблокировки, видимы другому потоку после захвата блокировки.
Синхронизация с помощью событий (например, Thread.join()):
Если операция A вызывает метод Thread.join() на потоке B, то все операции в потоке B до его завершения происходят до возвращения из Thread.join() в потоке A.
Синхронизация с помощью volatile переменных:
Запись в volatile переменную происходит до всех последующих чтений этой переменной другими потоками. Это гарантирует видимость изменений.
Транзитивность:
Если операция A происходит перед операцией B, а операция B происходит перед операцией C, то операция A происходит перед операцией C.
➤ Чем WAR (Web Application Archive) отличается от JAR (Java Archive)
WAR (Web Application Archive) и JAR (Java Archive) — это два разных формата архивов, используемых для упаковки и распространения приложений на платформе Java.
JAR-файлы:
используются для упаковки Java-программ и библиотек
WAR-файлы:
предназначены для упаковки и развертывания веб-приложений на сервере приложений.
➤ Какой жизненный цикл у бинов в Spring
Жизненный цикл бинов в Spring Framework включает несколько этапов, начиная с создания и заканчивая уничтожением. Понимание жизненного цикла бинов важно для правильного управления ресурсами и настройки бинов в приложениях на основе Spring.
Основные этапы жизненного цикла бинов в Spring:
Создание экземпляра:
Создание экземпляра бина начинается с вызова конструктора класса бина. Этот процесс обычно осуществляется контейнером Spring с использованием рефлексии.
Внедрение зависимостей:
После создания экземпляра контейнер Spring внедряет зависимости (с помощью сеттеров, конструкторов или полей, аннотированных @Autowired).
Настройка инициализации (Setters и @PostConstruct):
После внедрения зависимостей Spring вызывает методы настройки и инициализации.
Методы, аннотированные @PostConstruct, вызываются после внедрения зависимостей, но до использования бина.
Инициализация:
Если бин реализует интерфейс InitializingBean, Spring вызывает метод afterPropertiesSet().
Можно также указать метод инициализации с помощью аннотации @Bean(initMethod=”init”) в конфигурационном классе.
Использование бина:
После выполнения всех методов инициализации бин готов к использованию в приложении.
Деинициализация (DisposableBean и @PreDestroy):
Когда контейнер Spring закрывается (например, при завершении работы приложения), бины уничтожаются.
Если бин реализует интерфейс DisposableBean, Spring вызывает метод destroy().
Методы, аннотированные @PreDestroy, также вызываются перед уничтожением бина.
Можно также указать метод деинициализации с помощью аннотации @Bean(destroyMethod=”cleanup”) в конфигурационном классе.
Пример жизненного цикла бина:
import javax.annotation.PostConstruct import javax.annotation.PreDestroy import org.springframework.beans.factory.DisposableBean import org.springframework.beans.factory.InitializingBean class MyBean : InitializingBean, DisposableBean { init { println("1. Конструктор: Создание экземпляра бина") } @PostConstruct fun postConstruct() { println("3. @PostConstruct: Метод postConstruct()") } override fun afterPropertiesSet() { println("4. InitializingBean: Метод afterPropertiesSet()") } fun customInit() { println("5. @Bean(initMethod): Метод customInit()") } @PreDestroy fun preDestroy() { println("7. @PreDestroy: Метод preDestroy()") } override fun destroy() { println("6. DisposableBean: Метод destroy()") } fun customDestroy() { println("8. @Bean(destroyMethod): Метод customDestroy()") } }
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @Configuration class AppConfig { @Bean(initMethod = "customInit", destroyMethod = "customDestroy") fun myBean(): MyBean { return MyBean() } }
import org.springframework.context.annotation.AnnotationConfigApplicationContext fun main() { val context = AnnotationConfigApplicationContext(AppConfig::class.java) context.getBean(MyBean::class.java) context.close() }
Вывод при выполнении программы:
1. Конструктор: Создание экземпляра бина 2. Внедрение зависимостей (если есть) 3. @PostConstruct: Метод postConstruct() 4. InitializingBean: Метод afterPropertiesSet() 5. @Bean(initMethod): Метод customInit() 6. Использование бина 7. @PreDestroy: Метод preDestroy() 8. DisposableBean: Метод destroy() 9. @Bean(destroyMethod): Метод customDestroy()
Конструктор:
Spring создает экземпляр бина, вызывая его конструктор.
Внедрение зависимостей:
Spring внедряет все зависимости в бин (если есть).
@PostConstruct:
Вызывается метод, аннотированный @PostConstruct.
InitializingBean:
Если бин реализует интерфейс InitializingBean, вызывается метод afterPropertiesSet().
@Bean(initMethod):
Если в конфигурации указан метод инициализации, он вызывается.
Использование бина: Бин готов к использованию.
@PreDestroy:
Перед уничтожением бина вызывается метод, аннотированный @PreDestroy.
DisposableBean:
Если бин реализует интерфейс DisposableBean, вызывается метод destroy().
@Bean(destroyMethod):
Если в конфигурации указан метод деинициализации, он вызывается.
➤ Что такое Git Registry, GitHub Packages, GitHub Actions, Changelog, Release Notes
Git Registry:
это термин, который обычно используется для описания системы, управляющей версиями и артефактами программного обеспечения, построенной на базе Git. Это может включать в себя хостинг и управление пакетами, такими как Docker образы, библиотеки для различных языков программирования и другие артефакты.
GitHub Packages:
это встроенный в GitHub сервис для хостинга и управления пакетами. Он позволяет разработчикам публиковать, управлять и использовать пакеты в их проектах, используя тот же аккаунт и репозитории GitHub.
GitHub Actions:
это встроенная в GitHub платформа для автоматизации процессов CI/CD. Она позволяет создавать, тестировать и деплоить код непосредственно из репозитория GitHub с помощью рабочих процессов (workflow), написанных в файле YAML.
Changelog (журнал изменений):
это документ, который содержит список изменений, внесенных в проект с каждой версией. Changelog помогает отслеживать изменения, исправления багов, новые функции и другие важные события в истории проекта.
Release Notes (заметки о выпуске):
это документ, который предоставляет информацию о конкретной версии программного обеспечения, включая новые функции, улучшения, исправления багов и известные проблемы. Release Notes часто публикуются вместе с выпуском новой версии и помогают пользователям понять, что изменилось.
➤ Какие основные протоколы обмена данными в Spring
SOAP (Simple Object Access Protocol):
это протокол для обмена структурированными информационными сообщениями в распределенных вычислительных средах. SOAP является стандартом для веб-сервисов и позволяет приложениям взаимодействовать по сети независимо от их платформы и технологий, используемых для их реализации.
SOAP-сообщение состоит из следующих основных частей:
Envelope: Основной элемент, определяющий начало и конец сообщения. Он включает два подэлемента:
Header (необязательный): Содержит метаинформацию о сообщении, такую как информация о безопасности, транзакциях и маршрутизации.
Body: Содержит основное содержимое сообщения, включая вызовы методов и ответы.
Fault (необязательный): Элемент, используемый для передачи информации об ошибках.
Пример SOAP-сообщения:
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:web="http://www.example.org/webservice"> <soapenv:Header/> <soapenv:Body> <web:GetUserDetails> <web:UserId>12345</web:UserId> </web:GetUserDetails> </soapenv:Body> </soapenv:Envelope>
HTTP (Hypertext Transfer Protocol) и HTTPS (HTTP Secure):
основные протоколы, используемые для передачи данных между клиентами и серверами в веб-приложениях. Spring MVC и Spring WebFlux используют HTTP/HTTPS для обработки веб-запросов и ответов.
REST (Representational State Transfer):
архитектурный стиль, использующий HTTP-протокол для создания веб-сервисов, которые взаимодействуют с использованием стандартных HTTP-методов (GET, POST, PUT, DELETE и т.д.). В Spring Boot RESTful веб-сервисы реализуются с использованием аннотаций, таких как @RestController, @GetMapping, @PostMapping и т.д.
WebSocket:
протокол для двусторонней связи между клиентом и сервером через одно долговременное соединение. WebSockets часто используются для приложений в реальном времени, таких как чаты и онлайн-игры.
JMS (Java Message Service):
стандартный API для отправки сообщений и асинхронного взаимодействия между распределенными компонентами. Spring предоставляет интеграцию с JMS через Spring JMS.
RSocket:
протокол для асинхронной передачи данных и взаимодействия между сервисами. Spring поддерживает RSocket через Spring Messaging и Spring Boot.
AMQP (Advanced Message Queuing Protocol):
протокол для обмена сообщениями между компонентами системы. Spring поддерживает AMQP через проект Spring AMQP, который включает интеграцию с RabbitMQ.
➤ Что такое Protobuf
Protobuf (Protocol Buffers) — это механизм сериализации данных, разработанный Google. Он используется для эффективного обмена данными между системами и сохранения данных. Protobuf позволяет определять структуры данных и автоматически генерировать код для сериализации и десериализации этих структур на различных языках программирования.
Основные шаги работы с Protobuf:
Определение схемы данных.
Компиляция схемы в код для выбранного языка программирования.
Использование сгенерированного кода для сериализации и десериализации данных.
Пример использования Protobuf в Spring Boot:
Определение схемы данных:
Создайте файл схемы Protobuf, например user.proto:
syntax = "proto3"; package com.example.demo; message User { int32 id = 1; string name = 2; string email = 3; }
plugins { id("com.google.protobuf") version "0.8.17" id("org.springframework.boot") version "2.5.4" id("io.spring.dependency-management") version "1.0.11.RELEASE" kotlin("jvm") version "1.5.21" kotlin("plugin.spring") version "1.5.21" } dependencies { implementation("org.springframework.boot:spring-boot-starter-web") implementation("com.google.protobuf:protobuf-java:3.17.3") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") testImplementation("org.springframework.boot:spring-boot-starter-test") } protobuf { protoc { artifact = "com.google.protobuf:protoc:3.17.3" } generatedFilesBaseDir = "$projectDir/src/generated" } sourceSets { main { java { srcDirs("src/generated/main/java") } } }
import com.example.demo.UserProtos.User import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream @RestController @RequestMapping("/api") class UserController { @GetMapping("/user") fun getUser(): ByteArray { val user = User.newBuilder() .setId(1) .setName("John Doe") .setEmail("[email protected]") .build() val outputStream = ByteArrayOutputStream() user.writeTo(outputStream) return outputStream.toByteArray() } @GetMapping("/user-from-bytes") fun getUserFromBytes(): User { val userBytes = getUser() val inputStream = ByteArrayInputStream(userBytes) return User.parseFrom(inputStream) } }
# Получение сериализованного пользователя curl -X GET http://localhost:8080/api/user --output user.protobuf # Получение пользователя из байтов curl -X GET http://localhost:8080/api/user-from-bytes
➤ Что такое Javadoc
Javadoc — это инструмент для автоматической генерации документации для Java-кода. Javadoc берет комментарии, написанные в специальном формате, и генерирует из них HTML-документацию. Это один из стандартов документирования кода в Java и широко используется в профессиональной разработке для создания понятной и поддерживаемой документации.
Основные элементы Javadoc:
Javadoc использует специальные комментарии, которые начинаются с /** и заканчиваются */. Внутри этих комментариев используются теги для описания различных частей кода.
Основные теги Javadoc:
@param:
Описание параметра метода.
@return:
Описание возвращаемого значения метода.
@throws или @exception:
Описание исключения, которое может быть выброшено методом.
@see:
Ссылка на другую часть кода или документацию.
@since:
Версия, в которой был добавлен данный элемент.
@deprecated:
Указывает, что элемент устарел и не рекомендуется к использованию.
@author:
Автор кода.
@version:
Версия кода.
/** * Класс представляет собой модель пользователя в системе. * <p> * Этот класс используется для хранения информации о пользователе, * включая его идентификатор, имя и электронную почту. * </p> * * @author John Doe * @version 1.0 * @since 2023-07-05 */ public class User { private Long id; private String name; private String email; /** * Конструктор создает нового пользователя с заданным именем и электронной почтой. * * @param name Имя пользователя. * @param email Электронная почта пользователя. */ public User(String name, String email) { this.name = name; this.email = email; } /** * Возвращает идентификатор пользователя. * * @return Идентификатор пользователя. */ public Long getId() { return id; } /** * Устанавливает идентификатор пользователя. * * @param id Идентификатор пользователя. */ public void setId(Long id) { this.id = id; } /** * Возвращает имя пользователя. * * @return Имя пользователя. */ public String getName() { return name; } /** * Устанавливает имя пользователя. * * @param name Имя пользователя. */ public void setName(String name) { this.name = name; } /** * Возвращает электронную почту пользователя. * * @return Электронная почта пользователя. */ public String getEmail() { return email; } /** * Устанавливает электронную почту пользователя. * * @param email Электронная почта пользователя. */ public void setEmail(String email) { this.email = email; } }
Для генерации документации из комментариев Javadoc используется инструмент javadoc, который включен в JDK.
javadoc -d doc -sourcepath src com.example.demo
d doc:
указывает директорию для выходных HTML файлов.
sourcepath src:
указывает путь к исходным файлам.
com.example.demo:
указывает пакет, для которого будет генерироваться документация.
Интеграция Javadoc с Gradle:
plugins { id 'java' id 'maven-publish' } dependencies { implementation 'org.springframework.boot:spring-boot-starter-web:2.5.4' implementation 'org.springframework.boot:spring-boot-starter-data-jpa:2.5.4' implementation 'com.h2database:h2:1.4.200' testImplementation 'org.springframework.boot:spring-boot-starter-test:2.5.4' } tasks.withType(Javadoc) { options { encoding = 'UTF-8' charSet = 'UTF-8' links 'https://docs.oracle.com/en/java/javase/11/docs/api/' } } task javadocJar(type: Jar) { archiveClassifier = 'javadoc' from javadoc } publishing { publications { mavenJava(MavenPublication) { from components.java artifact javadocJar } } repositories { maven { url = uri("$buildDir/repo") } } }
tasks.withType(Javadoc):
Настраиваем параметры генерации Javadoc, включая кодировку и ссылки на внешнюю документацию.
task javadocJar:
Создаем задачу для создания JAR файла с Javadoc.
publishing:
Настраиваем публикацию артефактов, включая JAR с Javadoc, в локальный репозиторий.
➤ Какие основные отличия Maven от Gradle
Синтаксис и декларативность:
Maven использует XML (POM файл) для описания проекта и его конфигурации. Это делает его декларативным и структурированным, но иногда довольно громоздким и трудно читаемым.
Gradle использует Groovy или Kotlin DSL, что делает его более гибким и лаконичным. Синтаксис Groovy и Kotlin позволяет писать конфигурации, которые более читаемы и поддерживают программирование.
Производительность:
Gradle обычно быстрее благодаря его инкрементальной сборке и кэшированию. Gradle выполняет только те задачи, которые изменились, и использует кэширование для избежания повторных операций.
Maven более линейный и последовательный в выполнении задач, что может приводить к более длительным сборкам по сравнению с Gradle.
Гибкость и расширяемость:
Gradle более гибкий благодаря возможности использовать язык программирования (Groovy или Kotlin) для описания конфигураций. Это позволяет легко расширять и настраивать процессы сборки.
Maven менее гибкий из-за своей декларативной природы. Настройки и расширения обычно требуют написания и подключения дополнительных плагинов.
Поддержка многопроектных сборок:
Gradle изначально поддерживает многопроектные сборки, что делает его более подходящим для крупных проектов с несколькими модулями.
Maven тоже поддерживает многопроектные сборки, но настройка может быть сложнее и менее интуитивной по сравнению с Gradle.
Конфигурационные файлы:
Maven использует один основной файл конфигурации — pom.xml.
Gradle использует build.gradle для каждого проекта или модуля, а также может включать settings.gradle для многопроектных сборок.
Плагины и зависимости:
Gradle имеет более современную систему управления плагинами и зависимостями. Плагины можно подключать через plugins блок, что делает это проще и более интуитивно.
Maven использует плагины, которые необходимо явно объявлять в pom.xml.
Поддержка скриптов:
Gradle позволяет писать логические условия и циклы прямо в файле конфигурации, что делает его более мощным для сложных сборочных сценариев.
Maven больше ориентирован на декларативный подход, что делает написание сложной логики менее удобным.
Интеграция с IDE:
Gradle поддерживается большинством современных IDE, таких как IntelliJ IDEA, Eclipse и Android Studio, что делает его более удобным для разработчиков Android.
Maven также поддерживается всеми популярными IDE, но Gradle имеет большее распространение в среде разработки Android.
➤ Из чего состоит pom.xml
Model Version:
Версия модели POM
<modelVersion>4.0.0</modelVersion>
Group ID:
Уникальный идентификатор организации или группы, которая создает проект
<groupId>com.example</groupId>
Artifact ID:
Уникальный идентификатор самого проекта.
<artifactId>my-app</artifactId>
Version:
Версия проекта.
<version>1.0-SNAPSHOT</version>
Packaging:
Тип упаковки проекта (например, jar, war, pom и т.д.).
<packaging>jar</packaging>
Name:
Человеко-читаемое имя проекта.
<name>My App</name>
Description:
Описание проекта.
<description>My Maven Project</description>
Dependencies:
Раздел, описывающий зависимости проекта от других библиотек или модулей.
<dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> </dependencies>
Repositories:
Раздел, указывающий на репозитории, откуда Maven будет загружать зависимости.
<repositories> <repository> <id>central</id> <url>https://repo.maven.apache.org/maven2</url> </repository> </repositories>
Build:
Раздел, описывающий инструкции по сборке проекта.
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins> </build>
Properties:
Раздел, определяющий свойства, которые могут быть использованы в POM-файле.
<properties> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties>
➤ Из чего состоит build.gradle
Plugins:
Подключение плагинов, например, для компиляции Java кода.
plugins { id 'java' }
Group и Version:
Задание идентификатора и версии проекта.
group 'com.example' version '1.0-SNAPSHOT'
Source Compatibility:
Установка версии языка Java.
sourceCompatibility = 1.8
Repositories:
Указание репозиториев, откуда будут загружаться зависимости.
repositories { mavenCentral() }
Dependencies:
Определение зависимостей проекта.
dependencies { testImplementation 'junit:junit:4.12' }
Test:
Настройка задач для тестирования.
test { useJUnitPlatform() }
Tasks with Type:
Настройка задач компиляции.
tasks.withType(JavaCompile) { options.encoding = 'UTF-8' sourceCompatibility = '1.8' targetCompatibility = '1.8' }
Jar:
Настройка задачи упаковки в JAR файл.
jar { manifest { attributes( 'Implementation-Title': 'My App', 'Implementation-Version': version ) } }
➤ Что такое Supplier
В Java, Supplier — это функциональный интерфейс из пакета java.util.function, который представляет собой функцию, не принимающую аргументов и возвращающую результат. Supplier используется для ленивой инициализации, генерации значений по запросу и реализации различных шаблонов проектирования, таких как фабрика.
Основные особенности Supplier:
Функциональный интерфейс:
Интерфейс с единственным абстрактным методом.
Метод get:
Абстрактный метод, который не принимает аргументов и возвращает значение типа T.
Пример использования Supplier:
Определение Supplier:
import java.util.function.Supplier; public class SupplierExample { public static void main(String[] args) { Supplier<String> stringSupplier = () -> "Hello, World!"; System.out.println(stringSupplier.get()); } }
Использование Supplier для ленивой инициализации:
import java.util.function.Supplier; public class LazyInitializationExample { private Supplier<ExpensiveObject> expensiveObjectSupplier = this::createExpensiveObject; private ExpensiveObject expensiveObject; public ExpensiveObject getExpensiveObject() { if (expensiveObject == null) { expensiveObject = expensiveObjectSupplier.get(); } return expensiveObject; } private ExpensiveObject createExpensiveObject() { System.out.println("Creating Expensive Object..."); return new ExpensiveObject(); } public static void main(String[] args) { LazyInitializationExample example = new LazyInitializationExample(); System.out.println("First call:"); example.getExpensiveObject(); // Object will be created here System.out.println("Second call:"); example.getExpensiveObject(); // Object already created, no creation message } static class ExpensiveObject { // Some expensive operations } }
Использование Supplier в коллекциях:
import java.util.HashMap; import java.util.Map; import java.util.function.Supplier; public class CollectionExample { public static void main(String[] args) { Map<String, Supplier<Object>> registry = new HashMap<>(); registry.put("key1", () -> "Value for key1"); registry.put("key2", () -> 12345); registry.put("key3", () -> new Object()); Object value1 = registry.get("key1").get(); Object value2 = registry.get("key2").get(); Object value3 = registry.get("key3").get(); System.out.println(value1); // Output: Value for key1 System.out.println(value2); // Output: 12345 System.out.println(value3); // Output: java.lang.Object@<hashcode> } }
Применение в Spring:
В Spring Supplier также может быть полезен для ленивой инициализации бинов или фабричных методов.
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.function.Supplier; @Configuration public class AppConfig { @Bean public Supplier<MyBean> myBeanSupplier() { return MyBean::new; } @Bean public MyService myService(Supplier<MyBean> myBeanSupplier) { return new MyService(myBeanSupplier.get()); } } class MyBean { // Bean definition } class MyService { private final MyBean myBean; public MyService(MyBean myBean) { this.myBean = myBean; } // Service methods }
➤ В чем отличие record в Java и data class в Kotlin
Record в Java и data class в Kotlin служат для схожих целей — создания простых классов для хранения данных с минимальным шаблонным кодом. Однако между ними есть некоторые различия, обусловленные особенностями языков и их фреймворков.
Основные различия между record в Java и data class в Kotlin:
Иммутабельность:
Java record: Поля record по умолчанию являются финальными и неизменяемыми.
Kotlin data class: Поля по умолчанию не являются финальными, и их можно изменять. Однако можно объявить их val (immutable) или var (mutable).
Поддержка наследования:
Java record: record не поддерживает наследование от других классов (но может реализовывать интерфейсы).
Kotlin data class: data class может наследовать другие классы и реализовывать интерфейсы.
Генерация методов:
Java record: Автоматически генерирует конструктор, методы equals, hashCode, toString, а также методы доступа к полям.
Kotlin data class: Автоматически генерирует конструктор, методы equals, hashCode, toString, а также методы copy и componentN (для деструктуризации).
Конструкторы:
Java record: Позволяет определять дополнительные конструкторы и методы, но автоматически генерирует компактный конструктор.
Kotlin data class: Поддерживает первичные и вторичные конструкторы.
Особенности использования:
Java record: Используются для создания простых неизменяемых объектов данных. Поля должны быть объявлены в параметрах заголовка записи.
Kotlin data class: Широкие возможности, включая методы copy для клонирования объектов с изменением отдельных полей.
➤ Что такое Java JWT (Java JSON Web Token)
библиотека Java JWT (JSON Web Token) под названием JJWT, разработана для создания и проверки JWT (JSON Web Token) токенов. JJWT (Java JWT) — это библиотека с открытым исходным кодом, которая упрощает работу с JWT в приложениях на языке Java. JWT — это компактный, URL-безопасный способ представления токенов, которые могут использоваться для передачи заверенной информации между двумя сторонами.
Основные особенности JJWT:
Создание JWT:
Простое создание JWT с различными типами полезной нагрузки и подписи.
Парсинг и проверка JWT:
Возможность парсинга и проверки JWT токенов для проверки их подлинности и извлечения полезной нагрузки.
Поддержка различных алгоритмов подписи:
Поддержка различных алгоритмов подписи, таких как HMAC, RSA и EC.
dependencies { implementation("io.jsonwebtoken:jjwt-api:0.11.2") runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.2") runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.2") }
import io.jsonwebtoken.Jwts import io.jsonwebtoken.SignatureAlgorithm import io.jsonwebtoken.security.Keys import java.security.Key import java.util.Date object JwtUtil { private val key: Key = Keys.secretKeyFor(SignatureAlgorithm.HS256) fun createToken(subject: String): String { return Jwts.builder() .setSubject(subject) .setIssuedAt(Date()) .setExpiration(Date(System.currentTimeMillis() + 3600000)) // 1 час .signWith(key) .compact() } fun parseToken(token: String): String? { return Jwts.parserBuilder() .setSigningKey(key) .build() .parseClaimsJws(token) .body .subject } }
Создание ключа:
Keys.secretKeyFor(SignatureAlgorithm.HS256):
Создает ключ для алгоритма подписи HMAC-SHA256.
Создание токена:
Jwts.builder():
оздает новый JwtBuilder для создания JWT.
setSubject(subject):
Устанавливает субъект (subject) токена.
setIssuedAt(Date()):
Устанавливает время выпуска токена.
setExpiration(Date(System.currentTimeMillis() + 3600000)):
Устанавливает время истечения срока действия токена (1 час).
signWith(key):
Подписывает токен с использованием созданного ключа.
compact():
Завершает создание токена и возвращает его в виде строки.
Парсинг токена:
Jwts.parserBuilder():
Создает новый JwtParserBuilder для парсинга JWT.
setSigningKey(key):
Устанавливает ключ для проверки подписи токена.
build():
Завершает создание парсера.
parseClaimsJws(token):
Парсит и проверяет подпись JWT.
body.subject:
Извлекает субъект (subject) из полезной нагрузки токена.
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController @RestController class JwtController { @GetMapping("/generate") fun generateToken(@RequestParam subject: String): String { return JwtUtil.createToken(subject) } @GetMapping("/validate") fun validateToken(@RequestParam token: String): String { return JwtUtil.parseToken(token) ?: "Invalid token" } }
Методы контроллера:
generateToken:
Обрабатывает GET-запросы на /generate и возвращает сгенерированный JWT токен для заданного субъекта.
validateToken:
Обрабатывает GET-запросы на /validate и проверяет подлинность переданного JWT токена, возвращая субъект или сообщение об ошибке.
Запуск приложения:
Когда приложение запускается, Spring Boot автоматически конфигурирует Spring MVC и создает маршруты, определенные в JwtController. Запросы к /generate создадут новый JWT токен, а запросы к /validate проверят переданный токен.
ReactiveAuthenticationManager:
Этот интерфейс используется для проверки подлинности пользователя на основе предоставленного токена JWT.
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken import org.springframework.security.core.Authentication import org.springframework.security.core.userdetails.User import org.springframework.security.core.userdetails.UserDetails import org.springframework.security.core.userdetails.UserDetailsService import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.security.web.server.authentication.ServerAuthenticationConverter import org.springframework.stereotype.Component import reactor.core.publisher.Mono @Component class JwtReactiveAuthenticationManager( private val jwtUtil: JwtUtil, private val userDetailsService: UserDetailsService ) : ReactiveAuthenticationManager { override fun authenticate(authentication: Authentication): Mono<Authentication> { val authToken = authentication.credentials.toString() val username = jwtUtil.extractUsername(authToken) return if (username.isNotEmpty() && jwtUtil.validateToken(authToken)) { val userDetails: UserDetails = userDetailsService.loadUserByUsername(username) val auth = UsernamePasswordAuthenticationToken(userDetails, null, userDetails.authorities) Mono.just(auth) } else { Mono.empty() } } } @Component class JwtUtil(private val secret: String) { fun extractUsername(token: String): String { return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).body.subject } fun validateToken(token: String): Boolean { try { Jwts.parser().setSigningKey(secret).parseClaimsJws(token) return true } catch (e: Exception) { return false } } }
ServerSecurityContextRepository:
Этот интерфейс используется для извлечения токена из HTTP-запроса и установки контекста безопасности.
import org.springframework.http.HttpHeaders import org.springframework.security.authentication.UsernamePasswordAuthenticationToken import org.springframework.security.core.context.SecurityContext import org.springframework.security.core.context.SecurityContextImpl import org.springframework.security.web.server.context.ServerSecurityContextRepository import org.springframework.stereotype.Component import org.springframework.web.server.ServerWebExchange import reactor.core.publisher.Mono @Component class JwtSecurityContextRepository( private val authenticationManager: ReactiveAuthenticationManager ) : ServerSecurityContextRepository { override fun save(exchange: ServerWebExchange?, context: SecurityContext?): Mono<Void> { // Saving security context is not required return Mono.empty() } override fun load(exchange: ServerWebExchange): Mono<SecurityContext> { val authHeader = exchange.request.headers.getFirst(HttpHeaders.AUTHORIZATION) return if (authHeader != null && authHeader.startsWith("Bearer ")) { val authToken = authHeader.substring(7) val auth = UsernamePasswordAuthenticationToken(authToken, authToken) authenticationManager.authenticate(auth).map { SecurityContextImpl(it) } } else { Mono.empty() } } }
Теперь, когда у нас есть ReactiveAuthenticationManager и ServerSecurityContextRepository, необходимо настроить безопасность в Spring Security.
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity import org.springframework.security.config.web.server.ServerHttpSecurity import org.springframework.security.web.server.SecurityWebFilterChain @Configuration @EnableWebFluxSecurity class SecurityConfig( private val authenticationManager: JwtReactiveAuthenticationManager, private val securityContextRepository: JwtSecurityContextRepository ) { @Bean fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { return http .csrf().disable() .authorizeExchange() .pathMatchers("/login", "/register").permitAll() .anyExchange().authenticated() .and() .authenticationManager(authenticationManager) .securityContextRepository(securityContextRepository) .build() } }
ReactiveAuthenticationManager:
JwtReactiveAuthenticationManager реализует интерфейс ReactiveAuthenticationManager.
Он извлекает имя пользователя из токена JWT и проверяет его действительность.
Если токен действителен, возвращает аутентификационный объект.
ServerSecurityContextRepository:
JwtSecurityContextRepository реализует интерфейс ServerSecurityContextRepository.
Он извлекает токен из заголовка HTTP Authorization и аутентифицирует его, используя ReactiveAuthenticationManager.
Конфигурация безопасности:
В конфигурационном классе SecurityConfig настраивается цепочка фильтров безопасности.
Определяются открытые пути (например, для входа в систему и регистрации) и защищенные пути.
Устанавливается пользовательский ReactiveAuthenticationManager и ServerSecurityContextRepository.
Еще пример:
import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Component import io.jsonwebtoken.Claims import io.jsonwebtoken.Jwts import io.jsonwebtoken.security.Keys import java.util.* import kotlin.collections.HashMap data class User( val username: String, val role: String ) @Component class JwtUtil { @Value("\${jwt.secret}") lateinit var secret: String @Value("\${jwt.expiration}") lateinit var expirationTime: String fun extractUsername(authToken: String): String { return getClaimsFromToken(authToken).subject } fun getClaimsFromToken(authToken: String): Claims { val key = Base64.getEncoder().encodeToString(secret.toByteArray()) return Jwts.parserBuilder() .setSigningKey(key) .build() .parseClaimsJws(authToken) .body } fun validateToken(authToken: String): Boolean { return getClaimsFromToken(authToken).expiration.after(Date()) } fun generateToken(user: User): String { val claims = HashMap<String, Any>() claims["role"] = listOf(user.role) val expirationSeconds = expirationTime.toLong() val creationDate = Date() val expirationDate = Date(creationDate.time + expirationSeconds * 1000) return Jwts.builder() .setClaims(claims) .setSubject(user.username) .setIssuedAt(creationDate) .setExpiration(expirationDate) .signWith(Keys.hmacShaKeyFor(secret.toByteArray())) .compact() } }
➤ Как использовать Lombok в Java
Lombok — это библиотека, которая упрощает процесс написания Java-кода, предоставляя аннотации для автоматической генерации кода, такого как геттеры, сеттеры, конструкторы и многое другое.
dependencies { compileOnly 'org.projectlombok:lombok:1.18.32' annotationProcessor 'org.projectlombok:lombok:1.18.32' testCompileOnly 'org.projectlombok:lombok:1.18.32' testAnnotationProcessor 'org.projectlombok:lombok:1.18.32' }
@Getter:
Генерирует геттер для поля.
@Setter:
Генерирует сеттер для поля.
@ToString:
Генерирует метод toString(), включающий все поля класса.
@EqualsAndHashCode:
Генерирует методы equals() и hashCode(), включающие все поля класса.
@NoArgsConstructor:
Генерирует конструктор без аргументов.
@RequiredArgsConstructor:
Генерирует конструктор для полей, помеченных как final или @NonNull.
@AllArgsConstructor:
Генерирует конструктор с аргументами для всех полей.
@Data:
Объединяет несколько аннотаций: @Getter, @Setter, @ToString, @EqualsAndHashCode и @RequiredArgsConstructor.
@Value:
Обозначает неизменяемый класс, эквивалентен комбинации @Getter, @AllArgsConstructor, @ToString, @EqualsAndHashCode и делает все поля private и final.
@Builder:
Обеспечивает паттерн Builder для класса.
@Slf4j:
Генерирует логгер для класса, используя org.slf4j.Logger.
@NonNull:
Проверяет, что поле или параметр метода не является null. Если значение null, то выбрасывается NullPointerException.
@Synchronized:
Синхронизирует метод или блок кода, аналогично ключевому слову synchronized, но использует частный монитор объекта.
@Getter(lazy=true):
Создает ленивая инициализацию для поля.
➤ Что такое Swagger / OpenAPI
OpenAPI, ранее известный как Swagger, является спецификацией для создания, описания, документирования и потребления RESTful веб-сервисов. OpenAPI Specification (OAS) позволяет разработчикам автоматизировать процессы документирования API, генерации кода клиента и сервера, а также тестирования API.
Пример спецификации OpenAPI:
openapi: 3.0.0 info: title: Simple User API description: A simple API to manage users. version: 1.0.0 servers: - url: http://localhost:8080/api paths: /users: get: summary: Get all users responses: '200': description: A list of users content: application/json: schema: type: array items: $ref: '#/components/schemas/User' post: summary: Create a new user requestBody: description: User to add required: true content: application/json: schema: $ref: '#/components/schemas/User' responses: '201': description: User created /users/{id}: get: summary: Get a user by ID parameters: - name: id in: path required: true schema: type: string responses: '200': description: A single user content: application/json: schema: $ref: '#/components/schemas/User' '404': description: User not found components: schemas: User: type: object properties: id: type: string name: type: string email: type: string
Инструменты OpenAPI:
Swagger UI:
Генерирует интерактивную документацию для API на основе спецификации OpenAPI.
Swagger Editor:
Веб-редактор для создания спецификаций OpenAPI.
Swagger Codegen:
Инструмент для генерации клиентского и серверного кода на основе спецификации OpenAPI.
OpenAPI Generator:
Альтернативный инструмент для генерации кода, поддерживающий больше языков и платформ.
Интеграция OpenAPI в Spring Boot:
Для интеграции OpenAPI в Spring Boot можно использовать Springdoc OpenAPI.
dependencies { implementation 'org.springdoc:springdoc-openapi-ui:1.6.14' }
Настройка OpenAPI:
Springdoc OpenAPI автоматически генерирует спецификацию OpenAPI на основе ваших контроллеров и моделей Spring.
import org.springframework.web.bind.annotation.* import java.util.concurrent.ConcurrentHashMap data class User( var id: String, var name: String, var email: String ) @RestController @RequestMapping("/api/users") class UserController { private val users = ConcurrentHashMap<String, User>() @GetMapping fun getAllUsers(): Map<String, User> { return users } @PostMapping fun createUser(@RequestBody user: User): User { users[user.id] = user return user } @GetMapping("/{id}") fun getUserById(@PathVariable id: String): User? { return users[id] } }
Доступ к Swagger UI:
Запустите Spring Boot приложение и перейдите по адресу http://localhost:8080/swagger-ui.html, чтобы увидеть автоматически сгенерированную документацию для вашего API.
➤ Как настроить доступ через OpenVPN
В этом примере будет рассмотрена настройка OpenVPN на сервере Ubuntu.
Установка OpenVPN:
sudo apt update sudo apt install openvpn easy-rsa
Создайте директорию для хранения конфигурационных файлов:
make-cadir ~/openvpn-ca cd ~/openvpn-ca
Настройте переменные:
nano vars
Измените строки, чтобы они соответствовали вашим параметрам:
export KEY_COUNTRY="US" export KEY_PROVINCE="CA" export KEY_CITY="SanFrancisco" export KEY_ORG="MyOrg" export KEY_EMAIL="[email protected]" export KEY_OU="MyOrgUnit"
Сгенерируйте сертификаты и ключи:
source vars ./clean-all ./build-ca ./build-key-server server ./build-dh openvpn --genkey --secret keys/ta.key
Настройте серверный конфигурационный файл:
nano /etc/openvpn/server.conf
Пример конфигурации:
port 1194 proto udp dev tun ca ca.crt cert server.crt key server.key dh dh2048.pem tls-auth ta.key 0 cipher AES-256-CBC auth SHA256 keepalive 10 120 persist-key persist-tun status /var/log/openvpn/status.log log /var/log/openvpn/openvpn.log verb 3
Запустите OpenVPN сервер:
sudo systemctl start openvpn@server sudo systemctl enable openvpn@server
Настройка клиента OpenVPN:
Создайте клиентский конфигурационный файл, который вы будете использовать для подключения к VPN.
Пример клиентского конфигурационного файла:
client dev tun proto udp remote YOUR_SERVER_IP 1194 resolv-retry infinite nobind persist-key persist-tun remote-cert-tls server cipher AES-256-CBC auth SHA256 key-direction 1 verb 3 <ca> -----BEGIN CERTIFICATE----- # Your CA certificate here -----END CERTIFICATE----- </ca> <cert> -----BEGIN CERTIFICATE----- # Your client certificate here -----END CERTIFICATE----- </cert> <key> -----BEGIN PRIVATE KEY----- # Your client private key here -----END PRIVATE KEY----- </key> <tls-auth> -----BEGIN OpenVPN Static key V1----- # Your TLS key here -----END OpenVPN Static key V1----- </tls-auth>
➤ Что такое Keycloak
Keycloak — это система управления идентификацией и доступом с открытым исходным кодом (IAM), разработанная для упрощения управления пользователями, аутентификацией и авторизацией в современных приложениях и сервисах. Keycloak предоставляет различные функции для обеспечения безопасности приложений и сервисов, такие как единый вход (SSO), многофакторная аутентификация (MFA), социальная аутентификация и другие.
Сначала установите Keycloak. Вы можете использовать Docker для этого:
docker run -p 8080:8080 -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin jboss/keycloak
dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.keycloak:keycloak-spring-boot-starter:15.0.2' }
server.port=8081 spring.main.allow-bean-definition-overriding=true keycloak.auth-server-url=http://localhost:8080/auth keycloak.realm=myrealm keycloak.resource=myclient keycloak.public-client=true keycloak.security-constraints[0].authRoles[0]=user keycloak.security-constraints[0].securityCollections[0].patterns[0]=/*
import org.keycloak.adapters.springsecurity.KeycloakSecurityComponents import org.keycloak.adapters.springsecurity.config.KeycloakWebSecurityConfigurerAdapter import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder import org.springframework.security.config.annotation.web.builders.HttpSecurity import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy @Configuration @EnableWebSecurity class SecurityConfig : KeycloakWebSecurityConfigurerAdapter() { @Autowired fun configureGlobal(auth: AuthenticationManagerBuilder) { auth.authenticationProvider(keycloakAuthenticationProvider()) } @Bean override fun sessionAuthenticationStrategy(): SessionAuthenticationStrategy { return NullAuthenticatedSessionStrategy() } @Throws(Exception::class) override fun configure(http: HttpSecurity) { super.configure(http) http.authorizeRequests() .antMatchers("/public*").permitAll() .anyRequest().authenticated() } }
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RestController @RestController class TestController { @GetMapping("/public") fun publicEndpoint(): String { return "This is a public endpoint" } @GetMapping("/secured") fun securedEndpoint(): String { return "This is a secured endpoint" } }
➤ Что такое Testcontainers
Библиотека Java, которая упрощает написание интеграционных тестов, требующих работы с внешними системами, такими как базы данных, очереди сообщений и другие сервисы, заключенные в Docker-контейнеры. Она предоставляет простой API для управления жизненным циклом Docker-контейнеров, что делает интеграционные тесты более изолированными, воспроизводимыми и надежными.
Основные особенности Testcontainers:
Легкость использования:
Testcontainers предоставляет удобный API для запуска и остановки Docker-контейнеров в тестах.
Поддержка различных систем:
Поддержка контейнеров для различных баз данных (PostgreSQL, MySQL, MongoDB и др.), систем очередей (Kafka, RabbitMQ) и других сервисов.
Жизненный цикл контейнеров:
Автоматическое управление жизненным циклом контейнеров, включая их запуск перед тестом и остановку после теста.
Сетевые возможности:
Поддержка сетевых режимов и настройки сетевых связей между контейнерами.
dependencies { implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("org.springframework.boot:spring-boot-starter-web") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") runtimeOnly("mysql:mysql-connector-java") testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.testcontainers:junit-jupiter") testImplementation("org.testcontainers:mysql") }
# application-test.properties spring.datasource.url=jdbc:tc:mysql:latest:///testdb spring.datasource.username=test spring.datasource.password=test spring.datasource.driver-class-name=org.testcontainers.jdbc.ContainerDatabaseDriver spring.jpa.hibernate.ddl-auto=update
import javax.persistence.Entity import javax.persistence.GeneratedValue import javax.persistence.GenerationType import javax.persistence.Id @Entity data class User( @Id @GeneratedValue(strategy = GenerationType.IDENTITY) val id: Long? = null, val email: String, val username: String )
import org.springframework.data.jpa.repository.JpaRepository interface UserRepository : JpaRepository<User, Long>
import org.springframework.stereotype.Service @Service class UserService(private val userRepository: UserRepository) { fun createUser(user: User): User { return userRepository.save(user) } fun getUserById(id: Long): User? { return userRepository.findById(id).orElse(null) } fun getAllUsers(): List<User> { return userRepository.findAll() } fun updateUser(id: Long, updatedUser: User): User? { val user = userRepository.findById(id).orElseThrow { RuntimeException("User not found") } user.email = updatedUser.email user.username = updatedUser.username return userRepository.save(user) } fun deleteUser(id: Long) { userRepository.deleteById(id) } }
import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest import org.springframework.test.context.junit.jupiter.SpringExtension import org.testcontainers.junit.jupiter.Container import org.testcontainers.junit.jupiter.Testcontainers import org.testcontainers.containers.MySQLContainer @ExtendWith(SpringExtension::class) @SpringBootTest @Testcontainers class UserServiceTest { @Autowired private lateinit var userService: UserService companion object { @Container val mysqlContainer = MySQLContainer<Nothing>("mysql:latest").apply { withDatabaseName("testdb") withUsername("test") withPassword("test") } } @Test fun `test createUser`() { val user = User(email = "[email protected]", username = "testuser") val savedUser = userService.createUser(user) assertNotNull(savedUser.id) assertEquals("[email protected]", savedUser.email) assertEquals("testuser", savedUser.username) } @Test fun `test getAllUsers`() { val user1 = User(email = "[email protected]", username = "testuser1") val user2 = User(email = "[email protected]", username = "testuser2") userService.createUser(user1) userService.createUser(user2) val users = userService.getAllUsers() assertEquals(2, users.size) } }
@Testcontainers:
Аннотация, обозначающая, что этот класс теста использует Testcontainers.
@Container:
Обозначает, что поле является контейнером, который будет автоматически управляться Testcontainers.
MySQLContainer:
Класс Testcontainers для запуска контейнера MySQL.
➤ Чем отличаются Jetty, Netty и Tomcat
Jetty, Netty и Tomcat — это три популярных сервера/контейнера в мире Java, каждый из которых имеет свои особенности, архитектуру и области применения. Рассмотрим основные отличия между ними:
Jetty:
Назначение:
Jetty — это HTTP-сервер и Servlet-контейнер, разработанный для хостинга веб-приложений. Он поддерживает HTTP/2, WebSocket и сервлеты.
Использование:
Часто используется в проектах, требующих встраивания сервера в приложения (например, в Spring Boot).
Подходит для разработки и тестирования, а также для хостинга полноценных веб-приложений.
Производительность:
Хорошо справляется с обработкой большого количества одновременных запросов благодаря асинхронной обработке. Однако, может быть не таким высокопроизводительным и низкоуровневым, как Netty.
Простота использования:
Легко интегрируется в проекты благодаря хорошей документации и поддержке в популярных фреймворках (например, Spring Boot).
Основные особенности:
Поддержка Servlet 3.1, HTTP/2, WebSocket, и асинхронного IO. Легко встраиваемый и модульный.
Netty:
Назначение:
Асинхронный сетевой фреймворк, предназначенный для создания высокопроизводительных, масштабируемых сетевых приложений. Подходит для реализации низкоуровневых сетевых протоколов.
Использование:
Широко используется для разработки серверов и клиентов для высоконагруженных систем, таких как распределенные системы, игровые серверы, прокси-серверы, и базы данных.
Производительность:
Высокая производительность благодаря асинхронной и неблокирующей архитектуре. Оптимизирован для минимизации задержек и максимизации пропускной способности.
Простота использования:
Требует больше низкоуровневого программирования и знания сетевых протоколов. Может быть сложнее в освоении по сравнению с Jetty и Tomcat, но предоставляет больше контроля и гибкости.
Основные особенности:
Поддержка TCP, UDP, HTTP, WebSocket, и других протоколов. Асинхронный и неблокирующий IO, поддержка NIO.
Tomcat:
Назначение:
Servlet-контейнер и HTTP-сервер, разработанный для хостинга Java сервлетов и JSP (JavaServer Pages).
Использование:
Широко используется для хостинга веб-приложений, особенно тех, которые следуют спецификации Java EE (сервлеты и JSP).
Производительность:
Хорошая производительность для хостинга веб-приложений, но не так оптимизирован для низкоуровневых сетевых операций, как Netty. Поддерживает асинхронную обработку запросов, но основная архитектура более ориентирована на традиционные веб-приложения.
Простота использования:
Хорошо документирован и поддерживается в большинстве инструментов разработки и развертывания Java. Легко интегрируется с фреймворками Java EE и Spring.
Основные особенности:
Поддержка Servlet 4.0, JSP, WebSocket, и асинхронного IO. Часто используется в комбинации с Apache HTTP Server для создания гибридных решений.
Jetty:
Хороший выбор для встраиваемых серверов и асинхронных веб-приложений.
Netty:
Оптимален для высокопроизводительных и низкоуровневых сетевых приложений.
Tomcat:
Подходит для традиционных веб-приложений и приложений, соответствующих спецификации Java EE.
➤ Что такое Netty
Асинхронный сетевой фреймворк на базе Java, предназначенный для разработки высокопроизводительных, масштабируемых серверных приложений и клиентов, таких как сетевые протоколы, веб-серверы, прокси-серверы, базы данных и другие. Он широко используется в индустрии благодаря своей высокой производительности и гибкости.
Вот пример простого сервера Echo на базе Netty, который возвращает клиенту те же сообщения, которые он получил.
dependencies { implementation("io.netty:netty-all:4.1.72.Final") }
import io.netty.channel.ChannelHandlerContext import io.netty.channel.ChannelInboundHandlerAdapter class EchoServerHandler : ChannelInboundHandlerAdapter() { override fun channelRead(ctx: ChannelHandlerContext, msg: Any) { ctx.write(msg) // Write the received message back to the sender ctx.flush() // Flush all previous written messages to the sender } override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { cause.printStackTrace() ctx.close() // Close the connection when an exception is raised } }
import io.netty.bootstrap.ServerBootstrap import io.netty.channel.ChannelFuture import io.netty.channel.ChannelInitializer import io.netty.channel.nio.NioEventLoopGroup import io.netty.channel.socket.SocketChannel import io.netty.channel.socket.nio.NioServerSocketChannel class EchoServer( private val port: Int ) { fun start() { val bossGroup = NioEventLoopGroup() val workerGroup = NioEventLoopGroup() try { val bootstrap = ServerBootstrap() bootstrap.group(bossGroup, workerGroup) .channel(NioServerSocketChannel::class.java) .childHandler(object : ChannelInitializer<SocketChannel>() { @Throws(Exception::class) override fun initChannel(ch: SocketChannel) { ch.pipeline().addLast(EchoServerHandler()) } }) val channelFuture: ChannelFuture = bootstrap.bind(port).sync() channelFuture.channel().closeFuture().sync() } finally { bossGroup.shutdownGracefully() workerGroup.shutdownGracefully() } } } fun main() { val port = 8080 EchoServer(port).start() }
EchoServerHandler:
Класс, расширяющий ChannelInboundHandlerAdapter, используется для обработки входящих сообщений.
В методе channelRead сервер просто записывает и отправляет обратно полученное сообщение.
В методе exceptionCaught обрабатываются исключения, которые могут возникнуть во время обработки сообщений.
EchoServer:
Класс, представляющий сервер. Он использует ServerBootstrap для настройки и запуска сервера.
NioEventLoopGroup используется для управления потоками ввода/вывода.
ChannelInitializer используется для настройки нового канала, добавляя в его конвейер экземпляр EchoServerHandler.
main:
В методе main создается и запускается экземпляр EchoServer на порту 8080.
➤ Что такое Nginx
Nginx (произносится как “энджин-экс”) — это высокопроизводительный HTTP-сервер и обратный прокси-сервер (reverse proxy), а также почтовый (IMAP/POP3) прокси-сервер. Он был создан Игорем Сысоевым и впервые выпущен в октябре 2004 года. Nginx стал очень популярным благодаря своей высокой производительности, малому потреблению ресурсов и гибкости.
Nginx часто используется вместе со Spring для различных задач, таких как обратное проксирование, балансировка нагрузки и обеспечение безопасности. В этом контексте Nginx выполняет роль фронтенд-сервера, который обрабатывает входящие HTTP(S) запросы и перенаправляет их на сервер Spring Boot приложения. Вот несколько примеров, как Nginx можно использовать вместе с Spring Boot:
Пример использования Nginx со Spring Boot:
Обратное проксирование:
Nginx может служить обратным прокси-сервером для перенаправления запросов на Spring Boot приложение, работающего на другом порту.
Конфигурация Nginx:
# Конфигурационный файл Nginx (обычно находится в /etc/nginx/nginx.conf или /etc/nginx/sites-available/default) server { listen 80; server_name example.com; location / { proxy_pass http://localhost:8080; # URL вашего Spring Boot приложения proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # Настройка ошибок error_page 404 /404.html; location = /404.html { root /var/www/html; } error_page 500 502 503 504 /50x.html; location = /50x.html { root /var/www/html; } }
Балансировка нагрузки:
Если у вас несколько экземпляров Spring Boot приложения, Nginx может распределять запросы между ними, обеспечивая балансировку нагрузки.
Конфигурация Nginx для балансировки нагрузки:
# Конфигурационный файл Nginx upstream spring_app { server localhost:8080; server localhost:8081; server localhost:8082; } server { listen 80; server_name example.com; location / { proxy_pass http://spring_app; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } error_page 404 /404.html; location = /404.html { root /var/www/html; } error_page 500 502 503 504 /50x.html; location = /50x.html { root /var/www/html; } }
SSL/TLS настройка:
Nginx может обрабатывать SSL/TLS сертификаты для обеспечения безопасного соединения (HTTPS) для вашего Spring Boot приложения.
Конфигурация Nginx с SSL:
# Конфигурационный файл Nginx server { listen 80; server_name example.com; return 301 https://$host$request_uri; } server { listen 443 ssl; server_name example.com; ssl_certificate /path/to/your/cert.pem; ssl_certificate_key /path/to/your/cert.key; location / { proxy_pass http://localhost:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } error_page 404 /404.html; location = /404.html { root /var/www/html; } error_page 500 502 503 504 /50x.html; location = /50x.html { root /var/www/html; } }
➤ Пример простого веб сервера
Чтобы создать простейший серверный сокет-сервлет с нуля, нужно реализовать сервер, который будет слушать на определенном порту и обрабатывать HTTP-запросы. В данном примере мы создадим сервер, который сможет обрабатывать GET и POST запросы.
plugins { id 'org.jetbrains.kotlin.jvm' version '1.8.0' id 'application' } repositories { mavenCentral() } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib" } application { mainClass = 'SimpleHttpServerKt' }
Используя Socket:
import java.io.* import java.net.ServerSocket import java.net.Socket fun main() { val server = SimpleHttpServer() server.start(8080) } class SimpleHttpServer { fun start(port: Int) { val serverSocket = ServerSocket(port) println("Server is listening on port $port") while (true) { val socket = serverSocket.accept() Thread(ClientHandler(socket)).start() } } private class ClientHandler(private val socket: Socket) : Runnable { override fun run() { socket.use { val input = it.getInputStream() val reader = BufferedReader(InputStreamReader(input)) val output = it.getOutputStream() val writer = PrintWriter(output, true) val request = reader.readLine() val tokens = request.split(" ") val method = tokens[0] val path = tokens[1] when (method) { "GET" -> handleGetRequest(writer, path) "POST" -> handlePostRequest(reader, writer, path) else -> { writer.println("HTTP/1.1 405 Method Not Allowed") writer.println("Content-Length: 0") writer.println() } } } } private fun handleGetRequest(writer: PrintWriter, path: String) { writer.println("HTTP/1.1 200 OK") writer.println("Content-Type: text/html") writer.println() writer.println("<html><body>") writer.println("<h1>GET request received</h1>") writer.println("<p>Path: $path</p>") writer.println("</body></html>") } private fun handlePostRequest(reader: BufferedReader, writer: PrintWriter, path: String) { val payload = StringBuilder() var line: String? while (reader.readLine().also { line = it } != "") { payload.append(line).append("\n") } writer.println("HTTP/1.1 200 OK") writer.println("Content-Type: text/html") writer.println() writer.println("<html><body>") writer.println("<h1>POST request received</h1>") writer.println("<p>Path: $path</p>") writer.println("<p>Payload: $payload</p>") writer.println("</body></html>") } } }
Используя AsynchronousServerSocketChannel:
import java.net.InetSocketAddress import java.nio.ByteBuffer import java.nio.channels.AsynchronousServerSocketChannel import java.nio.channels.AsynchronousSocketChannel import java.nio.channels.CompletionHandler import java.nio.charset.Charset fun main() { val server = SimpleAsyncHttpServer() server.start(8080) } class SimpleAsyncHttpServer { fun start(port: Int) { val serverChannel = AsynchronousServerSocketChannel.open() serverChannel.bind(InetSocketAddress(port)) println("Server is listening on port $port") serverChannel.accept(null, object : CompletionHandler<AsynchronousSocketChannel, Void?> { override fun completed(clientChannel: AsynchronousSocketChannel?, attachment: Void?) { serverChannel.accept(null, this) handleClient(clientChannel) } override fun failed(exc: Throwable?, attachment: Void?) { println("Failed to accept connection: ${exc?.message}") } }) // Keep the main thread alive to accept connections Thread.currentThread().join() } private fun handleClient(clientChannel: AsynchronousSocketChannel?) { if (clientChannel != null && clientChannel.isOpen) { val buffer = ByteBuffer.allocate(1024) clientChannel.read(buffer, buffer, object : CompletionHandler<Int, ByteBuffer> { override fun completed(result: Int?, attachment: ByteBuffer?) { if (result != null && result > 0) { attachment?.flip() val request = Charset.defaultCharset().decode(attachment).toString() println("Received request: $request") val response = buildResponse(request) writeResponse(clientChannel, response) attachment?.clear() clientChannel.read(attachment, attachment, this) } else { clientChannel.close() } } override fun failed(exc: Throwable?, attachment: ByteBuffer?) { println("Failed to read from client: ${exc?.message}") clientChannel.close() } }) } } private fun buildResponse(request: String): String { val lines = request.split("\r\n") val requestLine = lines.firstOrNull() val method = requestLine?.split(" ")?.get(0) ?: "" val path = requestLine?.split(" ")?.get(1) ?: "" return when (method) { "GET" -> { "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<h1>GET request received</h1><p>Path: $path</p>" } "POST" -> { "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<h1>POST request received</h1><p>Path: $path</p>" } else -> { "HTTP/1.1 405 Method Not Allowed\r\nContent-Length: 0\r\n\r\n" } } } private fun writeResponse(clientChannel: AsynchronousSocketChannel, response: String) { val buffer = ByteBuffer.wrap(response.toByteArray(Charset.defaultCharset())) clientChannel.write(buffer, buffer, object : CompletionHandler<Int, ByteBuffer> { override fun completed(result: Int?, attachment: ByteBuffer?) { if (attachment?.hasRemaining() == true) { clientChannel.write(attachment, attachment, this) } else { println("Response sent to client") clientChannel.close() } } override fun failed(exc: Throwable?, attachment: ByteBuffer?) { println("Failed to write to client: ${exc?.message}") clientChannel.close() } }) } }