Roman Kryvolapov Engineering Blog

Java Spring — Концепції

Що таке 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 — це абревіатура, що позначає п'ять принципів об'єктно-орієнтованого програмування та дизайну, які допомагають створювати гнучкі, масштабовані та легко підтримувані програмні системи.

Single Responsibility Principle . (принцип єдиної відповідальності)

Клас повинен мати лише одну причину для зміни, повинен виконувати лише одне завдання.

Поганий приклад:

class UserService {
fun registerUser(user: User) {
// Реєстрація користувача
println("User registered: ${user.name}")
sendWelcomeEmail(user)
}
private fun sendWelcomeEmail(user: User) {
// Надсилання привітального листа
println("Welcome email sent to ${user.email}")
}
}

Що тут погано:
Клас UserService виконує два завдання: реєстрацію користувача та надсилання привітального листа. Це порушує принцип єдиної відповідальності, оскільки зміна логіки надсилання листів потребуватиме зміни UserService.

Хороший приклад:

class UserService(private val emailService: EmailService) {
fun registerUser(user: User) {
// Реєстрація користувача
println("User registered: ${user.name}")
emailService.sendWelcomeEmail(user)
}
}
class EmailService {
fun sendWelcomeEmail(user: User) {
// Надсилання привітального листа
println("Welcome email sent to ${user.email}")
}
}

Що тут добре:
Клас UserService відповідає лише за реєстрацію користувача, а EmailService - за надсилання листів. Це відповідає принципу єдиної відповідальності.

Open-Closed Principle . (принцип відкритості/закритості)

Клас має бути відкритий для розширення й закритий для модифікації.

Поганий приклад:

class DiscountService {
fun applyDiscount(price: Double, customerType: String): Double {
return if (customerType == "Regular") {
price * 0.9
} else if (customerType == "Premium") {
price * 0.8
} else {
price
}
}
}

Що тут погано:
Для додавання нового типу клієнта потрібно буде змінити клас DiscountService, що порушує принцип відкритості/закритості.

Хороший приклад:

interface DiscountPolicy {
fun applyDiscount(price: Double): Double
}
class RegularDiscountPolicy : DiscountPolicy {
override fun applyDiscount(price: Double): Double {
return price * 0.9
}
}
class PremiumDiscountPolicy : DiscountPolicy {
override fun applyDiscount(price: Double): Double {
return price * 0.8
}
}
class DiscountService(private val discountPolicy: DiscountPolicy) {
fun applyDiscount(price: Double): Double {
return discountPolicy.applyDiscount(price)
}
}

Що тут добре:
Клас DiscountService відкритий для розширення (можна додати нові типи знижок), але закритий для модифікації (не потрібно змінювати наявний код для додавання нових знижок).

Liskov Substitution Principle . (принцип підстановки Барбари Лісков)

Код, розрахований на базовий клас, має коректно працювати з будь-яким його нащадком.

Поганий приклад:

open class Bird {
open fun fly() {
println("Bird is flying")
}
}
class Penguin : Bird() {
override fun fly() {
throw UnsupportedOperationException("Penguins can't fly")
}
}

Що тут погано:
Підклас Penguin порушує принцип LSP, оскільки він не може замінити базовий клас Bird без зміни логіки (метод fly не застосовний до пінгвінів).

Хороший приклад:

open class Bird {
open fun move() {
println("Bird is moving")
}
}
class Penguin : Bird() {
override fun move() {
println("Penguin is swimming")
}
}

Що тут добре:
Підклас Penguin замінює базовий клас Bird, не порушуючи його логіки. Метод move може означати різну поведінку для різних птахів.

Interface Segregation Principle . (принцип розділення інтерфейсів)

Інтерфейс має містити лише потрібні клієнту методи.

Поганий приклад:

interface Worker {
fun work()
fun eat()
}
class HumanWorker : Worker {
override fun work() {
println("Human is working")
}
override fun eat() {
println("Human is eating")
}
}
class RobotWorker : Worker {
override fun work() {
println("Robot is working")
}
override fun eat() {
throw UnsupportedOperationException("Robots don't eat")
}
}

Що тут погано:
Інтерфейс Worker містить методи, які не застосовні до всіх його реалізацій. Роботи не повинні реалізовувати метод eat.

Хороший приклад:

interface Worker {
fun work()
}
interface Eater {
fun eat()
}
class HumanWorker : Worker, Eater {
override fun work() {
println("Human is working")
}
override fun eat() {
println("Human is eating")
}
}
class RobotWorker : Worker {
override fun work() {
println("Robot is working")
}
}

Що тут добре:
Інтерфейси Worker і Eater розділені за функціональністю. Класи реалізують лише ті інтерфейси, які їм справді потрібні.

Dependency Inversion Principle . (принцип інверсії залежності)

Високорівневий код працює з інтерфейсами й не повинен залежати від їхньої реалізації.

Поганий приклад:

class EmailService {
fun sendEmail(message: String) {
println("Sending email: $message")
}
}
class NotificationService {
private val emailService = EmailService()
fun notify(message: String) {
emailService.sendEmail(message)
}
}

Що тут погано:
NotificationService жорстко залежить від конкретного класу EmailService, що робить його важко змінюваним і тестованим.

Хороший приклад:

interface MessageService {
fun sendMessage(message: String)
}
class EmailService : MessageService {
override fun sendMessage(message: String) {
println("Sending email: $message")
}
}
class SmsService : MessageService {
override fun sendMessage(message: String) {
println("Sending SMS: $message")
}
}
class NotificationService(private val messageService: MessageService) {
fun notify(message: String) {
messageService.sendMessage(message)
}
}

Що тут добре:
NotificationService залежить від абстракції MessageService, а не від конкретної реалізації. Це дозволяє легко замінювати реалізацію та покращує тестованість коду.

Як використовувати підхід Чистої архітектури (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 працює так само, як і 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()
}
})
}
}

Copyright: Roman Kryvolapov