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 консолі:
# Увімкнення консолі H2spring.h2.console.enabled=true# Шлях доступу до консолі H2spring.h2.console.path=/h2-consoleКонфігурація сервера:
# Порт, на якому запускається серверserver.port=8080# Кодування за замовчуваннямserver.servlet.encoding.charset=UTF-8server.servlet.encoding.enabled=trueserver.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=INFOlogging.level.org.springframework.web=DEBUG# Шлях до файлу логівlogging.file.name=logs/spring-boot-application.log# Формат виведення логівlogging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} - %msg%nlogging.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# Порт SMTPspring.mail.port=587# Ім'я користувача для SMTPspring.mail.username=myemail@example.com# Пароль для SMTPspring.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.Configurationimport org.springframework.web.socket.config.annotation.EnableWebSocketimport org.springframework.web.socket.config.annotation.WebSocketConfigurerimport org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry
@Configuration@EnableWebSocketclass WebSocketConfig(private val webSocketHandler: MyWebSocketHandler) : WebSocketConfigurer { override fun registerWebSocketHandlers(registry: WebSocketHandlerRegistry) { registry.addHandler(webSocketHandler, "/ws").setAllowedOrigins("*") }}Реалізація обробника WebSocket
import org.springframework.stereotype.Componentimport org.springframework.web.socket.TextMessageimport org.springframework.web.socket.WebSocketSessionimport org.springframework.web.socket.handler.TextWebSocketHandler
@Componentclass 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
@Repositoryclass 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.GraphQLQueryResolverimport org.springframework.stereotype.Component
@Componentclass 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.GraphQLMutationResolverimport org.springframework.stereotype.Component
data class CreateUserInput(val name: String, val email: String)
@Componentclass 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_IDspring.security.oauth2.client.registration.google.client-secret=YOUR_CLIENT_SECRETspring.security.oauth2.client.registration.google.scope=profile,emailspring.security.oauth2.client.registration.google.redirect-uri={baseUrl}/login/oauth2/code/{registrationId}spring.security.oauth2.client.registration.google.client-name=Googlespring.security.oauth2.client.provider.google.authorization-uri=https://accounts.google.com/o/oauth2/authspring.security.oauth2.client.provider.google.token-uri=https://oauth2.googleapis.com/tokenspring.security.oauth2.client.provider.google.user-info-uri=https://www.googleapis.com/oauth2/v3/userinfospring.security.oauth2.client.provider.google.user-name-attribute=subimport org.springframework.security.core.annotation.AuthenticationPrincipalimport org.springframework.security.oauth2.core.oidc.user.OidcUserimport org.springframework.stereotype.Controllerimport org.springframework.ui.Modelimport org.springframework.web.bind.annotation.GetMapping
@Controllerclass 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.Beanimport org.springframework.context.annotation.Configurationimport org.springframework.security.config.annotation.web.builders.HttpSecurityimport org.springframework.security.config.annotation.web.configuration.EnableWebSecurityimport org.springframework.security.web.SecurityFilterChain
@Configuration@EnableWebSecurityclass 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/dataimport org.springframework.context.annotation.Beanimport org.springframework.context.annotation.Configurationimport org.springframework.web.client.RestTemplate
@Configurationclass AppConfig { @Bean fun restTemplate(): RestTemplate { return RestTemplate() }}import org.springframework.beans.factory.annotation.Autowiredimport org.springframework.stereotype.Serviceimport org.springframework.web.client.RestTemplate
@Serviceclass ApiService(@Autowired private val restTemplate: RestTemplate) { fun getDataFromExternalApi(url: String): String? { return restTemplate.getForObject(url, String::class.java) }}import org.springframework.beans.factory.annotation.Autowiredimport org.springframework.http.HttpEntityimport org.springframework.http.HttpMethodimport org.springframework.http.ResponseEntityimport org.springframework.stereotype.Serviceimport org.springframework.web.client.RestTemplate
data class RequestObject(val name: String, val value: String)data class ResponseObject(val id: Long, val status: String)
@Serviceclass 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.comimport org.springframework.context.annotation.Beanimport org.springframework.context.annotation.Configurationimport org.springframework.web.reactive.function.client.WebClient
@Configurationclass WebClientConfig { @Bean fun webClient(): WebClient { return WebClient.builder().build() }}import org.springframework.beans.factory.annotation.Autowiredimport org.springframework.stereotype.Serviceimport org.springframework.web.reactive.function.client.WebClient
@Serviceclass 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.Autowiredimport org.springframework.stereotype.Serviceimport org.springframework.web.reactive.function.client.WebClientimport reactor.core.publisher.Mono
data class RequestObject(val name: String, val value: String)data class ResponseObject(val id: Long, val status: String)
@Serviceclass 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.comimport org.springframework.cloud.openfeign.EnableFeignClientsimport org.springframework.context.annotation.Configuration
@Configuration@EnableFeignClientsclass FeignConfigimport org.springframework.cloud.openfeign.FeignClientimport 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.Autowiredimport org.springframework.stereotype.Service
@Serviceclass 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.HttpGetimport org.apache.http.impl.client.CloseableHttpClientimport org.apache.http.impl.client.HttpClientsimport 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.OkHttpClientimport 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.Retrofitimport retrofit2.converter.gson.GsonConverterFactoryimport retrofit2.http.Bodyimport retrofit2.http.GETimport 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.WebClientimport 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.OkHttpimport org.http4k.core.Methodimport org.http4k.core.Requestimport org.http4k.core.Responseimport org.http4k.format.Gson.autoimport 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
@Serializabledata class RequestObject(val name: String, val value: String)
@Serializabledata 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.Beanimport org.springframework.context.annotation.Configurationimport org.springframework.security.config.annotation.web.builders.HttpSecurityimport org.springframework.security.config.annotation.web.configuration.EnableWebSecurityimport org.springframework.security.core.userdetails.Userimport org.springframework.security.core.userdetails.UserDetailsServiceimport org.springframework.security.provisioning.InMemoryUserDetailsManagerimport org.springframework.security.crypto.bcrypt.BCryptPasswordEncoderimport org.springframework.security.crypto.password.PasswordEncoderimport org.springframework.security.web.SecurityFilterChain
@Configuration@EnableWebSecurityclass 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=yourEncryptionPasswordimport org.jasypt.encryption.StringEncryptorimport org.jasypt.encryption.pbe.StandardPBEStringEncryptorimport org.jasypt.spring31.properties.EncryptablePropertyPlaceholderConfigurerimport org.springframework.context.annotation.Beanimport org.springframework.context.annotation.Configurationimport org.springframework.core.env.Environmentimport java.util.*
@Configurationclass 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.AES256TextEncryptorimport org.springframework.stereotype.Service
@Serviceclass 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.GetMappingimport org.springframework.web.bind.annotation.RequestParamimport org.springframework.web.bind.annotation.RestController
@RestControllerclass 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.
keytool -genkeypair -alias myalias -keyalg RSA -keysize 2048 -storetype PKCS12 -keystore keystore.p12 -validity 3650myalias:
псевдонім ключа.
keystore.p12:
ім'я файлу сховища ключів.
validity 3650:
термін дії сертифіката (у днях).
Під час виконання цієї команди вас попросять ввести різні дані, такі як пароль для сховища ключів, ваше ім'я, організація тощо. Після цього буде створено файл keystore.p12.
Налаштування Spring Boot для використання SSL:
Додайте налаштування SSL у файл application.properties.
server.port=8443server.ssl.key-store=classpath:keystore.p12server.ssl.key-store-password=your_passwordserver.ssl.key-store-type=PKCS12server.ssl.key-alias=myaliasПереміщення файлу keystore.p12 до теки ресурсів
Перемістіть файл keystore.p12 до теки src/main/resources вашого проєкту, щоб Spring Boot міг його знайти.
Оновлення конфігурації безпеки (якщо потрібно)
Якщо ваш застосунок використовує Spring Security, ви можете налаштувати його для підтримки HTTPS.
import org.springframework.context.annotation.Beanimport org.springframework.context.annotation.Configurationimport org.springframework.security.config.annotation.web.builders.HttpSecurityimport org.springframework.security.config.annotation.web.configuration.EnableWebSecurityimport org.springframework.security.web.SecurityFilterChain
@Configuration@EnableWebSecurityclass 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
# Увімкнення всіх кінцевих точок Actuatormanagement.endpoints.web.exposure.include=*# Налаштування шляху до кінцевих точок Actuatormanagement.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.Configurationimport org.springframework.security.config.annotation.web.builders.HttpSecurityimport org.springframework.security.config.annotation.web.configuration.EnableWebSecurityimport org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
@Configuration@EnableWebSecurityclass 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.Beanimport org.springframework.context.annotation.Configurationimport org.springframework.security.config.annotation.web.builders.HttpSecurityimport org.springframework.security.config.annotation.web.configuration.EnableWebSecurityimport org.springframework.security.core.userdetails.Userimport org.springframework.security.core.userdetails.UserDetailsServiceimport org.springframework.security.provisioning.InMemoryUserDetailsManagerimport org.springframework.security.web.SecurityFilterChain
@Configuration@EnableWebSecurityclass 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.Controllerimport org.springframework.ui.Modelimport org.springframework.web.bind.annotation.GetMapping
@Controllerclass 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.*
@Entitydata 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.Entityimport javax.persistence.GeneratedValueimport javax.persistence.GenerationTypeimport javax.persistence.Id
@Entitydata 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.GrantedAuthorityimport org.springframework.security.core.authority.SimpleGrantedAuthorityimport 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.UserDetailsimport org.springframework.security.core.userdetails.UserDetailsServiceimport org.springframework.security.core.userdetails.UsernameNotFoundExceptionimport org.springframework.stereotype.Service
@Serviceclass 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.Serviceimport org.springframework.transaction.annotation.Transactional
@Service@Transactionalclass 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.Beanimport org.springframework.context.annotation.Configurationimport org.springframework.security.config.annotation.web.builders.HttpSecurityimport org.springframework.security.config.annotation.web.configuration.EnableWebSecurityimport org.springframework.security.core.userdetails.UserDetailsServiceimport org.springframework.security.crypto.bcrypt.BCryptPasswordEncoderimport org.springframework.security.crypto.password.PasswordEncoderimport org.springframework.security.web.SecurityFilterChain
@Configuration@EnableWebSecurityclass 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.CommandLineRunnerimport org.springframework.stereotype.Component
@Componentclass 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.PreAuthorizeimport org.springframework.stereotype.Controllerimport org.springframework.ui.Modelimport org.springframework.web.bind.annotation.GetMapping
@Controllerclass 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.Securedimport org.springframework.stereotype.Controllerimport org.springframework.ui.Modelimport org.springframework.web.bind.annotation.GetMapping
@Controllerclass 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 зберігання користувачів — найпростіший спосіб для створення користувачів. Ви вже бачили приклад вище. Ось іще раз приклад для ясності:
@Beanfun 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.Beanimport org.springframework.context.annotation.Configurationimport org.springframework.security.config.annotation.web.builders.HttpSecurityimport org.springframework.security.config.annotation.web.configuration.EnableWebSecurityimport org.springframework.security.core.userdetails.Userimport org.springframework.security.provisioning.JdbcUserDetailsManagerimport org.springframework.security.provisioning.UserDetailsManagerimport org.springframework.security.web.SecurityFilterChainimport javax.sql.DataSource
@Configuration@EnableWebSecurityclass 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:testdbspring.datasource.driverClassName=org.h2.Driverspring.datasource.username=saspring.datasource.password=passwordspring.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.UserDetailsimport org.springframework.security.core.userdetails.UserDetailsServiceimport org.springframework.security.core.userdetails.UsernameNotFoundExceptionimport org.springframework.stereotype.Service
@Serviceclass 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.Beanimport org.springframework.context.annotation.Configurationimport org.springframework.security.config.annotation.web.builders.HttpSecurityimport org.springframework.security.config.annotation.web.configuration.EnableWebSecurityimport org.springframework.security.core.userdetails.UserDetailsServiceimport org.springframework.security.web.SecurityFilterChain
@Configuration@EnableWebSecurityclass 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.Beanimport org.springframework.context.annotation.Configurationimport org.springframework.integration.annotation.ServiceActivatorimport org.springframework.integration.channel.DirectChannelimport org.springframework.integration.config.EnableIntegrationimport org.springframework.integration.core.MessageHandlerimport org.springframework.integration.jms.dsl.Jmsimport org.springframework.jms.annotation.EnableJmsimport org.springframework.jms.core.JmsTemplateimport javax.jms.ConnectionFactory
@Configuration@EnableIntegration@EnableJmsclass 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.JmsTemplateimport org.springframework.stereotype.Component
@Componentclass JmsProducer(private val jmsTemplate: JmsTemplate) { fun sendMessage(destination: String, message: String) { jmsTemplate.convertAndSend(destination, message) }}import org.springframework.boot.autoconfigure.SpringBootApplicationimport org.springframework.boot.runApplicationimport org.springframework.context.annotation.Beanimport javax.annotation.PostConstruct
@SpringBootApplicationclass 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.Jobimport org.springframework.batch.core.Stepimport org.springframework.batch.core.configuration.annotation.EnableBatchProcessingimport org.springframework.batch.core.configuration.annotation.JobBuilderFactoryimport org.springframework.batch.core.configuration.annotation.StepBuilderFactoryimport org.springframework.batch.core.launch.support.RunIdIncrementerimport org.springframework.batch.item.ItemProcessorimport org.springframework.batch.item.ItemReaderimport org.springframework.batch.item.ItemWriterimport org.springframework.batch.item.file.FlatFileItemReaderimport org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapperimport org.springframework.batch.item.file.mapping.DefaultLineMapperimport org.springframework.batch.item.file.mapping.DelimitedLineTokenizerimport org.springframework.batch.item.file.transform.LineTokenizerimport org.springframework.batch.item.file.transform.Rangeimport org.springframework.batch.item.support.ListItemReaderimport org.springframework.beans.factory.annotation.Valueimport org.springframework.context.annotation.Beanimport org.springframework.context.annotation.Configurationimport org.springframework.core.io.ClassPathResourceimport javax.sql.DataSource
@Configuration@EnableBatchProcessingclass 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,DoeJane,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;
@Configurationpublic 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.SpringBootApplicationimport org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfigurationimport 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.
app.base.url=https://dev.example.comapp.base.url=https://test.example.comapp.base.url=https://prod.example.comНалаштування основного файлу конфігурації:
В основному файлі application.properties або application.yml можна визначити загальні налаштування та вказати активний профіль за замовчуванням (необов'язково).
spring.profiles.active=devЧитання конфігурації у вашому коді:
Використовуйте анотацію @Value або @ConfigurationProperties для отримання значення base URL із конфігураційних файлів.
import org.springframework.beans.factory.annotation.Valueimport org.springframework.context.annotation.Configuration
@Configurationclass AppConfig { @Value("\${app.base.url}") lateinit var baseUrl: String}Використання профілів у коді:
Ви можете також використовувати анотацію @Profile для увімкнення або вимкнення бінів залежно від активного профілю.
import org.springframework.context.annotation.Profileimport 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.Entityimport javax.persistence.Idimport javax.persistence.GeneratedValueimport javax.persistence.GenerationType
@Entitydata 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
@Serviceclass 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.Idimport 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.ReactiveMongoRepositoryimport reactor.core.publisher.Mono
interface UserRepository : ReactiveMongoRepository<User, String>import org.springframework.stereotype.Serviceimport reactor.core.publisher.Fluximport reactor.core.publisher.Mono
@Serviceclass 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.Fluximport 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 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.ApplicationContextimport 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.Beanimport org.springframework.context.annotation.Configuration
@Configurationclass AppConfig { @Bean fun exampleBean(): ExampleBean { return ExampleBean("Hello, Spring with Kotlin DSL!") }}import org.springframework.context.ApplicationContextimport 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 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.BeanFactoryimport org.springframework.beans.factory.xml.XmlBeanFactoryimport 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.ApplicationContextimport 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.Beanimport org.springframework.context.annotation.Configuration
@Configurationclass AppConfig { @Bean fun myBean(): MyBean { return MyBean().apply { name = "Bean Name" } }}import org.springframework.beans.factory.BeanFactoryimport org.springframework.stereotype.Service
@Serviceclass 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.Beanimport org.springframework.context.annotation.Configurationimport org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurityimport org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurityimport org.springframework.security.config.web.server.ServerHttpSecurityimport org.springframework.security.core.userdetails.MapReactiveUserDetailsServiceimport org.springframework.security.core.userdetails.Userimport org.springframework.security.core.userdetails.UserDetailsimport org.springframework.security.web.server.SecurityWebFilterChainimport org.springframework.security.access.prepost.PreAuthorizeimport org.springframework.stereotype.Serviceimport reactor.core.publisher.Mono
@Configuration@EnableWebFluxSecurity@EnableReactiveMethodSecurityclass 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) }}
@Serviceclass 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.Beanimport org.springframework.context.annotation.Configurationimport org.springframework.http.HttpStatusimport org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurityimport org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurityimport org.springframework.security.config.web.server.ServerHttpSecurityimport org.springframework.security.crypto.password.NoOpPasswordEncoderimport org.springframework.security.crypto.password.PasswordEncoderimport org.springframework.security.web.server.SecurityWebFilterChainimport reactor.core.publisher.Mono
@Configuration@EnableWebFluxSecurity@EnableReactiveMethodSecurityclass 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.Emailimport javax.validation.constraints.NotBlankimport 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.HttpStatusimport org.springframework.http.ResponseEntityimport 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.HttpStatusimport org.springframework.http.ResponseEntityimport org.springframework.web.bind.MethodArgumentNotValidExceptionimport org.springframework.web.bind.annotation.ControllerAdviceimport org.springframework.web.bind.annotation.ExceptionHandlerimport org.springframework.web.bind.annotation.ResponseStatus
@ControllerAdviceclass 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/registerContent-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.MediaTypeimport org.springframework.stereotype.Componentimport org.springframework.web.reactive.function.server.ServerRequestimport org.springframework.web.reactive.function.server.ServerResponseimport reactor.core.publisher.Mono
@Componentclass 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.GreetingHandlerimport org.springframework.context.annotation.Beanimport org.springframework.context.annotation.Configurationimport org.springframework.http.MediaTypeimport org.springframework.web.reactive.function.server.RequestPredicateimport org.springframework.web.reactive.function.server.RequestPredicates.*import org.springframework.web.reactive.function.server.RouterFunctionimport org.springframework.web.reactive.function.server.RouterFunctions.routeimport org.springframework.web.reactive.function.server.ServerResponse
@Configurationclass 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.MediaTypeimport org.springframework.web.bind.annotation.GetMappingimport org.springframework.web.bind.annotation.RequestMappingimport org.springframework.web.bind.annotation.RestControllerimport 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.Serviceimport reactor.core.publisher.Fluximport reactor.core.publisher.Mono
@Serviceclass 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.Componentimport org.springframework.web.reactive.function.server.ServerRequestimport org.springframework.web.reactive.function.server.ServerResponseimport reactor.core.publisher.Mono
@Componentclass 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.Beanimport org.springframework.context.annotation.Configurationimport org.springframework.web.reactive.function.server.RequestPredicates.*import org.springframework.web.reactive.function.server.RouterFunctionimport org.springframework.web.reactive.function.server.RouterFunctions.routeimport org.springframework.web.reactive.function.server.ServerResponse
@Configurationclass 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.Configurationimport org.springframework.scheduling.annotation.EnableScheduling
@Configuration@EnableSchedulingclass SchedulerConfigСтворіть сервіс для фонових завдань:
Створіть сервіс, у якому будуть знаходитися методи, що виконуються за розкладом. Ці методи потрібно анотувати @Scheduled.
import org.springframework.scheduling.annotation.Scheduledimport org.springframework.stereotype.Serviceimport java.time.LocalDateTime
@Serviceclass 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.Beanimport org.springframework.context.annotation.Configurationimport org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler
@Configurationclass 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.CsvToBeanBuilderimport org.springframework.stereotype.Serviceimport org.springframework.web.multipart.MultipartFileimport java.io.InputStreamReader
@Serviceclass 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.HttpStatusimport org.springframework.http.ResponseEntityimport org.springframework.web.bind.annotation.PostMappingimport org.springframework.web.bind.annotation.RequestParamimport org.springframework.web.bind.annotation.RestControllerimport org.springframework.web.multipart.MultipartFile
@RestControllerclass 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,email1,John Doe,30,johndoe@example.com2,Jane Smith,25,janesmith@example.com