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 и завършва или отменя поръчката.

Kotlin
import org.springframework.stereotype.Service
@Service
class Service1 {
fun performTransaction1() {
// Логика за T1
}
fun compensateTransaction1() {
// Логика за компенсация на T1
}
}
Kotlin
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).

Принципи на работа на Rollback Pattern:

Компенсиращи действия:
За всяка операция, която променя състоянието на системата, се определя компенсиращо действие. Това действие трябва да отмени промените, направени от първоначалната операция.

Идентифициране на неуспехите:
Системата трябва да умее да открива кога дадена операция е неуспешна и да стартира компенсиращите действия.

Изолирани операции:
Операциите трябва да са независими и изолирани, за да не засегнат компенсиращите действия други операции.

Транзакционност:
Прилага се в системи, където е необходимо да се осигури атомарност на операциите. Това означава, че всички операции в рамките на една транзакция или се изпълняват изцяло, или изобщо не се изпълняват.

Журналиране на състоянията:
Често изисква системата да води журнал на промените или на състоянията преди началото на транзакцията, за да може да се върне предишното състояние.

Схема на работа на Rollback Pattern:
Клиентът инициира създаването на поръчка.
Order Service създава поръчката и публикува събитието OrderCreatedEvent.
Payment Service обработва събитието OrderCreatedEvent и се опитва да извърши плащане.
Ако плащането е успешно, Payment Service публикува събитието PaymentCompletedEvent.
Ако плащането е неуспешно, Payment Service публикува събитието PaymentFailedEvent.
Order Service обработва събитието PaymentFailedEvent и отменя поръчката, като публикува събитието OrderCancelledEvent.

Kotlin
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
@Service
class Service1 {
@Transactional
fun performTransaction1() {
// Логика за T1
}
@Transactional
fun rollbackTransaction1() {
// Логика за отмяна на T1
}
}
// По аналогия за Service3 и Service4
Kotlin
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 . (принцип на единствената отговорност)

Класът трябва да има само една причина за промяна и да изпълнява само една задача.

Лош пример:

Kotlin
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.

Добър пример:

Kotlin
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 . (принцип на отвореността/затвореността)

Класът трябва да е отворен за разширение и затворен за модификация.

Лош пример:

Kotlin
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, което нарушава принципа на отвореността/затвореността.

Добър пример:

Kotlin
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 . (принцип на заместването на Барбара Лисков)

Кодът, разчитащ на базов клас, трябва да работи коректно с всеки негов наследник.

Лош пример:

Kotlin
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 не е приложим за пингвините).

Добър пример:

Kotlin
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 . (принцип на разделянето на интерфейсите)

Интерфейсът трябва да съдържа само методите, които са нужни на клиента.

Лош пример:

Kotlin
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.

Добър пример:

Kotlin
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 . (принцип на инверсията на зависимостите)

Кодът на високо ниво работи с интерфейси и не трябва да зависи от тяхната реализация.

Лош пример:

Kotlin
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, което го прави труден за промяна и за тестване.

Добър пример:

Kotlin
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 за продукти.
Именуване: Ресурсите обикновено се именуват в множествено число (например /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 не променят състоянието на сървъра и се смятат за идемпотентни (т.е. повторната заявка няма да доведе до промени).

Пример за използване:

HTTP
GET /users

Връща списък на всички потребители.

HTTP
GET /users/1

Връща данните на потребителя с идентификатор 1.

POST

Предназначение:
Създаване на нов ресурс.

Описание:
Методът POST се използва за изпращане на данни към сървъра с цел създаване на нов ресурс. Този метод не е идемпотентен – при повторни заявки може да се създава нов ресурс.

Пример за използване:

HTTP, JSON
POST /users
Content-Type: application/json
{
"name": "Alice",
"email": "[email protected]"
}

Създава нов потребител с име „Alice“.

PUT

Предназначение:
Пълно обновяване на ресурс.

Описание:
Методът PUT се използва за замяна на съществуващ ресурс с нови данни. Ако ресурс с указания идентификатор съществува, той ще бъде обновен, а ако не – може да бъде създаден (в зависимост от реализацията). Методът е идемпотентен – повторното изпълнение на заявката няма да промени резултата.

Пример за използване:

HTTP, JSON
PUT /users/1
Content-Type: application/json
{
"name": "Bob",
"email": "[email protected]"
}

Обновява данните на потребителя с идентификатор 1, като заменя всички полета с новите стойности.

PATCH

Предназначение:
Частично обновяване на ресурс.

Описание:
Методът PATCH се използва за частично обновяване на ресурс. За разлика от PUT, който обновява ресурса изцяло, PATCH обновява само полетата, които са предадени в заявката. Този метод също може да е идемпотентен, ако е реализиран по съответния начин.

Пример за използване:

HTTP, JSON
PATCH /users/1
Content-Type: application/json
{
"email": "[email protected]"
}

Обновява само полето email на потребителя с идентификатор 1.

DELETE

Предназначение:
Изтриване на ресурс.

Описание:
Методът DELETE се използва за изтриване на ресурс от сървъра. Ако ресурсът е изтрит успешно, сървърът трябва да върне статус за успешно изпълнение (например 200 OK или 204 No Content). Този метод също е идемпотентен – повторното изпълнение на заявката няма да промени резултата.

Пример за използване:

HTTP
DELETE /users/1

Изтрива потребителя с идентификатор 1.

OPTIONS

Предназначение:
Заявка за разрешените методи за ресурс.

Описание:
Методът OPTIONS се използва за заявяване на наличните HTTP методи за определен ресурс или за цялата услуга. Отговорът съдържа информация какви методи са разрешени на сървъра за този ресурс. Често се използва за проверка на поддръжката на CORS (Cross-Origin Resource Sharing).

Пример за използване:

HTTP
OPTIONS /users

Връща методите, които могат да бъдат изпълнени над /users (например GET, POST и т.н.).

Предназначение:
Получаване на хедърите на отговора без тяло.

Описание:
Методът HEAD работи по същия начин като GET, с тази разлика, че в отговора се връщат само хедърите, а тяло на отговора няма. Този метод е полезен за проверка дали даден ресурс съществува или за получаване на метаданни, като размер или последно променени данни.

Пример за използване:

HTTP
HEAD /users/1

Връща хедърите за заявката за потребителя с идентификатор 1

TRACE

Предназначение:
Диагностика на мрежовия път.

Описание:
Този метод се използва за тестване и дебъгване на мрежова връзка. Сървърът трябва да върне заявката обратно във вида, в който е била получена. Използва се за диагностика на маршрутите между клиента и сървъра.

Пример за използване:

HTTP
TRACE /users

CONNECT

Предназначение:
Установяване на тунел за защитена връзка.

Описание:
Този метод се използва за създаване на тунел през сървъра, обикновено за сигурна връзка чрез SSL/TLS. На практика този метод най-често се прилага при реализиране на прокси сървъри.

Пример за използване:

HTTP
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:
интерфейс, който се използва за предаване на данни от контролера към изгледа (view). Той позволява да се добавят атрибути, които ще бъдат достъпни в шаблона на изгледа. Model обикновено се използва в методите на контролера. В този пример методът greeting добавя атрибута message в Model, който след това ще бъде достъпен в изгледа с име greeting.

Kotlin
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.

Kotlin
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 може да търси изгледи във файловата система, в classpath или да използва други механизми. Този конфигурационен клас настройва InternalResourceViewResolver, който търси JSP файлове в указаната директория.

Properties
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp
Kotlin
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, тъй като прави зависимостите явно видими и допринася за неизменяемостта на обектите.

Java
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);
}
}
Kotlin
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 (Инжектиране чрез сетъри):
Зависимостите се предават през сетъри или други методи на класа.
Полезно за опционални зависимости и позволява лесно да се променят зависимостите след създаването на обекта.

Java
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);
}
}
Kotlin
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 (Инжектиране чрез полета):
Зависимостите се предават директно в полетата на класа.
Прост начин за инжектиране, но по-малко предпочитан заради недостатъци по отношение на тестваемостта и управляемостта.

Java
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);
}
}
Kotlin
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):

Java
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan(basePackages = "com.example")
public class AppConfig {}

Използване на XML конфигурация:

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 приложения.

Groovy
dependencies {
implementation 'io.github.resilience4j:resilience4j-spring-boot2:1.7.1'
}
YAML
resilience4j.circuitbreaker:
instances:
myService:
registerHealthIndicator: true
slidingWindowSize: 10
failureRateThreshold: 50
waitDurationInOpenState: 10000
permittedNumberOfCallsInHalfOpenState: 3
Kotlin
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:

Kotlin
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")
}
Kotlin
import com.googlecode.jsonrpc4j.JsonRpcService
@JsonRpcService("/api")
interface CalculatorService {
fun subtract(a: Int, b: Int): Int
}
Kotlin
import org.springframework.stereotype.Service
@Service
class CalculatorServiceImpl : CalculatorService {
override fun subtract(a: Int, b: Int): Int {
return a - b
}
}
Kotlin
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()
}
}
Kotlin
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 това често са същности, които отразяват таблиците на базата данни.

Kotlin
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 операции (създаване, четене, обновяване, изтриване).

Kotlin
import org.springframework.data.jpa.repository.JpaRepository
interface UserRepository : JpaRepository<User, Long> {
fun findByUsername(username: String): User?
}

Услуга (Service):
Услугата съдържа бизнес логиката на приложението. Тя обработва данните, получени от хранилищата, и ги предава на контролерите.

Kotlin
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 заявките и връща отговори. Той използва услугите, за да изпълнява бизнес логиката и да подготвя данните за изгледите.

Kotlin
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):
Конфигурацията за сигурност отговаря за настройването на достъпа и оторизацията.

Kotlin
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.

HTML
<!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>

Главен клас:

Kotlin
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

Кое е по-добро в Spring – анотациите или XML

В Spring Framework съществуват два основни подхода към конфигурирането и управлението на зависимостите: използване на анотации и използване на XML конфигурация. И двата подхода имат свои предимства и недостатъци, а изборът между тях зависи от конкретните изисквания на проекта и от предпочитанията на разработчика.

Анотации в Spring:
Анотациите в Spring позволяват зависимостите да се конфигурират и управляват директно в кода, което прави конфигурацията по-четима и удобна за поддръжка.

XML конфигурация в Spring:
XML конфигурацията позволява бийновете и техните зависимости да се описват в отделен XML файл, като конфигурацията се отделя от кода. Това я прави по-декларативна и удобна за промени без промяна на кода.

Основни елементи на XML конфигурацията:

<bean>:
Определя бийн и неговите свойства.

<property>:
Определя свойствата на бийна.

<constructor-arg>:
Определя аргументите на конструктора на бийна.

context:component-scan:
Сканира указаните пакети за анотирани класове.

Пример за използване на XML конфигурация:

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 и предоставя реактивни възможности за създаване на уеб приложения, взаимодействие с бази данни и други задачи.

Kotlin
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
)
}
Kotlin
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
)
}
Kotlin
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<Void>).

Основни методи на 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<Void>

Kotlin
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 с помощта на предикат.

Kotlin
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:

Kotlin
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:

Kotlin
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:

Kotlin
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 и последващо обединяване:

Kotlin
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 се извиква от контейнера преди унищожаването на сервлета. Този метод се изпълнява веднъж и служи за освобождаване на ресурси.

Пример за прост сервлет:

Kotlin
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:

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 и т.н. За целта трябва да се припокрият съответните методи в класа на сервлета.

Kotlin
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) {
}
}

Използване на параметри на заявката и сесии:
Сервлетите могат да извличат параметри на заявката и да работят със сесии, за да запазват данни между заявките.
Извличане на параметри на заявката

Kotlin
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>")
}
}
}

Работа със сесии:

Kotlin
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.

Kotlin
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

HTML
<!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 токена във формата.

HTML
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />

Обработка на AJAX заявки с CSRF:
При AJAX заявки CSRF токенът трябва да се включи в хедъра на заявката.
Пример за AJAX заявка с CSRF токен

JavaScript
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:

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:
Gauge, който следи текущото използване на CPU.

request_duration_seconds:
Хистограма, която следи продължителността на заявките.

response_size_bytes:
Summary, което следи размерите на HTTP отговорите.

Как се използва форматът Prometheus:

Groovy
dependencies {
implementation 'io.micrometer:micrometer-registry-prometheus'
}
Kotlin
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") }
}

Метод за експониране на метриките:

Kotlin
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”) в конфигурационния клас.

Пример за жизнения цикъл на бийн:

Kotlin
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("8. DisposableBean: Метод destroy()")
}
fun customDestroy() {
println("9. @Bean(destroyMethod): Метод customDestroy()")
}
}
Kotlin
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()
}
}
Kotlin
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 съобщение:

XML
<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:

Protobuf
syntax = "proto3";
package com.example.demo;
message User {
int32 id = 1;
string name = 2;
string email = 3;
}
Kotlin
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")
}
}
}
Kotlin
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)
}
}
Shell
# Получаване на сериализирания потребител
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:
Версията на кода.

Java
/**
* Класът представлява модел на потребител в системата.
* <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.

Shell
javadoc -d doc -sourcepath src com.example.demo

-d doc:
указва директорията за изходните HTML файлове.

sourcepath src:
указва пътя към изходните файлове.

com.example.demo:
указва пакета, за който ще се генерира документацията.

Интеграция на Javadoc с Gradle:

Groovy
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

XML
<modelVersion>4.0.0</modelVersion>

Group ID:
Уникален идентификатор на организацията или групата, която създава проекта

XML
<groupId>com.example</groupId>

Artifact ID:
Уникален идентификатор на самия проект.

XML
<artifactId>my-app</artifactId>

Version:
Версия на проекта.

XML
<version>1.0-SNAPSHOT</version>

Packaging:
Тип на пакетирането на проекта (например jar, war, pom и т.н.).

XML
<packaging>jar</packaging>

Name:
Четимо за хора име на проекта.

XML
<name>My App</name>

Description:
Описание на проекта.

XML
<description>My Maven Project</description>

Dependencies:
Раздел, който описва зависимостите на проекта от други библиотеки или модули.

XML
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>

Repositories:
Раздел, който указва хранилищата, от които Maven ще изтегля зависимостите.

XML
<repositories>
<repository>
<id>central</id>
<url>https://repo.maven.apache.org/maven2</url>
</repository>
</repositories>

Build:
Раздел, който описва инструкциите за билд на проекта.

XML
<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.

XML
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>

От какво се състои build.gradle

Plugins:
Подключване на плъгини, например за компилиране на Java код.

Groovy
plugins {
id 'java'
}

Group и Version:
Задаване на идентификатор и версия на проекта.

Groovy
group 'com.example'
version '1.0-SNAPSHOT'

Source Compatibility:
Задаване на версията на езика Java.

Groovy
sourceCompatibility = 1.8

Repositories:
Указване на хранилищата, от които ще се изтеглят зависимостите.

Groovy
repositories {
mavenCentral()
}

Dependencies:
Определяне на зависимостите на проекта.

Groovy
dependencies {
testImplementation 'junit:junit:4.12'
}

Test:
Настройка на задачите за тестване.

Groovy
test {
useJUnitPlatform()
}

Tasks with Type:
Настройка на задачите за компилиране.

Groovy
tasks.withType(JavaCompile) {
options.encoding = 'UTF-8'
sourceCompatibility = '1.8'
targetCompatibility = '1.8'
}

Jar:
Настройка на задачата за пакетиране в JAR файл.

Groovy
jar {
manifest {
attributes(
'Implementation-Title': 'My App',
'Implementation-Version': version
)
}
}

Какво е Supplier

В Java Supplier е функционален интерфейс от пакета java.util.function, който представлява функция, която не приема аргументи и връща резултат. Supplier се използва за ленива инициализация, за генериране на стойности при поискване и за реализиране на различни шаблони за проектиране, като фабрика.

Основни особености на Supplier:

Функционален интерфейс:
Интерфейс с един-единствен абстрактен метод.

Метод get:
Абстрактен метод, който не приема аргументи и връща стойност от тип T.

Пример за използване на Supplier:

Определяне на Supplier:

Java
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 за ленива инициализация:

Java
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 в колекции:

Java
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 също може да бъде полезен за ленива инициализация на бийнове или за фабрични методи.

Java
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 по подразбиране са final и неизменяеми.
Kotlin data class: Полетата по подразбиране не са final и могат да се променят. Може обаче да се декларират като 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: Използват се за създаване на прости неизменяеми обекти с данни. Полетата трябва да са декларирани в параметрите на заглавната част на 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.

Kotlin
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")
}
Kotlin
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) от полезния товар на токена.

Kotlin
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 токен.

Kotlin
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 заявката и за задаване на контекста за сигурност.

Kotlin
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.

Kotlin
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.

Още един пример:

Kotlin
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 код, като предоставя анотации за автоматично генериране на код – например гетъри, сетъри, конструктори и още много други.

Groovy
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:

YAML
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.

Groovy
dependencies {
implementation 'org.springdoc:springdoc-openapi-ui:1.6.14'
}

Настройка на OpenAPI:
Springdoc OpenAPI автоматично генерира спецификацията на OpenAPI въз основа на вашите контролери и модели в Spring.

Kotlin
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:

Shell
sudo apt update
sudo apt install openvpn easy-rsa

Създайте директория за съхранение на конфигурационните файлове:

Shell
make-cadir ~/openvpn-ca
cd ~/openvpn-ca

Настройте променливите:

Shell
nano vars

Променете редовете, така че да отговарят на вашите параметри:

Shell
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"

Генерирайте сертификатите и ключовете:

Shell
source vars
./clean-all
./build-ca
./build-key-server server
./build-dh
openvpn --genkey --secret keys/ta.key

Настройте конфигурационния файл на сървъра:

Shell
nano /etc/openvpn/server.conf

Пример за конфигурация:

OpenVPN
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 сървъра:

Shell
sudo systemctl start openvpn@server
sudo systemctl enable openvpn@server

Настройка на клиента на OpenVPN:
Създайте клиентски конфигурационен файл, който ще използвате за свързване към VPN.

Пример за клиентски конфигурационен файл:

OpenVPN
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:

Shell
docker run -p 8080:8080 -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin jboss/keycloak
Groovy
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'
}
Properties
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]=/*
Kotlin
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()
}
}
Kotlin
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) и други услуги.

Жизнен цикъл на контейнерите:
Автоматично управление на жизнения цикъл на контейнерите, включително стартирането им преди теста и спирането им след теста.

Мрежови възможности:
Поддръжка на мрежови режими и настройка на мрежови връзки между контейнерите.

Kotlin
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")
}
Properties · 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
Kotlin
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
)
Kotlin
import org.springframework.data.jpa.repository.JpaRepository
interface UserRepository : JpaRepository<User, Long>
Kotlin
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)
}
}
Kotlin
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, който връща на клиента същите съобщения, които е получил.

Kotlin
dependencies {
implementation("io.netty:netty-all:4.1.72.Final")
}
Kotlin
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
}
}
Kotlin
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
# Конфигурационен файл на 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
# Конфигурационен файл на 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
# Конфигурационен файл на 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 заявки.

Groovy
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:

Kotlin
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:

Kotlin
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