diff --git a/README.md b/README.md index 383c859..470c268 100644 --- a/README.md +++ b/README.md @@ -38,3 +38,9 @@ * `models.*` - базовые `Entity` * `repositories` - утилиты и доп. интерфейсы для репозиториев * `EnableJpaAuditingExtra` - подключение `JpaAuditing` с поддержкой типа `ZoneDateTime` + +## common-spring-web + +* `request.Utils` - различные `extensions` для работы с `HttpServletRequest` +* `errors.*` - исключения и типы данных для `web` +* `webclient.*` - классы для расширения webclient, включая логирование diff --git a/common-spring-web/build.gradle.kts b/common-spring-web/build.gradle.kts new file mode 100644 index 0000000..00e2078 --- /dev/null +++ b/common-spring-web/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + id("kotlin") + id("kotlin-spring") + id("maven-publish") +} + +dependencies { + api("com.fasterxml.jackson.module:jackson-module-kotlin") + + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter-webflux") +} diff --git a/common-spring-web/src/main/kotlin/ru/touchin/common/spring/web/dto/ApiError.kt b/common-spring-web/src/main/kotlin/ru/touchin/common/spring/web/dto/ApiError.kt new file mode 100644 index 0000000..c8106d1 --- /dev/null +++ b/common-spring-web/src/main/kotlin/ru/touchin/common/spring/web/dto/ApiError.kt @@ -0,0 +1,13 @@ +package ru.touchin.common.spring.web.dto + +interface ApiError { + + val errorCode: Int + val errorMessage: String? + + companion object { + const val SUCCESS_CODE = 0 + const val FAILURE_CODE = -1 + } + +} diff --git a/common-spring-web/src/main/kotlin/ru/touchin/common/spring/web/dto/DefaultApiError.kt b/common-spring-web/src/main/kotlin/ru/touchin/common/spring/web/dto/DefaultApiError.kt new file mode 100644 index 0000000..7a50737 --- /dev/null +++ b/common-spring-web/src/main/kotlin/ru/touchin/common/spring/web/dto/DefaultApiError.kt @@ -0,0 +1,15 @@ +package ru.touchin.common.spring.web.dto + +class DefaultApiError( + override val errorCode: Int, + override val errorMessage: String? = null +): ApiError { + + companion object { + fun createFailure(errorMessage: String? = null) = DefaultApiError( + errorCode = ApiError.FAILURE_CODE, + errorMessage = errorMessage + ) + } + +} diff --git a/common-spring-web/src/main/kotlin/ru/touchin/common/spring/web/request/RequestUtils.kt b/common-spring-web/src/main/kotlin/ru/touchin/common/spring/web/request/RequestUtils.kt new file mode 100644 index 0000000..5b19c21 --- /dev/null +++ b/common-spring-web/src/main/kotlin/ru/touchin/common/spring/web/request/RequestUtils.kt @@ -0,0 +1,19 @@ +@file:Suppress("unused") +package ru.touchin.common.spring.web.request + +import javax.servlet.http.HttpServletRequest + +object RequestUtils { + + fun HttpServletRequest.doesPrefixMatch(prefixes: List): Boolean { + return prefixes.any { pathPrefix -> + this.requestURI.startsWith(pathPrefix, ignoreCase = true) + } + } + + val HttpServletRequest.publicIp: String get() { + return getHeader("X-Real-Ip") + ?: this.remoteAddr + } + +} diff --git a/common-spring-web/src/main/kotlin/ru/touchin/common/spring/web/webclient/BaseLogWebClient.kt b/common-spring-web/src/main/kotlin/ru/touchin/common/spring/web/webclient/BaseLogWebClient.kt new file mode 100644 index 0000000..fec785e --- /dev/null +++ b/common-spring-web/src/main/kotlin/ru/touchin/common/spring/web/webclient/BaseLogWebClient.kt @@ -0,0 +1,71 @@ +@file:Suppress("unused") +package ru.touchin.common.spring.web.webclient + +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import com.fasterxml.jackson.module.kotlin.KotlinModule +import org.springframework.http.HttpStatus +import org.springframework.http.client.reactive.ClientHttpConnector +import org.springframework.web.reactive.function.client.ClientResponse +import org.springframework.web.reactive.function.client.ExchangeStrategies +import org.springframework.web.reactive.function.client.WebClient +import org.springframework.web.reactive.function.client.bodyToMono +import reactor.core.publisher.Mono +import ru.touchin.common.spring.web.webclient.errors.WebClientStatusException +import ru.touchin.common.spring.web.webclient.logger.WebClientLogger + +abstract class BaseLogWebClient( + private val webClientLogger: WebClientLogger, + private val builder: WebClient.Builder, +) : LogWebClient { + + protected open var strategies: ExchangeStrategies? = null + + protected open var clientConnector: ClientHttpConnector? = null + + override fun getLogger(): WebClientLogger { + return webClientLogger + } + + override fun getObjectMapper(): ObjectMapper { + return defaultObjectMapper + } + + protected inline fun checkServiceAvailable(clientResponse: ClientResponse): Mono { + return when (val status = clientResponse.statusCode()) { + HttpStatus.OK -> clientResponse.bodyToMono() + else -> throw WebClientStatusException( + "Status code $status", + status, + ) + } + } + + protected fun getWebClientBuilder(url: String): WebClient.Builder { + val webClient = builder.baseUrl(url) + + this.clientConnector?.let { + webClient.clientConnector(it) + } + + this.strategies?.let { + webClient.exchangeStrategies(it) + } + + return webClient + } + + abstract fun getWebClient(): WebClient + + companion object { + val defaultObjectMapper = ObjectMapper() + .registerModule(KotlinModule()) + .registerModule(JavaTimeModule()) + .setSerializationInclusion(JsonInclude.Include.NON_NULL) + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + ?: throw IllegalStateException("unable to retrieve ObjectMapper") + } + +} diff --git a/common-spring-web/src/main/kotlin/ru/touchin/common/spring/web/webclient/LogWebClient.kt b/common-spring-web/src/main/kotlin/ru/touchin/common/spring/web/webclient/LogWebClient.kt new file mode 100644 index 0000000..09eac0d --- /dev/null +++ b/common-spring-web/src/main/kotlin/ru/touchin/common/spring/web/webclient/LogWebClient.kt @@ -0,0 +1,38 @@ +@file:Suppress("unused") +package ru.touchin.common.spring.web.webclient + +import com.fasterxml.jackson.databind.ObjectMapper +import org.springframework.web.reactive.function.client.WebClient +import reactor.core.publisher.Mono +import ru.touchin.common.spring.web.webclient.dto.RequestLogData +import ru.touchin.common.spring.web.webclient.logger.WebClientLogger + +interface LogWebClient { + + fun getLogger(): WebClientLogger + + fun getObjectMapper(): ObjectMapper + + fun WebClient.RequestHeadersSpec<*>.exchange( + clazz: Class, + requestLogData: RequestLogData, + ): Mono { + return exchangeToMono { clientResponse -> + clientResponse.bodyToMono(String::class.java) + }.map { responseBody -> + getLogger().log(requestLogData.copy(responseBody = responseBody)) + + parseValue(responseBody, clazz) + } + } + + private fun parseValue(source: String?, clazz: Class): T { + return if (clazz.canonicalName != String::class.java.canonicalName) { + getObjectMapper().readValue(source, clazz) + } else { + @Suppress("UNCHECKED_CAST") + source as T // T is String + } + } + +} diff --git a/common-spring-web/src/main/kotlin/ru/touchin/common/spring/web/webclient/dto/RequestLogData.kt b/common-spring-web/src/main/kotlin/ru/touchin/common/spring/web/webclient/dto/RequestLogData.kt new file mode 100644 index 0000000..b38801f --- /dev/null +++ b/common-spring-web/src/main/kotlin/ru/touchin/common/spring/web/webclient/dto/RequestLogData.kt @@ -0,0 +1,11 @@ +package ru.touchin.common.spring.web.webclient.dto + +import org.springframework.http.HttpMethod + +data class RequestLogData( + val uri: String, + val logTags: List, + val method: HttpMethod, + val requestBody: Any? = null, + val responseBody: Any? = null, +) diff --git a/common-spring-web/src/main/kotlin/ru/touchin/common/spring/web/webclient/errors/WebClientStatusException.kt b/common-spring-web/src/main/kotlin/ru/touchin/common/spring/web/webclient/errors/WebClientStatusException.kt new file mode 100644 index 0000000..809c9c3 --- /dev/null +++ b/common-spring-web/src/main/kotlin/ru/touchin/common/spring/web/webclient/errors/WebClientStatusException.kt @@ -0,0 +1,6 @@ +@file:Suppress("unused") +package ru.touchin.common.spring.web.webclient.errors + +import org.springframework.http.HttpStatus + +class WebClientStatusException(message: String, val status: HttpStatus) : Exception(message) diff --git a/common-spring-web/src/main/kotlin/ru/touchin/common/spring/web/webclient/logger/WebClientLogger.kt b/common-spring-web/src/main/kotlin/ru/touchin/common/spring/web/webclient/logger/WebClientLogger.kt new file mode 100644 index 0000000..58775fb --- /dev/null +++ b/common-spring-web/src/main/kotlin/ru/touchin/common/spring/web/webclient/logger/WebClientLogger.kt @@ -0,0 +1,9 @@ +package ru.touchin.common.spring.web.webclient.logger + +import ru.touchin.common.spring.web.webclient.dto.RequestLogData + +interface WebClientLogger { + + fun log(requestLogData: RequestLogData) + +} diff --git a/settings.gradle.kts b/settings.gradle.kts index c1192c0..fd46427 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -22,3 +22,4 @@ pluginManagement { include("common") include("common-spring") include("common-spring-jpa") +include("common-spring-web")