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# SMTP портspring.mail.port=587# Потребителско име за SMTPspring.mail.username[email protected]# Парола за 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 { 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.
Типична употреба: Legacy проекти и прости 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:
Все още се използва в legacy проекти, но употребата му намалява.
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>) { runApplication<MyApplication>(*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, за да създадете дъмп на паметта.
Анализирайте броя на обектите и техните retained размери.
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 приложения са
Кеширани обекти, които не се премахват:
Уверете се, че кешовете са настроени правилно и обектите се премахват от кеша при необходимост.
Неправилно използване на синглтони:
Обърнете внимание на използването на синглтони и проверете дали не държат референции към обекти, които трябва да бъдат събрани от garbage collector.
Изтичане в сесиите:
Уверете се, че обектите на сесиите не се запазват по-дълго, отколкото е необходимо.
Неосвободени ресурси:
Проверете дали всички външни ресурси (файлове, мрежови връзки и т.н.) се затварят коректно.
Пример за използване на 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( ) 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:
Този метод ще се изпълнява всяка минута.
@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,email