Merge pull request #14 from TouchInstinct/response-wrapper-spring-web

add response-wrapper-spring-web
This commit is contained in:
Alexander Buntakov 2021-06-08 18:21:23 +03:00 committed by GitHub
commit 07d00bc65e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 202 additions and 0 deletions

View File

@ -83,3 +83,7 @@ Interceptor для логирования запросов/ответов.
## version-spring-web
Добавляет возможность задавать версию апи через `properties` без необходимости явно указывать в каждом маппинге
## response-wrapper-spring-web
Добавляет обертку для успешного ответа

View File

@ -0,0 +1,13 @@
plugins {
id("kotlin")
id("kotlin-spring")
id("maven-publish")
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation("org.springframework.boot:spring-boot-starter-web")
testImplementation("org.springframework.boot:spring-boot-starter-test")
}

View File

@ -0,0 +1,8 @@
@file:Suppress("unused")
package ru.touchin.wrapper
import org.springframework.context.annotation.Import
import ru.touchin.wrapper.configurations.SpringResponseWrapper
@Import(value = [SpringResponseWrapper::class])
annotation class EnableSpringResponseWrapper

View File

@ -0,0 +1,39 @@
@file:Suppress("unused")
package ru.touchin.wrapper.advices
import org.springframework.core.MethodParameter
import org.springframework.http.MediaType
import org.springframework.http.converter.HttpMessageConverter
import org.springframework.http.server.ServerHttpRequest
import org.springframework.http.server.ServerHttpResponse
import org.springframework.web.bind.annotation.RestControllerAdvice
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice
import ru.touchin.wrapper.annotations.NoResponseWrap
import ru.touchin.wrapper.annotations.ResponseWrap
import ru.touchin.wrapper.components.ResponseBodyWrapper
@RestControllerAdvice(annotations = [ResponseWrap::class])
class WrapResponseAdvice(
private val responseWrapper: ResponseBodyWrapper
): ResponseBodyAdvice<Any> {
override fun supports(returnType: MethodParameter, converterType: Class<out HttpMessageConverter<*>>): Boolean {
return !returnType.hasMethodAnnotation(NoResponseWrap::class.java)
}
/***
* Не будет работать, если контроллер возвращает тип String, так как по умолчанию будет выбираться конвертер для строки.
* Решить проблему можно так: https://stackoverflow.com/questions/44121648/controlleradvice-responsebodyadvice-failed-to-enclose-a-string-response
*/
override fun beforeBodyWrite(
body: Any?,
returnType: MethodParameter,
selectedContentType: MediaType,
selectedConverterType: Class<out HttpMessageConverter<*>>,
request: ServerHttpRequest,
response: ServerHttpResponse
): Any {
return responseWrapper.wrap(body)
}
}

View File

@ -0,0 +1,4 @@
package ru.touchin.wrapper.annotations
@Target(allowedTargets = [AnnotationTarget.FUNCTION, AnnotationTarget.CLASS])
annotation class NoResponseWrap

View File

@ -0,0 +1,4 @@
package ru.touchin.wrapper.annotations
@Target(allowedTargets = [AnnotationTarget.FUNCTION, AnnotationTarget.CLASS])
annotation class ResponseWrap

View File

@ -0,0 +1,7 @@
package ru.touchin.wrapper.components
interface ResponseBodyWrapper {
fun wrap(body: Any?): Any
}

View File

@ -0,0 +1,22 @@
@file:Suppress("unused")
package ru.touchin.wrapper.components
import org.springframework.stereotype.Component
import ru.touchin.wrapper.dto.DefaultWrapper
import ru.touchin.wrapper.dto.Wrapper
@Component
class ResponseBodyWrapperImpl : ResponseBodyWrapper {
override fun wrap(body: Any?): Any {
if (body is Wrapper) {
return body
}
return DefaultWrapper(
result = body,
errorCode = 0
)
}
}

View File

@ -0,0 +1,9 @@
package ru.touchin.wrapper.configurations
import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.Configuration
@Suppress("SpringFacetCodeInspection")
@Configuration
@ComponentScan("ru.touchin.wrapper.advices", "ru.touchin.wrapper.components")
class SpringResponseWrapper

View File

@ -0,0 +1,8 @@
package ru.touchin.wrapper.dto
@Suppress("unused")
class DefaultWrapper(
override val result: Any?,
val errorCode: Int,
val errorMessage: String? = null
): Wrapper

View File

@ -0,0 +1,5 @@
package ru.touchin.wrapper.dto
interface Wrapper {
val result: Any?
}

View File

@ -0,0 +1,8 @@
package ru.touchin.web
import org.springframework.boot.autoconfigure.SpringBootApplication
import ru.touchin.wrapper.EnableSpringResponseWrapper
@SpringBootApplication
@EnableSpringResponseWrapper
class TestApplication

View File

@ -0,0 +1,70 @@
package ru.touchin.web.wrapper
import org.hamcrest.Matchers
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.http.MediaType
import org.springframework.test.context.ActiveProfiles
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
import org.springframework.test.web.servlet.result.MockMvcResultHandlers
import org.springframework.test.web.servlet.result.MockMvcResultMatchers
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import ru.touchin.wrapper.annotations.NoResponseWrap
import ru.touchin.wrapper.annotations.ResponseWrap
@RestController
@ResponseWrap
@RequestMapping("/wrapper")
class WrapperController {
@GetMapping("/wrap")
fun wrap(): Map<String, String> {
return mapOf("wrap" to "yes")
}
@NoResponseWrap
@GetMapping("/no-wrap")
fun noWrap(): Map<String, String> {
return mapOf("wrap" to "no")
}
}
@ActiveProfiles("test")
@SpringBootTest
@AutoConfigureMockMvc
internal class WrapResponseAdviceMvcTest {
@Autowired
private lateinit var mockMvc: MockMvc
@Test
@DisplayName("Результат должен быть обернут")
fun shouldBeWrappedResponse() {
mockMvc
.perform(MockMvcRequestBuilders.get("/wrapper/wrap"))
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().isOk)
.andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON_VALUE))
.andExpect(MockMvcResultMatchers.jsonPath("$.result.wrap", Matchers.`is`("yes")))
.andExpect(MockMvcResultMatchers.jsonPath("$.errorCode", Matchers.`is`(0)))
}
@Test
@DisplayName("Результат НЕ должен быть обернут")
fun shouldBeNoWrappedResponse() {
mockMvc
.perform(MockMvcRequestBuilders.get("/wrapper/no-wrap"))
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().isOk)
.andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON_VALUE))
.andExpect(MockMvcResultMatchers.jsonPath("$.wrap", Matchers.`is`("no")))
}
}

View File

@ -31,3 +31,4 @@ include("logger-spring-web")
include("exception-handler-spring-web")
include("exception-handler-logger-spring-web")
include("version-spring-web")
include("response-wrapper-spring-web")