Roman Kryvolapov Engineering Blog

Java Spring — Компоненти Spring

Які можуть бути параметри в application.properties

Конфігурація бази даних:

# URL підключення до бази даних
spring.datasource.url=jdbc:h2:mem:testdb
# Драйвер бази даних
spring.datasource.driverClassName=org.h2.Driver
# Ім'я користувача бази даних
spring.datasource.username=sa
# Пароль бази даних
spring.datasource.password=password
# Платформа бази даних (використовується Hibernate)
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
# Автоматичне створення та оновлення схеми бази даних
spring.jpa.hibernate.ddl-auto=update

Конфігурація H2 консолі:

# Увімкнення консолі H2
spring.h2.console.enabled=true
# Шлях доступу до консолі H2
spring.h2.console.path=/h2-console

Конфігурація сервера:

# Порт, на якому запускається сервер
server.port=8080
# Кодування за замовчуванням
server.servlet.encoding.charset=UTF-8
server.servlet.encoding.enabled=true
server.servlet.encoding.force=true

Конфігурація безпеки:

# Шлях до сторінки логіна
spring.security.user.name=user
# Пароль для сторінки логіна
spring.security.user.password=secret
# Роль користувача
spring.security.user.roles=USER

Конфігурація кешування:

# Увімкнення кешування
spring.cache.type=simple

Конфігурація логування:

# Рівень логування для застосунку
logging.level.root=INFO
logging.level.org.springframework.web=DEBUG
# Шлях до файлу логів
logging.file.name=logs/spring-boot-application.log
# Формат виведення логів
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} - %msg%n
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss} - %msg%n

Конфігурація інтернаціоналізації:

# Локаль за замовчуванням
spring.mvc.locale=ru_RU
# Увімкнення автоматичного визначення локалі
spring.mvc.locale-resolver=accept-header

Конфігурація надсилання електронної пошти:

# SMTP-сервер
spring.mail.host=smtp.example.com
# Порт SMTP
spring.mail.port=587
# Ім'я користувача для SMTP
spring.mail.username=myemail@example.com
# Пароль для SMTP
spring.mail.password=password
# Протокол
spring.mail.protocol=smtp
# Шлях до шаблонів листів
spring.mail.templates.path=classpath:/templates/

Конфігурація параметрів застосунку:

# Приклад користувацького параметра
myapp.custom-property=value

Як використовувати WebSocket у Spring

Додайте залежності для Spring WebSocket у ваш build.gradle.kts (якщо ви використовуєте Kotlin DSL для Gradle):

dependencies {
implementation("org.springframework.boot:spring-boot-starter-websocket")
implementation("org.springframework.boot:spring-boot-starter-web")
}

Конфігурація WebSocket

import org.springframework.context.annotation.Configuration
import org.springframework.web.socket.config.annotation.EnableWebSocket
import org.springframework.web.socket.config.annotation.WebSocketConfigurer
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry
@Configuration
@EnableWebSocket
class WebSocketConfig(private val webSocketHandler: MyWebSocketHandler) : WebSocketConfigurer {
override fun registerWebSocketHandlers(registry: WebSocketHandlerRegistry) {
registry.addHandler(webSocketHandler, "/ws").setAllowedOrigins("*")
}
}

Реалізація обробника WebSocket

import org.springframework.stereotype.Component
import org.springframework.web.socket.TextMessage
import org.springframework.web.socket.WebSocketSession
import org.springframework.web.socket.handler.TextWebSocketHandler
@Component
class MyWebSocketHandler : TextWebSocketHandler() {
override fun handleTextMessage(session: WebSocketSession, message: TextMessage) {
val payload = message.payload
println("Received: $payload")
val response = TextMessage("Server received: $payload")
session.sendMessage(response)
}
}

Як використовувати GraphQL у Spring

Додавання залежностей

dependencies {
implementation("com.graphql-java-kickstart:graphql-spring-boot-starter:11.1.0")
implementation("com.graphql-java-kickstart:graphiql-spring-boot-starter:11.1.0")
implementation("com.graphql-java-kickstart:altair-spring-boot-starter:11.1.0")
implementation("com.graphql-java-kickstart:voyager-spring-boot-starter:11.1.0")
implementation("com.graphql-java-kickstart:graphql-java-tools:11.1.0")
}

Визначення схеми GraphQL:
Створіть файл schema.graphqls у теці src/main/resources.

type Query {
getUser(id: ID!): User
getAllUsers: [User]
}
type Mutation {
createUser(input: CreateUserInput): User
}
type User {
id: ID!
name: String!
email: String!
}
input CreateUserInput {
name: String!
email: String!
}
data class User(
val id: Long,
val name: String,
val email: String
)
import org.springframework.stereotype.Repository
@Repository
class UserRepository {
private val users = mutableListOf<User>()
private var idCounter = 1L
fun getUserById(id: Long): User? {
return users.find { it.id == id }
}
fun getAllUsers(): List<User> {
return users
}
fun createUser(name: String, email: String): User {
val user = User(id = idCounter++, name = name, email = email)
users.add(user)
return user
}
}

GraphQL Resolvers

import com.coxautodev.graphql.tools.GraphQLQueryResolver
import org.springframework.stereotype.Component
@Component
class UserQueryResolver(private val userRepository: UserRepository) : GraphQLQueryResolver {
fun getUser(id: Long): User? {
return userRepository.getUserById(id)
}
fun getAllUsers(): List<User> {
return userRepository.getAllUsers()
}
}
import com.coxautodev.graphql.tools.GraphQLMutationResolver
import org.springframework.stereotype.Component
data class CreateUserInput(val name: String, val email: String)
@Component
class UserMutationResolver(private val userRepository: UserRepository) : GraphQLMutationResolver {
fun createUser(input: CreateUserInput): User {
return userRepository.createUser(input.name, input.email)
}
}

Приклади запитів і мутацій

Запит на отримання користувача за ID

query {
getUser(id: 1) {
id
name
email
}
}

Запит на отримання всіх користувачів

query {
getAllUsers {
id
name
email
}
}

Мутація для створення нового користувача

mutation {
createUser(input: { name: "John Doe", email: "john.doe@example.com" }) {
id
name
email
}
}

Як використовувати OAuth 2.0 у Spring

Налаштування OAuth 2.0 у Spring Boot дозволяє вашому застосунку безпечно взаємодіяти зі сторонніми сервісами, такими як Google, Facebook та іншими провайдерами OAuth. У цьому прикладі я покажу, як налаштувати Spring Boot застосунок для автентифікації користувачів через OAuth 2.0 з використанням провайдера Google.

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
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'
}
spring.security.oauth2.client.registration.google.client-id=YOUR_CLIENT_ID
spring.security.oauth2.client.registration.google.client-secret=YOUR_CLIENT_SECRET
spring.security.oauth2.client.registration.google.scope=profile,email
spring.security.oauth2.client.registration.google.redirect-uri={baseUrl}/login/oauth2/code/{registrationId}
spring.security.oauth2.client.registration.google.client-name=Google
spring.security.oauth2.client.provider.google.authorization-uri=https://accounts.google.com/o/oauth2/auth
spring.security.oauth2.client.provider.google.token-uri=https://oauth2.googleapis.com/token
spring.security.oauth2.client.provider.google.user-info-uri=https://www.googleapis.com/oauth2/v3/userinfo
spring.security.oauth2.client.provider.google.user-name-attribute=sub
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.security.oauth2.core.oidc.user.OidcUser
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.web.bind.annotation.GetMapping
@Controller
class HomeController {
@GetMapping("/")
fun home(model: Model, @AuthenticationPrincipal principal: OidcUser?): String {
if (principal != null) {
model.addAttribute("name", principal.name)
model.addAttribute("email", principal.email)
}
return "home"
}
@GetMapping("/login")
fun login(): String {
return "login"
}
}
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.web.SecurityFilterChain
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http.authorizeRequests { requests ->
requests.antMatchers("/", "/login").permitAll()
.anyRequest().authenticated()
}.oauth2Login { oauth2Login ->
oauth2Login.loginPage("/login")
.defaultSuccessURL("/")
}
return http.build()
}
}

Як Spring застосунок може надсилати та приймати дані з інших API

Для цього можуть використовуватися RestTemplate, WebClient, FeignClient, Apache HttpClient, OkHttp, Retrofit, HTTP4K, Ktor

RestTemplate:
Природа: Синхронний.
Використання: Простий, блокуючий HTTP клієнт.
Статус: У режимі підтримки; рекомендується переходити на WebClient.
Типове використання: Легасі проєкти та прості HTTP виклики.
Переваги: Легкість використання, знайомий багатьом розробникам.

WebClient:
Природа: Асинхронний і неблокуючий.
Використання: Частина Spring WebFlux, підтримує реактивне програмування.
Статус: Сучасний клієнт для HTTP запитів.
Типове використання: Високонавантажені системи та застосунки, що вимагають високої продуктивності.
Переваги: Підтримка асинхронних і синхронних запитів, потоків даних.

FeignClient:
Природа: Синхронний і асинхронний.
Використання: Високорівневий клієнт для взаємодії із зовнішніми API.
Статус: Інтегрується зі Spring Cloud, використовується для мікросервісів.
Типове використання: Мікросервісні архітектури.
Переваги: Автоматичне створення клієнтів, висока абстракція.

Apache HttpClient:
Природа: Синхронний і асинхронний.
Використання: Низькорівневий HTTP клієнт із багатьма конфігураціями.
Статус: Широко використовуваний, потужний і гнучкий.
Типове використання: Випадки, коли потрібен низькорівневий контроль над HTTP запитами.
Переваги: Гнучкість і потужні можливості налаштування.

OkHttp:
Природа: Синхронний і асинхронний.
Використання: Сучасний HTTP клієнт із простим API.
Статус: Часто використовується в поєднанні з Retrofit.
Типове використання: Android-розробка та JVM-базовані застосунки.
Переваги: Простий і чистий API.

Retrofit:
Природа: Синхронний і асинхронний.
Використання: Високорівневий клієнт для роботи з REST API, інтегрується з OkHttp.
Статус: Популярний вибір для Android і мікросервісних архітектур.
Типове використання: Android і мікросервіси.
Переваги: Автоматичне створення клієнтів, підтримка різних конвертерів.

HTTP4K:
Природа: Асинхронний.
Використання: Функціональна бібліотека для створення HTTP клієнтів і серверів.
Статус: Легковагова та гнучка бібліотека.
Типове використання: Проєкти, що вимагають функціонального програмування.
Переваги: Функціональний підхід, простота та гнучкість.

Ktor:
Природа: Асинхронний.
Використання: Фреймворк від JetBrains для створення серверних і клієнтських застосунків на Kotlin.
Статус: Модульна та легко налаштовувана бібліотека.
Типове використання: Високопродуктивні асинхронні застосунки.
Переваги: Відмінна інтеграція з Kotlin, висока продуктивність.

Порівняльний аналіз використання:

RestTemplate:
Усе ще використовується в легасі проєктах, але його використання скорочується.

WebClient:
Дедалі популярніший вибір для нових проєктів, особливо тих, які вимагають асинхронності та реактивності.

FeignClient:
Часто використовується в мікросервісних архітектурах, інтегрованих зі Spring Cloud.

Apache HttpClient і OkHttp:
Використовуються для низькорівневого контролю та в специфічних завданнях.

Retrofit:
Популярний в Android-розробці.

HTTP4K і Ktor:
Менш поширені, але популярні серед Kotlin-розробників і тих, хто віддає перевагу функціональному та модульному підходам.

RestTemplate:

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
}
rest:
url: https://api.example.com/data
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.web.client.RestTemplate
@Configuration
class AppConfig {
@Bean
fun restTemplate(): RestTemplate {
return RestTemplate()
}
}
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import org.springframework.web.client.RestTemplate
@Service
class ApiService(@Autowired private val restTemplate: RestTemplate) {
fun getDataFromExternalApi(url: String): String? {
return restTemplate.getForObject(url, String::class.java)
}
}
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.http.HttpEntity
import org.springframework.http.HttpMethod
import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Service
import org.springframework.web.client.RestTemplate
data class RequestObject(val name: String, val value: String)
data class ResponseObject(val id: Long, val status: String)
@Service
class ApiService(@Autowired private val restTemplate: RestTemplate) {
fun sendAndReceiveObject(url: String, requestObject: RequestObject): ResponseObject? {
val requestEntity = HttpEntity(requestObject)
val responseEntity: ResponseEntity<ResponseObject> = restTemplate.exchange(
url,
HttpMethod.POST,
requestEntity,
ResponseObject::class.java
)
return responseEntity.body
}
}

WebClient:

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-webflux'
}
webclient:
base-url: https://api.example.com
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.web.reactive.function.client.WebClient
@Configuration
class WebClientConfig {
@Bean
fun webClient(): WebClient {
return WebClient.builder().build()
}
}
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import org.springframework.web.reactive.function.client.WebClient
@Service
class ApiService(@Autowired private val webClient: WebClient) {
fun getDataFromExternalApi(url: String): String? {
return webClient.get()
.uri(url)
.retrieve()
.bodyToMono(String::class.java)
.block()
}
}
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import org.springframework.web.reactive.function.client.WebClient
import reactor.core.publisher.Mono
data class RequestObject(val name: String, val value: String)
data class ResponseObject(val id: Long, val status: String)
@Service
class ApiService(@Autowired private val webClient: WebClient) {
fun sendAndReceiveObject(url: String, requestObject: RequestObject): ResponseObject? {
return webClient.post()
.uri(url)
.body(Mono.just(requestObject), RequestObject::class.java)
.retrieve()
.bodyToMono(ResponseObject::class.java)
.block()
}
}

FeignClient:

dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
}
feign:
client:
config:
externalApiClient:
url: https://api.example.com
import org.springframework.cloud.openfeign.EnableFeignClients
import org.springframework.context.annotation.Configuration
@Configuration
@EnableFeignClients
class FeignConfig
import org.springframework.cloud.openfeign.FeignClient
import org.springframework.web.bind.annotation.GetMapping
@FeignClient(name = "externalApiClient", url = "https://api.external.com")
interface ExternalApiClient {
@GetMapping("/data")
fun getData(): String
}
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
@Service
class ApiService(@Autowired private val externalApiClient: ExternalApiClient) {
fun getDataFromExternalApi(): String {
return externalApiClient.getData()
}
}

Apache HttpClient:

dependencies {
implementation 'org.apache.httpcomponents:httpclient:4.5.13'
}
import org.apache.http.client.methods.HttpGet
import org.apache.http.impl.client.CloseableHttpClient
import org.apache.http.impl.client.HttpClients
import org.apache.http.util.EntityUtils
fun sendHttpRequest(url: String): String? {
val httpClient: CloseableHttpClient = HttpClients.createDefault()
val httpGet = HttpGet(url)
httpClient.execute(httpGet).use { response ->
return EntityUtils.toString(response.entity)
}
}

OkHttp:

dependencies {
implementation 'com.squareup.okhttp3:okhttp:4.9.0'
}
import okhttp3.OkHttpClient
import okhttp3.Request
fun sendOkHttpRequest(url: String): String? {
val client = OkHttpClient()
val request = Request.Builder()
.url(url)
.build()
client.newCall(request).execute().use { response ->
return response.body?.string()
}
}

Retrofit:

dependencies {
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
}
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
data class RequestObject(val name: String, val value: String)
data class ResponseObject(val id: Long, val status: String)
interface ApiService {
@POST("data")
suspend fun sendData(@Body requestObject: RequestObject): ResponseObject
@GET("data")
suspend fun getData(): ResponseObject
}
val retrofit = Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
val apiService = retrofit.create(ApiService::class.java)

Spring WebClient with Reactor:

import org.springframework.web.reactive.function.client.WebClient
import reactor.core.publisher.Mono
val webClient = WebClient.create("https://api.example.com")
fun sendAndReceiveReactiveObject(requestObject: RequestObject): Mono<ResponseObject> {
return webClient.post()
.uri("/data")
.body(Mono.just(requestObject), RequestObject::class.java)
.retrieve()
.bodyToMono(ResponseObject::class.java)
}

HTTP4K:

dependencies {
implementation "org.http4k:http4k-core:4.9.9.0"
implementation "org.http4k:http4k-client-okhttp:4.9.9.0"
}
import org.http4k.client.OkHttp
import org.http4k.core.Method
import org.http4k.core.Request
import org.http4k.core.Response
import org.http4k.format.Gson.auto
import org.http4k.lens.Body
data class RequestObject(val name: String, val value: String)
data class ResponseObject(val id: Long, val status: String)
val client = OkHttp()
fun sendHttp4kRequest(url: String, requestObject: RequestObject): ResponseObject? {
val requestLens = Body.auto<RequestObject>().toLens()
val responseLens = Body.auto<ResponseObject>().toLens()
val request = Request(Method.POST, url)
.with(requestLens of requestObject)
val response: Response = client(request)
return responseLens.extract(response)
}

Ktor:

dependencies {
implementation "io.ktor:ktor-client-core:1.5.2"
implementation "io.ktor:ktor-client-cio:1.5.2"
implementation "io.ktor:ktor-client-json:1.5.2"
implementation "io.ktor:ktor-client-serialization:1.5.2"
}
import io.ktor.client.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.client.features.json.*
import io.ktor.client.features.json.serializer.*
import kotlinx.serialization.Serializable
@Serializable
data class RequestObject(val name: String, val value: String)
@Serializable
data class ResponseObject(val id: Long, val status: String)
val client = HttpClient(CIO) {
install(JsonFeature) {
serializer = KotlinxSerializer()
}
}
suspend fun sendKtorRequest(url: String, requestObject: RequestObject): ResponseObject {
return client.post(url) {
body = requestObject
}
}

Як налаштувати шифрування у Spring

У Spring можна налаштувати шифрування даних за допомогою різних бібліотек і підходів. Одним із популярних способів є використання Spring Security Crypto для шифрування та дешифрування даних. У цьому прикладі ми розглянемо, як налаштувати шифрування паролів за допомогою BCrypt, а також як шифрувати дані за допомогою Jasypt.

Шифрування паролів за допомогою BCrypt:
BCrypt — це хеш-функція, спеціально розроблена для хешування паролів. Вона забезпечує надійний захист від атак, таких як атаки за словником і атаки перебором.

dependencies {
implementation("org.springframework.boot:spring-boot-starter-security")
}
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.User
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.provisioning.InMemoryUserDetailsManager
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.web.SecurityFilterChain
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
fun passwordEncoder(): PasswordEncoder {
return BCryptPasswordEncoder()
}
@Bean
fun userDetailsService(passwordEncoder: PasswordEncoder): UserDetailsService {
val user = User.withUsername("user")
.password(passwordEncoder.encode("password"))
.roles("USER")
.build()
return InMemoryUserDetailsManager(user)
}
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.httpBasic()
return http.build()
}
}

Шифрування даних за допомогою Jasypt:
Jasypt (Java Simplified Encryption) — це бібліотека, яка надає прості способи для шифрування та дешифрування даних.

dependencies {
implementation("com.github.ulisesbocchio:jasypt-spring-boot-starter:3.0.4")
}
jasypt.encryptor.password=yourEncryptionPassword
import org.jasypt.encryption.StringEncryptor
import org.jasypt.encryption.pbe.StandardPBEStringEncryptor
import org.jasypt.spring31.properties.EncryptablePropertyPlaceholderConfigurer
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.env.Environment
import java.util.*
@Configuration
class JasyptConfig {
@Bean(name = ["jasyptStringEncryptor"])
fun stringEncryptor(): StringEncryptor {
val encryptor = StandardPBEStringEncryptor()
encryptor.setPassword("yourEncryptionPassword")
return encryptor
}
@Bean
fun propertyPlaceholderConfigurer(environment: Environment): EncryptablePropertyPlaceholderConfigurer {
val configurer = EncryptablePropertyPlaceholderConfigurer(stringEncryptor())
configurer.setLocation(environment)
return configurer
}
}
import org.jasypt.util.text.AES256TextEncryptor
import org.springframework.stereotype.Service
@Service
class DataService {
private val textEncryptor = AES256TextEncryptor().apply {
setPassword("yourEncryptionPassword")
}
fun encryptData(data: String): String {
return textEncryptor.encrypt(data)
}
fun decryptData(encryptedData: String): String {
return textEncryptor.decrypt(encryptedData)
}
}
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
@RestController
class DataController(private val dataService: DataService) {
@GetMapping("/encrypt")
fun encrypt(@RequestParam data: String): String {
return dataService.encryptData(data)
}
@GetMapping("/decrypt")
fun decrypt(@RequestParam encryptedData: String): String {
return dataService.decryptData(encryptedData)
}
}

Як налаштувати HTTPS у Spring

Використання SSL/TLS-сертифікатів для забезпечення безпечного з'єднання у Spring Boot застосунку включає налаштування HTTPS. Для цього потрібно створити або отримати SSL-сертифікат, налаштувати сервер і оновити конфігурацію Spring Boot. Нижче наведено покроковий приклад налаштування HTTPS із використанням самопідписаного сертифіката.

Генерація самопідписаного сертифіката:
Для генерації самопідписаного сертифіката можна використовувати keytool, який входить до складу JDK.

Terminal window
keytool -genkeypair -alias myalias -keyalg RSA -keysize 2048 -storetype PKCS12 -keystore keystore.p12 -validity 3650

myalias:
псевдонім ключа.

keystore.p12:
ім'я файлу сховища ключів.

validity 3650:
термін дії сертифіката (у днях).

Під час виконання цієї команди вас попросять ввести різні дані, такі як пароль для сховища ключів, ваше ім'я, організація тощо. Після цього буде створено файл keystore.p12.

Налаштування Spring Boot для використання SSL:
Додайте налаштування SSL у файл application.properties.

server.port=8443
server.ssl.key-store=classpath:keystore.p12
server.ssl.key-store-password=your_password
server.ssl.key-store-type=PKCS12
server.ssl.key-alias=myalias

Переміщення файлу keystore.p12 до теки ресурсів

Перемістіть файл keystore.p12 до теки src/main/resources вашого проєкту, щоб Spring Boot міг його знайти.

Оновлення конфігурації безпеки (якщо потрібно)

Якщо ваш застосунок використовує Spring Security, ви можете налаштувати його для підтримки HTTPS.

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.web.SecurityFilterChain
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http.authorizeRequests { requests ->
requests.anyRequest().authenticated()
}.formLogin { form ->
form.loginPage("/login").permitAll()
}.logout { logout ->
logout.permitAll()
}.requiresChannel { channel ->
channel.anyRequest().requiresSecure()
}
return http.build()
}
}

Що таке Spring Boot Actuator

Spring Boot Actuator — це потужний інструмент в екосистемі Spring Boot, який надає готові функції для моніторингу та керування працюючими застосунками. Actuator надає набір кінцевих точок (endpoints), які можна використовувати для отримання інформації про застосунок, виконання операцій керування та моніторингу його стану.

Основні можливості Spring Boot Actuator:

Моніторинг стану:
Надає інформацію про стан застосунку, таку як метрики продуктивності, інформація про базу даних, дані про систему тощо.

Керування застосунком:
Дозволяє виконувати адміністративні операції, такі як перезапуск застосунку, отримання інформації про конфігурацію тощо.

Інтеграція з інструментами моніторингу:
Легко інтегрується з інструментами моніторингу та сповіщення, такими як Prometheus, Grafana та інші.

Увімкнення Spring Boot Actuator:
Для початку роботи з Actuator необхідно додати залежність у ваш проєкт.
Приклад проєкту з використанням Gradle

plugins {
id("org.springframework.boot") version "2.6.2"
id("io.spring.dependency-management") version "1.0.11.RELEASE"
kotlin("jvm") version "1.6.0"
kotlin("plugin.spring") version "1.6.0"
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}

Налаштування Spring Boot Actuator:
За замовчуванням деякі кінцеві точки Actuator увімкнені, але доступ до них обмежений. Можна налаштувати, які кінцеві точки будуть доступні, а також рівень їхньої безпеки.
application.properties

# Увімкнення всіх кінцевих точок Actuator
management.endpoints.web.exposure.include=*
# Налаштування шляху до кінцевих точок Actuator
management.endpoints.web.base-path=/actuator

Основні кінцеві точки Actuator:

/actuator/health:
Перевірка стану застосунку.

/actuator/info:
Загальна інформація про застосунок.

/actuator/metrics:
Метрики продуктивності застосунку.

/actuator/env:
Інформація про поточну конфігурацію середовища.

/actuator/beans:
Перелік усіх бінів Spring у контексті застосунку.

/actuator/threaddump:
Знімок поточних потоків.

/actuator/loggers:
Рівні логування та їх зміна.

Приклади запитів:
Перевірка стану застосунку

curl http://localhost:8080/actuator/health

Отримання інформації про застосунок

curl http://localhost:8080/actuator/info

Отримання метрик

curl http://localhost:8080/actuator/metrics

Захист кінцевих точок Actuator:
Для захисту кінцевих точок Actuator можна використовувати Spring Security. Це дозволяє обмежити доступ до адміністративних функцій лише авторизованим користувачам.

Приклад конфігурації безпеки:
У цьому прикладі доступ до кінцевих точок Actuator обмежений користувачами з роллю ADMIN

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("/actuator/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.httpBasic()
}
}

Як налаштувати авторизацію за допомогою Spring Security

dependencies {
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
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.User
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.provisioning.InMemoryUserDetailsManager
import org.springframework.security.web.SecurityFilterChain
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http.authorizeHttpRequests { authz ->
authz
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
}.formLogin { form ->
form
.loginPage("/login")
.permitAll()
}.logout { logout ->
logout.permitAll()
}
return http.build()
}
@Bean
fun userDetailsService(): UserDetailsService {
val user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build()
val admin = User.withDefaultPasswordEncoder()
.username("admin")
.password("admin")
.roles("ADMIN")
.build()
return InMemoryUserDetailsManager(user, admin)
}
}
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.web.bind.annotation.GetMapping
@Controller
class HomeController {
@GetMapping("/")
fun home(model: Model): String {
model.addAttribute("message", "Welcome to the Home Page!")
return "home"
}
@GetMapping("/login")
fun login(): String {
return "login"
}
}

src/main/resources/templates/home.html:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Home</title>
</head>
<body>
<h1 th:text="${message}">Welcome to the Home Page!</h1>
<a href="/logout">Logout</a>
</body>
</html>

src/main/resources/templates/login.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">
<div>
<label>Username:</label>
<input type="text" name="username"/>
</div>
<div>
<label>Password:</label>
<input type="password" name="password"/>
</div>
<div>
<button type="submit">Login</button>
</div>
</form>
</body>
</html>

Користувач:
Для створення користувацького класу, що успадковує UserDetails, потрібно створити клас, який реалізовуватиме інтерфейс UserDetails. Цей клас використовуватиметься для представлення користувача в системі безпеки Spring.

import javax.persistence.*
@Entity
data class UserEntity(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0,
val username: String,
val password: String,
val enabled: Boolean = true,
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "users_roles",
joinColumns = [JoinColumn(name = "user_id")],
inverseJoinColumns = [JoinColumn(name = "role_id")]
)
val roles: Set<RoleEntity> = HashSet()
)
import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.GenerationType
import javax.persistence.Id
@Entity
data class RoleEntity(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0,
val name: String
)
import org.springframework.data.jpa.repository.JpaRepository
interface UserRepository : JpaRepository<UserEntity, Long> {
fun findByUsername(username: String): UserEntity?
}
import org.springframework.data.jpa.repository.JpaRepository
interface RoleRepository : JpaRepository<RoleEntity, Long> {
fun findByName(name: String): RoleEntity?
}
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.userdetails.UserDetails
class CustomUserDetails(private val user: UserEntity) : UserDetails {
override fun getAuthorities(): Collection<GrantedAuthority> {
return user.roles.map { SimpleGrantedAuthority(it.name) }
}
override fun getPassword(): String {
return user.password
}
override fun getUsername(): String {
return user.username
}
override fun isAccountNonExpired(): Boolean {
return true
}
override fun isAccountNonLocked(): Boolean {
return true
}
override fun isCredentialsNonExpired(): Boolean {
return true
}
override fun isEnabled(): Boolean {
return user.enabled
}
}
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.core.userdetails.UsernameNotFoundException
import org.springframework.stereotype.Service
@Service
class CustomUserDetailsService(private val userRepository: UserRepository) : UserDetailsService {
override fun loadUserByUsername(username: String): UserDetails {
val user = userRepository.findByUsername(username)
?: throw UsernameNotFoundException("User not found with username: $username")
return CustomUserDetails(user)
}
}
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
@Service
@Transactional
class UserService(
private val userRepository: UserRepository,
private val roleRepository: RoleRepository
) {
fun createUser(username: String, password: String, roles: Set<String>): UserEntity {
val roleEntities = roles.map { roleRepository.findByName(it) ?: RoleEntity(name = it) }.toSet()
val user = UserEntity(username = username, password = password, roles = roleEntities)
return userRepository.save(user)
}
}
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 customUserDetailsService: CustomUserDetailsService
) {
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http.authorizeRequests { authz ->
authz.antMatchers("/public/**").permitAll()
.anyRequest().authenticated()
}.formLogin { form ->
form.loginPage("/login")
.permitAll()
}.logout { logout ->
logout.permitAll()
}
return http.build()
}
@Bean
fun userDetailsService(): UserDetailsService {
return customUserDetailsService
}
@Bean
fun passwordEncoder(): PasswordEncoder {
return BCryptPasswordEncoder()
}
}

Використання UserService для створення користувача

import org.springframework.boot.CommandLineRunner
import org.springframework.stereotype.Component
@Component
class DataInitializer(private val userService: UserService) : CommandLineRunner {
override fun run(vararg args: String?) {
userService.createUser("user", passwordEncoder().encode("password"), setOf("ROLE_USER"))
userService.createUser("admin", passwordEncoder().encode("admin"), setOf("ROLE_ADMIN"))
}
fun passwordEncoder(): PasswordEncoder {
return BCryptPasswordEncoder()
}
}

У Spring Security можна використовувати анотації @PreAuthorize і @Secured для обмеження доступу до методів контролерів або сервісів на основі ролей. Ось приклади їх використання.

import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.web.bind.annotation.GetMapping
@Controller
class UserController {
@PreAuthorize("hasRole('ROLE_USER')")
@GetMapping("/user")
fun userPage(model: Model): String {
model.addAttribute("message", "Welcome, User!")
return "user"
}
@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping("/admin")
fun adminPage(model: Model): String {
model.addAttribute("message", "Welcome, Admin!")
return "admin"
}
}
import org.springframework.security.access.annotation.Secured
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.web.bind.annotation.GetMapping
@Controller
class AdminController {
@Secured("ROLE_USER")
@GetMapping("/user-secured")
fun userPageSecured(model: Model): String {
model.addAttribute("message", "Welcome, User! (Secured)")
return "user-secured"
}
@Secured("ROLE_ADMIN")
@GetMapping("/admin-secured")
fun adminPageSecured(model: Model): String {
model.addAttribute("message", "Welcome, Admin! (Secured)")
return "admin-secured"
}
}

In-Memory Storage:
In-Memory зберігання користувачів — найпростіший спосіб для створення користувачів. Ви вже бачили приклад вище. Ось іще раз приклад для ясності:

@Bean
fun userDetailsService(): UserDetailsService {
val user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build()
val admin = User.withDefaultPasswordEncoder()
.username("admin")
.password("admin")
.roles("ADMIN")
.build()
return InMemoryUserDetailsManager(user, admin)
}

JDBC Storage:
Для використання JDBC сховища вам потрібно налаштувати базу даних і використовувати JdbcUserDetailsManager. Приклад:

dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jdbc")
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
runtimeOnly("com.h2database:h2") // Або інший драйвер бази даних
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
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.User
import org.springframework.security.provisioning.JdbcUserDetailsManager
import org.springframework.security.provisioning.UserDetailsManager
import org.springframework.security.web.SecurityFilterChain
import javax.sql.DataSource
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http.authorizeHttpRequests { authz ->
authz.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
}.formLogin { form ->
form.loginPage("/login")
.permitAll()
} .logout { logout ->
logout.permitAll()
}
return http.build()
}
@Bean
fun userDetailsService(dataSource: DataSource): UserDetailsManager {
val userDetailsManager = JdbcUserDetailsManager(dataSource)
// Додавання користувачів, якщо їх немає в базі даних
if (!userDetailsManager.userExists("user")) {
val user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build()
userDetailsManager.createUser(user)
}
if (!userDetailsManager.userExists("admin")) {
val admin = User.withDefaultPasswordEncoder()
.username("admin")
.password("admin")
.roles("ADMIN")
.build()
userDetailsManager.createUser(admin)
}
return userDetailsManager
}
}
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=password
spring.h2.console.enabled=true

Скрипти для створення таблиць:
Spring Security надає готові SQL-скрипти для створення таблиць користувачів і ролей. Додайте їх у src/main/resources/schema.sql:

CREATE TABLE users (
username VARCHAR(50) NOT NULL PRIMARY KEY,
password VARCHAR(500) NOT NULL,
enabled BOOLEAN NOT NULL
);
CREATE TABLE authorities (
username VARCHAR(50) NOT NULL,
authority VARCHAR(50) NOT NULL,
FOREIGN KEY (username) REFERENCES users (username)
);
CREATE UNIQUE INDEX ix_auth_username ON authorities (username, authority);

Custom UserDetailsService:
Якщо вам потрібне кастомне сховище, створіть власну реалізацію UserDetailsService.

import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.core.userdetails.UsernameNotFoundException
import org.springframework.stereotype.Service
@Service
class CustomUserDetailsService : UserDetailsService {
override fun loadUserByUsername(username: String): UserDetails {
// Замініть цей код на власне сховище (наприклад, запит до бази даних)
if (username == "user") {
return User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build()
} else {
throw UsernameNotFoundException("User not found")
}
}
}
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.web.SecurityFilterChain
@Configuration
@EnableWebSecurity
class SecurityConfig(
val customUserDetailsService: CustomUserDetailsService
) {
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
http.authorizeHttpRequests { authz ->
authz .requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
}.formLogin { form ->
form.loginPage("/login")
.permitAll()
}.logout { logout ->
logout.permitAll()
}
return http.build()
}
@Bean
fun userDetailsService(): UserDetailsService {
return customUserDetailsService
}
}

Що таке Spring Integration

Spring Integration — це модуль в екосистемі Spring, призначений для створення корпоративних інтеграційних рішень. Він надає підтримку інтеграційних шаблонів (Enterprise Integration Patterns, EIP), які допомагають розробникам проєктувати та реалізовувати інтеграцію різних систем і компонентів у застосунку. Spring Integration дозволяє легко пов'язувати та координувати взаємодію між різними системами й компонентами, забезпечуючи масштабованість, надійність і простоту підтримки.

Основні можливості Spring Integration:

Підтримка інтеграційних шаблонів (EIP):
Включає підтримку популярних шаблонів інтеграції, таких як маршрутизація повідомлень, перетворення повідомлень, фільтрація повідомлень і агрегація повідомлень.

З'єднання з різними системами:
Забезпечує підключення до різних джерел даних і зовнішніх систем, таких як бази даних, черги повідомлень (JMS, RabbitMQ), веб-служби (REST, SOAP), файли та інші.

Модульна архітектура:
Надає модульну архітектуру, що дозволяє легко додавати та конфігурувати компоненти інтеграції.

Розширюваність:
Дозволяє розробникам легко розширювати функціональність шляхом додавання власних компонентів інтеграції.

Основні компоненти Spring Integration:

Message (Повідомлення):
Основна структура даних, яка передається між компонентами. Повідомлення складається з корисного навантаження (payload) і заголовків (headers).

Channel (Канал):
Транспортний механізм, через який передаються повідомлення. Канали можуть бути точка-точка (point-to-point) або публікація-підписка (publish-subscribe).

Endpoint (Кінцева точка):
Компоненти, які надсилають або отримують повідомлення. Кінцеві точки включають перетворювачі повідомлень (message transformers), маршрутизатори (routers), фільтри (filters) та інші обробники повідомлень.

Gateway (Шлюз):
Інтерфейси, які забезпечують інтеграцію із зовнішніми системами та протоколами. Шлюзи можуть використовуватися для надсилання та отримання повідомлень із/у веб-служби, черги повідомлень та інші системи.

Приклад використання Spring Integration:
Розгляньмо приклад, де ми використовуємо Spring Integration для читання повідомлень із черги JMS та їх обробки.

dependencies {
implementation("org.springframework.boot:spring-boot-starter-integration")
implementation("org.springframework.boot:spring-boot-starter-activemq")
implementation("org.springframework.integration:spring-integration-jms")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.integration.annotation.ServiceActivator
import org.springframework.integration.channel.DirectChannel
import org.springframework.integration.config.EnableIntegration
import org.springframework.integration.core.MessageHandler
import org.springframework.integration.jms.dsl.Jms
import org.springframework.jms.annotation.EnableJms
import org.springframework.jms.core.JmsTemplate
import javax.jms.ConnectionFactory
@Configuration
@EnableIntegration
@EnableJms
class IntegrationConfig {
@Bean
fun inputChannel() = DirectChannel()
@Bean
fun jmsInboundAdapter(connectionFactory: ConnectionFactory) =
Jms.inboundAdapter(connectionFactory)
.destination("inputQueue")
.channel(inputChannel())
.get()
@Bean
@ServiceActivator(inputChannel = "inputChannel")
fun messageHandler(): MessageHandler {
return MessageHandler { message ->
println("Received message: ${message.payload}")
}
}
}

Надсилання повідомлень у чергу JMS

import org.springframework.jms.core.JmsTemplate
import org.springframework.stereotype.Component
@Component
class JmsProducer(private val jmsTemplate: JmsTemplate) {
fun sendMessage(destination: String, message: String) {
jmsTemplate.convertAndSend(destination, message)
}
}
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.annotation.Bean
import javax.annotation.PostConstruct
@SpringBootApplication
class DemoApplication(private val jmsProducer: JmsProducer) {
@PostConstruct
fun init() {
jmsProducer.sendMessage("inputQueue", "Hello, Spring Integration!")
}
}
fun main(args: Array<String>) {
runApplication<DemoApplication>(*args)
}

Що таке Spring Batch

Spring Batch — це модуль в екосистемі Spring, призначений для створення масштабованих, надійних і багатопотокових пакетних застосунків. Він надає потужний фреймворк для обробки великих обсягів даних у пакетному режимі, включно з читанням, обробкою та записом даних.

Основні можливості Spring Batch:

Читання, обробка та запис даних:
Підтримує різні джерела даних для читання, такі як бази даних, файли, черги повідомлень тощо.
Дозволяє виконувати складні операції обробки даних.
Підтримує запис даних у різні джерела.

Модульна архітектура:
Пакетна робота (Job) складається з кроків (Step), кожен з яких може бути сконфігурований незалежно.
Кожен крок включає три основні фази: читання, обробка та запис (Read, Process, Write).

Керування транзакціями та відмовами:
Підтримує керування транзакціями, відкатами та повторними спробами для забезпечення надійності та цілісності даних.

Планування та моніторинг:
Інтеграція з різними планувальниками завдань (Quartz, Spring Scheduling та ін.).
Підтримка моніторингу та аудиту виконання пакетних завдань.

Підтримка паралелізму та багатопотоковості:
Забезпечує можливість виконання кроків і завдань у багатопотоковому режимі для підвищення продуктивності.

Основні компоненти Spring Batch:

Job (Робота):
Пакетне завдання, що складається з одного або кількох кроків (Step). Визначає, які кроки мають бути виконані та в якому порядку.

Step (Крок):
Одиниця виконання всередині роботи. Складається з етапів читання, обробки та запису даних. Кожен крок може мати власну логіку та конфігурацію.

ItemReader (Читач елементів):
Компонент, який відповідає за читання даних із джерела. Приклади включають FlatFileItemReader для читання даних із файлу та JdbcCursorItemReader для читання даних із бази даних.

ItemProcessor (Процесор елементів):
Компонент, який відповідає за обробку даних. Може виконувати будь-які перетворення або фільтрацію даних.

ItemWriter (Записувач елементів):
Компонент, який відповідає за запис даних у цільове джерело. Приклади включають FlatFileItemWriter для запису даних у файл та JdbcBatchItemWriter для запису даних у базу даних.

Приклад використання Spring Batch:
Розгляньмо приклад використання Spring Batch для читання даних із CSV-файлу, їх обробки та запису в базу даних.

dependencies {
implementation("org.springframework.boot:spring-boot-starter-batch")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("org.springframework.boot:spring-boot-starter")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
runtimeOnly("com.h2database:h2")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
import org.springframework.batch.core.Job
import org.springframework.batch.core.Step
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory
import org.springframework.batch.core.launch.support.RunIdIncrementer
import org.springframework.batch.item.ItemProcessor
import org.springframework.batch.item.ItemReader
import org.springframework.batch.item.ItemWriter
import org.springframework.batch.item.file.FlatFileItemReader
import org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper
import org.springframework.batch.item.file.mapping.DefaultLineMapper
import org.springframework.batch.item.file.mapping.DelimitedLineTokenizer
import org.springframework.batch.item.file.transform.LineTokenizer
import org.springframework.batch.item.file.transform.Range
import org.springframework.batch.item.support.ListItemReader
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.core.io.ClassPathResource
import javax.sql.DataSource
@Configuration
@EnableBatchProcessing
class BatchConfig(
private val jobBuilderFactory: JobBuilderFactory,
private val stepBuilderFactory: StepBuilderFactory,
private val dataSource: DataSource
) {
@Bean
fun personReader(): ItemReader<Person> {
val reader = FlatFileItemReader<Person>()
reader.setResource(ClassPathResource("people.csv"))
reader.setLineMapper(DefaultLineMapper<Person>().apply {
setLineTokenizer(DelimitedLineTokenizer().apply {
setNames("firstName", "lastName")
})
setFieldSetMapper(BeanWrapperFieldSetMapper<Person>().apply {
setTargetType(Person::class.java)
})
})
return reader
}
@Bean
fun personProcessor(): ItemProcessor<Person, Person> {
return ItemProcessor { person ->
person.copy(
firstName = person.firstName.toUpperCase(),
lastName = person.lastName.toUpperCase()
)
}
}
@Bean
fun personWriter(): ItemWriter<Person> {
return ItemWriter { items ->
items.forEach { println("Writing person: $it") }
}
}
@Bean
fun importUserJob(): Job {
return jobBuilderFactory.get("importUserJob")
.incrementer(RunIdIncrementer())
.flow(step1())
.end()
.build()
}
@Bean
fun step1(): Step {
return stepBuilderFactory.get("step1")
.chunk<Person, Person>(10)
.reader(personReader())
.processor(personProcessor())
.writer(personWriter())
.build()
}
}
data class Person(
val firstName: String = "",
val lastName: String = ""
)

CSV-файл (resources/people.csv)

John,Doe
Jane,Doe

Що таке AutoConfiguration у Spring

Автоконфігурація (Auto-configuration) у Spring Boot — це механізм, який автоматично конфігурує ваш Spring-застосунок на основі залежностей, знайдених у classpath, і різних умов. Це одна з ключових особливостей Spring Boot, що дозволяє мінімізувати кількість конфігурацій, необхідних для запуску застосунку.

Як працює автоконфігурація:
Spring Boot аналізує класи в classpath вашого проєкту та автоматично створює й налаштовує необхідні біни для вашого застосунку. Цей процес керується анотацією @EnableAutoConfiguration, яка зазвичай включена в анотацію @SpringBootApplication.

Приклади роботи автоконфігурації:

Конфігурація бази даних:
Якщо у вашому classpath є H2, HSQLDB або інша підтримувана база даних, Spring Boot автоматично створить біни для DataSource, EntityManager і налаштує їх.

Конфігурація веб-сервера:
Якщо у вас є залежності від spring-boot-starter-web, Spring Boot автоматично налаштує веб-сервер (наприклад, Tomcat) і зв'яже його з вашим застосунком.

Основні файли та класи автоконфігурації:

META-INF/spring.factories:
Цей файл вказує на класи автоконфігурації, які мають бути завантажені. Він міститься в JAR-файлах з автоконфігурацією.

Класи автоконфігурації:
Класи, позначені анотацією @Configuration і зазвичай такі, що містять логіку налаштування бінів, засновану на присутності або відсутності певних класів чи бінів у контексті застосунку.

Визначення автоконфігурації:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@Configuration
public class MyServiceAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public MyService myService() {
return new MyService();
}
}

Реєстрація автоконфігурації в META-INF/spring.factories:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.autoconfiguration.MyServiceAutoConfiguration

Умови автоконфігурації:
Spring Boot надає безліч анотацій, які можна використовувати для керування тим, коли автоконфігурація має бути застосована:

@ConditionalOnClass:
Автоконфігурація застосовується, якщо вказаний клас знаходиться в classpath.

@ConditionalOnMissingBean:
Автоконфігурація застосовується, якщо бін відсутній у контексті.

@ConditionalOnProperty:
Автоконфігурація застосовується, якщо певна властивість встановлена в application.properties або application.yml.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@Configuration
@ConditionalOnClass(MyService.class)
public class MyServiceAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public MyService myService() {
return new MyService();
}
}

Вимкнення автоконфігурації:
Іноді може знадобитися вимкнути певні автоконфігурації. Це можна зробити за допомогою анотації @SpringBootApplication або @EnableAutoConfiguration.

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
import org.springframework.boot.runApplication
@SpringBootApplication(exclude = [DataSourceAutoConfiguration::class])
class MyApplication
fun main(args: Array<String>) {
SpringApplication.run(Application::class.java, *args)
}

Як налаштувати різні профілі збірки у Spring

У Spring Boot можна використовувати профілі для керування різними конфігураціями вашого застосунку, включно з різними базовими URL. Профілі дозволяють вам створювати кілька конфігурацій для різних середовищ (наприклад, розробка, тестування, продакшен) і перемикатися між ними, не змінюючи основний код застосунку.

Кроки для створення кількох варіантів збірки з різним base URL:
Визначення профілів у application.properties або application.yml
Створіть файли конфігурації для кожного профілю. Наприклад, створимо три профілі: dev, test і prod, кожен з яких матиме свій базовий URL.

application-dev.properties
app.base.url=https://dev.example.com
application-test.properties
app.base.url=https://test.example.com
application-prod.properties
app.base.url=https://prod.example.com

Налаштування основного файлу конфігурації:
В основному файлі application.properties або application.yml можна визначити загальні налаштування та вказати активний профіль за замовчуванням (необов'язково).

application.properties
spring.profiles.active=dev

Читання конфігурації у вашому коді:
Використовуйте анотацію @Value або @ConfigurationProperties для отримання значення base URL із конфігураційних файлів.

import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Configuration
@Configuration
class AppConfig {
@Value("\${app.base.url}")
lateinit var baseUrl: String
}

Використання профілів у коді:
Ви можете також використовувати анотацію @Profile для увімкнення або вимкнення бінів залежно від активного профілю.

import org.springframework.context.annotation.Profile
import org.springframework.stereotype.Service
interface ExampleService {
fun getBaseUrl(): String
}
@Service
@Profile("dev")
class DevExampleService(private val config: AppConfig) : ExampleService {
override fun getBaseUrl(): String {
return config.baseUrl
}
}
@Service
@Profile("test")
class TestExampleService(private val config: AppConfig) : ExampleService {
override fun getBaseUrl(): String {
return config.baseUrl
}
}
@Service
@Profile("prod")
class ProdExampleService(private val config: AppConfig) : ExampleService {
override fun getBaseUrl(): String {
return config.baseUrl
}
}

Запуск застосунку із вказаним профілем:
Ви можете вказати активний профіль під час запуску застосунку через параметри командного рядка, змінні середовища або файл конфігурації.

Через параметри командного рядка

$ java -jar myapp.jar --spring.profiles.active=prod

Через змінні середовища

$ export SPRING_PROFILES_ACTIVE=prod
$ java -jar myapp.jar

Що таке Spring WebFlux

Spring WebFlux — це реактивний веб-фреймворк, представлений у Spring 5, який дозволяє створювати асинхронні та неблокуючі веб-застосунки. WebFlux може працювати на різних реактивних рушіях, таких як Netty, Jetty або Tomcat.

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb-reactive'
}

Приклади реактивного та не реактивного коду для Spring WebFlux і Spring Web (звичайний Spring MVC) з використанням Kotlin. Буде показано, як створювати REST API для простого CRUD-застосунку, який працює з користувачами (User).

Приклад не реактивного коду для Spring Web (Spring MVC):

import javax.persistence.Entity
import javax.persistence.Id
import javax.persistence.GeneratedValue
import javax.persistence.GenerationType
@Entity
data class User(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
val email: String,
val username: String
)
import org.springframework.data.jpa.repository.JpaRepository
interface UserRepository : JpaRepository<User, Long>
import org.springframework.stereotype.Service
@Service
class UserService(private val userRepository: UserRepository) {
fun getAllUsers(): List<User> {
return userRepository.findAll()
}
fun getUserById(id: Long): User? {
return userRepository.findById(id).orElse(null)
}
fun createUser(user: User): User {
return userRepository.save(user)
}
fun updateUser(id: Long, user: User): User? {
return userRepository.findById(id).map {
val updatedUser = it.copy(email = user.email, username = user.username)
userRepository.save(updatedUser)
}.orElse(null)
}
fun deleteUser(id: Long) {
userRepository.deleteById(id)
}
}
import org.springframework.web.bind.annotation.*
@RestController
@RequestMapping("/users")
class UserController(private val userService: UserService) {
@GetMapping
fun getAllUsers(): List<User> {
return userService.getAllUsers()
}
@GetMapping("/{id}")
fun getUserById(@PathVariable id: Long): User? {
return userService.getUserById(id)
}
@PostMapping
fun createUser(@RequestBody user: User): User {
return userService.createUser(user)
}
@PutMapping("/{id}")
fun updateUser(@PathVariable id: Long, @RequestBody user: User): User? {
return userService.updateUser(id, user)
}
@DeleteMapping("/{id}")
fun deleteUser(@PathVariable id: Long) {
userService.deleteUser(id)
}
}

Приклад реактивного коду для Spring WebFlux:

import org.springframework.data.annotation.Id
import org.springframework.data.mongodb.core.mapping.Document
@Document(collection = "users")
data class User(
@Id
val id: String? = null,
val email: String,
val username: String
)
import org.springframework.data.mongodb.repository.ReactiveMongoRepository
import reactor.core.publisher.Mono
interface UserRepository : ReactiveMongoRepository<User, String>
import org.springframework.stereotype.Service
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
@Service
class UserService(private val userRepository: UserRepository) {
fun getAllUsers(): Flux<User> {
return userRepository.findAll()
}
fun getUserById(id: String): Mono<User> {
return userRepository.findById(id)
}
fun createUser(user: User): Mono<User> {
return userRepository.save(user)
}
fun updateUser(id: String, user: User): Mono<User> {
return userRepository.findById(id)
.flatMap {
val updatedUser = it.copy(email = user.email, username = user.username)
userRepository.save(updatedUser)
}
}
fun deleteUser(id: String): Mono<Void> {
return userRepository.deleteById(id)
}
}
import org.springframework.web.bind.annotation.*
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
@RestController
@RequestMapping("/users")
class UserController(private val userService: UserService) {
@GetMapping
fun getAllUsers(): Flux<User> {
return userService.getAllUsers()
}
@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)
}
@PutMapping("/{id}")
fun updateUser(@PathVariable id: String, @RequestBody user: User): Mono<User> {
return userService.updateUser(id, user)
}
@DeleteMapping("/{id}")
fun deleteUser(@PathVariable id: String): Mono<Void> {
return userService.deleteUser(id)
}
}

Основні відмінності полягають у тому, що реактивний підхід використовує типи Mono і Flux для роботи з асинхронними потоками даних, що дозволяє покращити продуктивність і масштабованість застосунку, особливо під час роботи з великою кількістю одночасних запитів.

Що таке ApplicationContext у Spring

ApplicationContext це центральний інтерфейс для конфігурування застосунку та отримання доступу до його компонентів (бінів). Він є розширенням інтерфейсу BeanFactory, який забезпечує базову функціональність контейнера інверсії керування (IoC). Основна відмінність ApplicationContext від BeanFactory полягає в додаткових можливостях, таких як:
Підтримка різних типів подій.
Можливість завантаження повідомлень із файлів локалізації.
Підтримка декларативного керування транзакціями.

Приклад створення ApplicationContext:
Створення ApplicationContext із XML конфігурації

beans.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">
<bean id="exampleBean" class="com.example.ExampleBean">
<property name="message" value="Hello, Spring!" />
</bean>
</beans>
import org.springframework.context.ApplicationContext
import org.springframework.context.support.ClassPathXmlApplicationContext
fun main() {
val context: ApplicationContext = ClassPathXmlApplicationContext("beans.xml")
val exampleBean: ExampleBean = context.getBean("exampleBean", ExampleBean::class.java)
println(exampleBean.message)
}

Створення ApplicationContext

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
class AppConfig {
@Bean
fun exampleBean(): ExampleBean {
return ExampleBean("Hello, Spring with Kotlin DSL!")
}
}
import org.springframework.context.ApplicationContext
import org.springframework.context.annotation.AnnotationConfigApplicationContext
fun main() {
val context: ApplicationContext = AnnotationConfigApplicationContext(AppConfig::class.java)
val exampleBean: ExampleBean = context.getBean(ExampleBean::class.java)
println(exampleBean.message)
}

Що таке BeanFactory

BeanFactory є центральним інтерфейсом у Spring для керування об'єктами, які складають ваш застосунок. Хоча BeanFactory є фундаментальним інтерфейсом для контейнера Spring, частіше використовується його функціональніший нащадок — ApplicationContext. Проте BeanFactory може бути корисним у деяких ситуаціях, наприклад, коли вам потрібне мінімальне використання пам'яті або тонший контроль над ініціалізацією.

Основні методи BeanFactory:

getBean:
Повертає екземпляр біна за його іменем або типом.

containsBean:
Перевіряє, чи існує бін із вказаним іменем.

isSingleton:
Перевіряє, чи є бін із вказаним іменем синглтоном.

isPrototype:
Перевіряє, чи є бін із вказаним іменем прототипом.

getType:
Повертає тип біна за його іменем.

getAliases:
Повертає всі псевдоніми для біна з вказаним іменем.

Приклад використання BeanFactory з XML:

beans.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">
<bean id="myBean" class="com.example.MyBean">
<property name="name" value="Bean Name"/>
</bean>
</beans>
import org.springframework.beans.factory.BeanFactory
import org.springframework.beans.factory.xml.XmlBeanFactory
import org.springframework.core.io.ClassPathResource
class MyBean {
var name: String? = null
}
fun main() {
// Завантаження конфігураційного файлу Spring
val resource = ClassPathResource("beans.xml")
val beanFactory: BeanFactory = XmlBeanFactory(resource)
// Отримання біна за іменем
val myBean = beanFactory.getBean("myBean") as MyBean
println("Bean Name: ${myBean.name}")
// Перевірка, чи існує бін
val containsBean = beanFactory.containsBean("myBean")
println("Contains 'myBean': $containsBean")
// Перевірка, чи є бін синглтоном
val isSingleton = beanFactory.isSingleton("myBean")
println("Is 'myBean' Singleton: $isSingleton")
// Отримання типу біна
val beanType = beanFactory.getType("myBean")
println("Bean Type: $beanType")
// Отримання всіх псевдонімів для біна
val aliases = beanFactory.getAliases("myBean")
println("Bean Aliases: ${aliases.joinToString()}")
}
import org.springframework.context.ApplicationContext
import org.springframework.context.support.ClassPathXmlApplicationContext
fun main() {
// Завантаження конфігураційного файлу Spring
val context: ApplicationContext = ClassPathXmlApplicationContext("beans.xml")
// Отримання біна за іменем
val myBean = context.getBean("myBean") as MyBean
println("Bean Name: ${myBean.name}")
// Перевірка, чи існує бін
val containsBean = context.containsBean("myBean")
println("Contains 'myBean': $containsBean")
// Перевірка, чи є бін синглтоном
val isSingleton = context.isSingleton("myBean")
println("Is 'myBean' Singleton: $isSingleton")
// Отримання типу біна
val beanType = context.getType("myBean")
println("Bean Type: $beanType")
// Отримання всіх псевдонімів для біна
val aliases = context.getAliases("myBean")
println("Bean Aliases: ${aliases.joinToString()}")
}

Приклад із використанням анотацій:

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
class AppConfig {
@Bean
fun myBean(): MyBean {
return MyBean().apply {
name = "Bean Name"
}
}
}
import org.springframework.beans.factory.BeanFactory
import org.springframework.stereotype.Service
@Service
class TestClass(
private val beanFactory: BeanFactory
) {
fun start() {
val myBean = beanFactory.getBean("myBean") as GreetingHandler
}
}
import org.springframework.context.annotation.AnnotationConfigApplicationContext
fun main() {
// Створення контексту на основі конфігураційного класу
val context = AnnotationConfigApplicationContext(AppConfig::class.java)
// Отримання BeanFactory з контексту
val beanFactory = context.beanFactory
// Отримання біна за іменем
val myBean = beanFactory.getBean("myBean") as MyBean
println("Bean Name: ${myBean.name}")
// Перевірка, чи існує бін
val containsBean = beanFactory.containsBean("myBean")
println("Contains 'myBean': $containsBean")
// Перевірка, чи є бін синглтоном
val isSingleton = beanFactory.isSingleton("myBean")
println("Is 'myBean' Singleton: $isSingleton")
// Отримання типу біна
val beanType = beanFactory.getType("myBean")
println("Bean Type: $beanType")
// Отримання всіх псевдонімів для біна
val aliases = beanFactory.getAliases("myBean")
println("Bean Aliases: ${aliases.joinToString()}")
}

Що таке Spring BOM

Spring BOM (Bill Of Materials) — це механізм керування залежностями, що надається Spring для спрощення керування версіями різних артефактів (бібліотек) в екосистемі Spring. BOM використовується для того, щоб гарантувати сумісність і узгодженість версій усіх залежностей, що використовуються у проєкті. Використовуючи BOM, ви можете вказати одну версію BOM, і всі пов'язані бібліотеки автоматично використовуватимуть відповідні версії, вказані в BOM.

Приклад із використанням Spring BOM:

plugins {
id 'org.springframework.boot' version '2.6.7'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
dependencyManagement {
imports {
mavenBom "org.springframework.boot:spring-boot-dependencies:2.6.7"
}
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-security'
runtimeOnly 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

Приклад без використання Spring BOM:

plugins {
id 'org.springframework.boot' version '2.6.7'
id 'java'
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web:2.6.7'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa:2.6.7'
implementation 'org.springframework.boot:spring-boot-starter-security:2.6.7'
runtimeOnly 'com.h2database:h2:1.4.200'
testImplementation 'org.springframework.boot:spring-boot-starter-test:2.6.7'
}

Як налаштувати безпеку у WebFlux

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.web.server.ServerHttpSecurity
import org.springframework.security.core.userdetails.MapReactiveUserDetailsService
import org.springframework.security.core.userdetails.User
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.web.server.SecurityWebFilterChain
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.stereotype.Service
import reactor.core.publisher.Mono
@Configuration
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
class SecurityConfig {
@Bean
fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
return http.authorizeExchange { exchanges ->
exchanges.pathMatchers("/public/**")
.permitAll()
.anyExchange()
.authenticated()
}
.httpBasic().and()
.formLogin().and()
.csrf().disable()
.build()
}
@Bean
fun userDetailsService(): MapReactiveUserDetailsService {
val user: UserDetails = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build()
val admin: UserDetails = User.withDefaultPasswordEncoder()
.username("admin")
.password("password")
.roles("ADMIN")
.build()
return MapReactiveUserDetailsService(user, admin)
}
}
@Service
class SecuredService {
@PreAuthorize("hasRole('ADMIN')")
fun adminMethod(): Mono<String> {
return Mono.just("Admin access granted")
}
@PreAuthorize("hasRole('USER')")
fun userMethod(): Mono<String> {
return Mono.just("User access granted")
}
}

Використовується анотація @EnableWebFluxSecurity для увімкнення безпеки WebFlux.
Додано анотацію @EnableReactiveMethodSecurity для увімкнення безпеки на рівні методів.
Конфігураційний клас SecurityConfig визначає два біни: securityWebFilterChain і userDetailsService.
Метод securityWebFilterChain налаштовує правила безпеки, дозволяючи доступ до публічних URL (наприклад, /public/**) усім користувачам, і вимагаючи автентифікації для всіх інших запитів. Також увімкнені базова та форма автентифікації, а CSRF захист вимкнено.
Метод userDetailsService створює двох користувачів із ролями “USER” і “ADMIN” з використанням простого методу шифрування паролів.
Клас SecuredService містить методи, захищені анотаціями @PreAuthorize, які обмежують доступ до методів залежно від ролей користувачів. Метод adminMethod доступний лише користувачам із роллю “ADMIN”, а метод userMethod — користувачам із роллю “USER”.

Ще приклад:

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.HttpStatus
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity
import org.springframework.security.config.web.server.ServerHttpSecurity
import org.springframework.security.crypto.password.NoOpPasswordEncoder
import org.springframework.security.crypto.password.PasswordEncoder
import org.springframework.security.web.server.SecurityWebFilterChain
import reactor.core.publisher.Mono
@Configuration
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
class WebSecurityConfig(
private val authenticationManager: AuthenticationManager,
private val securityContextRepository: SecurityContextRepository
) {
@Bean
fun passwordEncoder(): PasswordEncoder {
return NoOpPasswordEncoder.getInstance()
}
@Bean
fun securityWebFilterChain(httpSecurity: ServerHttpSecurity): SecurityWebFilterChain {
return httpSecurity.exceptionHandling()
.authenticationEntryPoint { swe, _ ->
Mono.fromRunnable {
swe.response.statusCode = HttpStatus.UNAUTHORIZED
}
}
.accessDeniedHandler { swe, _ ->
Mono.fromRunnable {
swe.response.statusCode = HttpStatus.FORBIDDEN
}
}
.and()
.csrf().disable()
.formLogin().disable()
.httpBasic().disable()
.authenticationManager(authenticationManager)
.securityContextRepository(securityContextRepository)
.authorizeExchange()
.pathMatchers("/", "/login", "/favicon.ico").permitAll()
.pathMatchers("/controller").hasRole("ADMIN")
.anyExchange().authenticated()
.and()
.build()
}
}

Як налаштувати валідацію даних у Spring

Spring Framework надає потужні та гнучкі інструменти для валідації даних, включно з анотаціями, вбудованими валідаційними механізмами та підтримкою користувацьких валідаторів.

dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
import javax.validation.constraints.Email
import javax.validation.constraints.NotBlank
import javax.validation.constraints.Size
data class User(
@field:NotBlank(message = "Name is mandatory")
val name: String,
@field:Email(message = "Email should be valid")
@field:NotBlank(message = "Email is mandatory")
val email: String,
@field:Size(min = 8, message = "Password should be at least 8 characters")
val password: String
)
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*
import javax.validation.Valid
@RestController
@RequestMapping("/api/users")
class UserController {
@PostMapping("/register")
fun registerUser(@Valid @RequestBody user: User): ResponseEntity<String> {
// У реальному застосунку тут буде логіка реєстрації користувача
return ResponseEntity("User registered successfully", HttpStatus.OK)
}
}

Обробка помилок валідації:

import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.MethodArgumentNotValidException
import org.springframework.web.bind.annotation.ControllerAdvice
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.ResponseStatus
@ControllerAdvice
class ValidationHandler {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException::class)
fun handleValidationExceptions(ex: MethodArgumentNotValidException): ResponseEntity<Map<String, String>> {
val errors: MutableMap<String, String> = HashMap()
ex.bindingResult.fieldErrors.forEach { error ->
errors[error.field] = error.defaultMessage ?: "Invalid value"
}
return ResponseEntity(errors, HttpStatus.BAD_REQUEST)
}
}

Приклад запиту:

POST /api/users/register
Content-Type: application/json
{
"name": "",
"email": "invalid-email",
"password": "short"
}

Приклад відповіді:

{
"name": "Name is mandatory",
"email": "Email should be valid",
"password": "Password should be at least 8 characters"
}

Як визначати витоки пам'яті у Spring

Витоки пам'яті можуть бути серйозною проблемою для будь-яких застосунків, включно зі Spring-застосунками. Виявлення та усунення витоків пам'яті вимагає систематичного підходу, використання інструментів для профілювання та моніторингу, а також знання типових причин витоків пам'яті в Java-застосунках.

Основні кроки для виявлення витоків пам'яті:
Моніторинг пам'яті в реальному часі
Використання інструментів для профілювання
Аналіз дампів пам'яті
Використання логів і метрик
Пошук типових проблемних місць

Моніторинг пам'яті в реальному часі:
Для моніторингу використання пам'яті в реальному часі можна використовувати такі інструменти, як JVisualVM, JConsole або зовнішні APM (Application Performance Monitoring) рішення, такі як New Relic, Dynatrace або Prometheus.

Використання інструментів для профілювання:
Профілювальники допомагають виявляти витоки пам'яті, показуючи, які об'єкти займають багато пам'яті та як вони пов'язані один з одним.

JVisualVM:
Запустіть JVisualVM, який постачається з JDK.
Підключіться до працюючого JVM-процесу.
Перейдіть на вкладку Memory і натисніть Heap Dump для створення дампа пам'яті.
Аналізуйте кількість об'єктів і їхні утримувані розміри.

YourKit:
YourKit Java Profiler надає потужні можливості для аналізу пам'яті, включно зі збором дампів, аналізом витоків і профілюванням виконання.
Запустіть YourKit і підключіться до JVM.
Створіть дамп пам'яті та проаналізуйте його за допомогою вбудованих інструментів.

Аналіз дампів пам'яті:
Аналіз дампів пам'яті допомагає зрозуміти, які об'єкти не звільняються з пам'яті.

Eclipse Memory Analyzer (MAT):
Використовуйте Eclipse MAT для аналізу дампів пам'яті.
Відкрийте дамп пам'яті та використовуйте інструменти, такі як Leak Suspects Report і Histogram для виявлення проблем.

Використання логів і метрик:
Логи та метрики можуть допомогти у виявленні проблем із пам'яттю.

Spring Boot Actuator:
Використовуйте метрики, такі як jvm.memory.used, для моніторингу використання пам'яті.

dependencies {
implementation("org.springframework.boot:spring-boot-starter-actuator")
}
management.endpoints.web.exposure.include=*

Garbage Collection Logs:
Увімкніть логи збирача сміття у вашому застосунку, додавши такі параметри JVM:

-Xlog:gc*:file=gc.log:time

Пошук типових проблемних місць:
Деякі поширені причини витоків пам'яті у Spring-застосунках включають

Невидалювані кешовані об'єкти:
Переконайтеся, що кеші налаштовані правильно й об'єкти видаляються з кешу в міру необхідності.

Неправильне використання синглтонів:
Зверніть увагу на використання синглтонів і перевірте, що вони не утримують посилання на об'єкти, які мають бути зібрані збирачем сміття.

Витоки в сесіях:
Переконайтеся, що об'єкти сесій не зберігаються довше, ніж це необхідно.

Незвільнювані ресурси:
Перевірте, що всі зовнішні ресурси (файли, мережеві з'єднання тощо) закриваються коректно.

Приклад використання JVisualVM для виявлення витоків пам'яті:

Запуск Spring Boot застосунку:
Запустіть ваш Spring Boot застосунок.
Підключення JVisualVM:
Відкрийте JVisualVM із JDK.
Підключіться до вашого JVM процесу, який виконує Spring Boot застосунок.

Збір даних про пам'ять:
Перейдіть на вкладку Monitor для спостереження за загальним використанням пам'яті.
Перейдіть на вкладку Sampler і почніть профілювання пам'яті.

Створення та аналіз дампа пам'яті:
Перейдіть на вкладку Heap Dump і створіть дамп пам'яті.
Проаналізуйте дамп пам'яті на предмет великої кількості об'єктів одного типу або об'єктів, яких не має бути в пам'яті.

Що таке RouterFunction і RequestPredicate у Spring WebFlux

RouterFunction це концепція у Spring WebFlux, яка надає функціональний спосіб визначення маршрутів для обробки HTTP-запитів.

Замість анотацій, таких як @RequestMapping, використовується функціональний підхід для конфігурування маршрутів. Це дозволяє створювати маршрути декларативніше та гнучкіше.

Основні особливості RouterFunction:

Функціональний стиль:
Використовує функціональні інтерфейси для визначення маршрутів і обробників запитів.

Декларативність:
Маршрути та обробники визначаються в коді, що дозволяє легше керувати маршрутизацією.

Асинхронність:
Повністю підтримує асинхронне та неблокуюче програмування, що робить його придатним для високонавантажених застосунків.

RequestPredicate:
інтерфейс у Spring WebFlux, який використовується для перевірки відповідності вхідного HTTP-запиту певним умовам. Він відіграє ключову роль у функціональному програмуванні маршрутизації у WebFlux, допомагаючи визначити, які обробники мають обробляти конкретні запити на основі різних критеріїв, таких як шлях, HTTP-метод, заголовки та параметри запиту.

Основні особливості RequestPredicate:

Перевірка умов запиту:
Дозволяє перевіряти відповідність запитів певним умовам, таким як шляхи, методи, заголовки та параметри запиту.

Поєднання умов:
Можна комбінувати кілька умов за допомогою методів and(), or() і negate() для створення складних предикатів.

Використання в маршрутизації:
Використовується разом із RouterFunction для визначення маршрутів у функціональному стилі.

andRoute:
використовується для ланцюжка кількох маршрутів у функціональному стилі маршрутизації. Це дозволяє об'єднувати кілька маршрутів разом, щоб вони могли оброблятися однією конфігурацією маршрутизації.

dependencies {
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
import org.springframework.http.MediaType
import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.server.ServerRequest
import org.springframework.web.reactive.function.server.ServerResponse
import reactor.core.publisher.Mono
@Component
class GreetingHandler {
fun hello(request: ServerRequest): Mono<ServerResponse> {
return ServerResponse.ok()
.contentType(MediaType.TEXT_PLAIN)
.bodyValue("Hello, Spring WebFlux!")
}
fun index(request: ServerRequest): Mono<ServerResponse> {
return ServerResponse.ok()
.contentType(MediaType.TEXT_PLAIN)
.bodyValue("Welcome to the home page!")
}
}
import com.example.handler.GreetingHandler
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.http.MediaType
import org.springframework.web.reactive.function.server.RequestPredicate
import org.springframework.web.reactive.function.server.RequestPredicates.*
import org.springframework.web.reactive.function.server.RouterFunction
import org.springframework.web.reactive.function.server.RouterFunctions.route
import org.springframework.web.reactive.function.server.ServerResponse
@Configuration
class GreetingRouter {
@Bean
fun route(greetingHandler: GreetingHandler): RouterFunction<ServerResponse> {
val helloRoute: RequestPredicate = GET("/hello")
.and(accept(MediaType.TEXT_PLAIN))
return route(helloRoute, greetingHandler::hello)
.andRoute(GET("/"), greetingHandler::index)
}
}

або

import org.springframework.http.MediaType
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import reactor.core.publisher.Mono
@RestController
@RequestMapping(produces = [MediaType.TEXT_PLAIN_VALUE])
class GreetingHandler {
@GetMapping("/hello")
fun hello(): Mono<String> {
return Mono.just("Hello, Spring WebFlux!")
}
@GetMapping("/")
fun index(): Mono<String> {
return Mono.just("Welcome to the home page!")
}
}

ще приклади:

data class User(
val id: Long,
val name: String,
val email: String
)
import org.springframework.stereotype.Service
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
@Service
class UserService {
private val users = listOf(
User(1, "John Doe", "john@example.com"),
User(2, "Jane Doe", "jane@example.com")
)
fun getAllUsers(): Flux<User> {
return Flux.fromIterable(users)
}
fun getUserById(id: Long): Mono<User> {
return Mono.justOrEmpty(users.find { it.id == id })
}
}
import org.springframework.stereotype.Component
import org.springframework.web.reactive.function.server.ServerRequest
import org.springframework.web.reactive.function.server.ServerResponse
import reactor.core.publisher.Mono
@Component
class UserHandler(private val userService: UserService) {
fun getAllUsers(request: ServerRequest): Mono<ServerResponse> {
return ServerResponse.ok().body(userService.getAllUsers(), User::class.java)
}
fun getUserById(request: ServerRequest): Mono<ServerResponse> {
val userId = request.pathVariable("id").toLong()
return userService.getUserById(userId)
.flatMap { user -> ServerResponse.ok().bodyValue(user) }
.switchIfEmpty(ServerResponse.notFound().build())
}
fun getUsersByHeader(request: ServerRequest): Mono<ServerResponse> {
val headerValue = request.headers().firstHeader("X-USER-ROLE") ?: return ServerResponse.badRequest().build()
return ServerResponse.ok().body(userService.getUsersByRole(headerValue), User::class.java)
}
}
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.web.reactive.function.server.RequestPredicates.*
import org.springframework.web.reactive.function.server.RouterFunction
import org.springframework.web.reactive.function.server.RouterFunctions.route
import org.springframework.web.reactive.function.server.ServerResponse
@Configuration
class RouterConfig {
@Bean
fun userRoutes(userHandler: UserHandler): RouterFunction<ServerResponse> {
return route()
.GET("/users", userHandler::getAllUsers)
.GET("/users/{id}", userHandler::getUserById)
.GET("/users/role", headers("X-USER-ROLE"), userHandler::getUsersByHeader)
.build()
}
}

RequestPredicate:
Інтерфейс, що визначає умову, якій має відповідати запит. Використовується для створення умов маршрутизації.

Приклади предикатів:

GET(“/users”):
Перевіряє, що запит використовує метод GET і шлях “/users”.

GET(“/users/{id}”):
Перевіряє, що запит використовує метод GET і шлях “/users/{id}”.

headers(“X-USER-ROLE”):
Перевіряє, що запит містить заголовок “X-USER-ROLE”.

Комбінування предикатів:
Предикати можна комбінувати за допомогою методів and(), or() і negate(). Наприклад, можна створити складну умову, що перевіряє і шлях, і наявність заголовка, і метод HTTP.

Як у Spring зробити фонове завдання за таймером

Для виконання фонових завдань за таймером у Spring можна використовувати анотацію @Scheduled, яка дозволяє вам планувати виконання методів із заданою періодичністю.

Увімкнення підтримки завдань за розкладом:
Для початку потрібно увімкнути підтримку анотації @Scheduled у вашому застосунку. Це робиться за допомогою анотації @EnableScheduling в одному з конфігураційних класів або в основному класі застосунку.

import org.springframework.context.annotation.Configuration
import org.springframework.scheduling.annotation.EnableScheduling
@Configuration
@EnableScheduling
class SchedulerConfig

Створіть сервіс для фонових завдань:
Створіть сервіс, у якому будуть знаходитися методи, що виконуються за розкладом. Ці методи потрібно анотувати @Scheduled.

import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Service
import java.time.LocalDateTime
@Service
class ScheduledTasks {
@Scheduled(fixedRate = 5000)
fun reportCurrentTime() {
println("Поточний час: ${LocalDateTime.now()}")
}
}

Параметри анотації @Scheduled:

fixedRate:
Визначає фіксований інтервал між запусками методу, у мілісекундах. Наприклад, fixedRate = 300000 означає, що метод виконуватиметься кожні 5 хвилин (300000 мілісекунд).

fixedDelay:
Визначає інтервал між завершенням останнього запуску методу та початком наступного. Наприклад, fixedDelay = 300000 також запустить метод кожні 5 хвилин, але лише після завершення попереднього запуску.

@Scheduled(fixedDelay = 5000)
fun taskWithFixedDelay() {
println("Завдання з fixedDelay виконується: ${LocalDateTime.now()}")
}

initialDelay:
Визначає затримку перед першим запуском методу, у мілісекундах. Наприклад, initialDelay = 10000 означає, що метод буде виконано через 10 секунд після запуску застосунку.

@Scheduled(initialDelay = 10000, fixedRate = 5000)
fun taskWithInitialDelay() {
println("Завдання з initialDelay виконується: ${LocalDateTime.now()}")
}

cron:
Дозволяє задати розклад у форматі cron. Наприклад, cron = "0 0 * * * *" запустить метод щогодини.

Приклад використання cron:
Цей метод виконуватиметься щодня о 00:00.

@Scheduled(cron = "0 0/1 * 1/1 * ?")
fun taskWithCronExpression() {
println("Завдання з cron виразом виконується щохвилини: ${LocalDateTime.now()}")
}

Налаштування пулу потоків:
За замовчуванням завдання виконуються в одному потоці, що може бути обмеженням, якщо у вас кілька довгих завдань. Щоб використовувати пул потоків, можна налаштувати TaskScheduler.

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler
@Configuration
class SchedulerConfig {
@Bean
fun taskScheduler(): ThreadPoolTaskScheduler {
val scheduler = ThreadPoolTaskScheduler()
scheduler.poolSize = 10
scheduler.setThreadNamePrefix("scheduled-task-pool-")
return scheduler
}
}

Тепер завдання виконуватимуться в окремому пулі потоків, що дозволяє запускати кілька завдань паралельно.

Як у Spring імпортувати дані з CSV

Для імпорту CSV-файлів у Spring можна використовувати різні бібліотеки, такі як OpenCSV або Apache Commons CSV. Ось приклад використання OpenCSV для імпорту даних із CSV-файлу у Spring-застосунку.

dependencies {
implementation 'com.opencsv:opencsv:5.7.1'
}
data class Person(
val id: Long,
val name: String,
val age: Int,
val email: String
)
import com.opencsv.bean.CsvToBeanBuilder
import org.springframework.stereotype.Service
import org.springframework.web.multipart.MultipartFile
import java.io.InputStreamReader
@Service
class CsvService {
fun importCsv(file: MultipartFile): List<Person> {
val reader = InputStreamReader(file.inputStream)
val csvToBean = CsvToBeanBuilder<Person>(reader)
.withType(Person::class.java)
.withIgnoreLeadingWhiteSpace(true)
.build()
return csvToBean.parse()
}
}
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.multipart.MultipartFile
@RestController
class CsvController(private val csvService: CsvService) {
@PostMapping("/import")
fun importCsv(@RequestParam("file") file: MultipartFile): ResponseEntity<List<Person>> {
return try {
val people = csvService.importCsv(file)
ResponseEntity(people, HttpStatus.OK)
} catch (e: Exception) {
ResponseEntity(HttpStatus.BAD_REQUEST)
}
}
}

Тепер ви можете протестувати ваш API, надіславши POST-запит на /import із завантаженим CSV-файлом.

Приклад CSV-файлу

id,name,age,email
1,John Doe,30,johndoe@example.com
2,Jane Smith,25,janesmith@example.com

Copyright: Roman Kryvolapov