Merge pull request #11 from TouchInstinct/exception-handler-spring-web
add exception-handler-spring-web
This commit is contained in:
commit
6b3581f652
|
|
@ -71,3 +71,7 @@
|
|||
## logger-spring-web
|
||||
|
||||
Interceptor для логирования запросов/ответов.
|
||||
|
||||
## exception-handler-spring-web
|
||||
|
||||
Перехватывает ошибки сервера, определяет код ошибки и возвращает их в правильный `response`
|
||||
|
|
|
|||
|
|
@ -56,6 +56,9 @@ subprojects {
|
|||
dependency("org.junit.jupiter:junit-jupiter-api:5.4.2")
|
||||
dependency("org.junit.jupiter:junit-jupiter-params:5.4.2")
|
||||
dependency("org.junit.jupiter:junit-jupiter-engine:5.4.2")
|
||||
|
||||
dependency("com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0")
|
||||
dependency("org.mockito:mockito-inline:2.13.0")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
plugins {
|
||||
id("kotlin")
|
||||
id("kotlin-spring")
|
||||
id("maven-publish")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(project(":common-spring-web"))
|
||||
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
|
||||
|
||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||
testImplementation("com.nhaarman.mockitokotlin2:mockito-kotlin")
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package ru.touchin.exception.handler.dto
|
||||
|
||||
import org.springframework.http.HttpStatus
|
||||
import ru.touchin.common.spring.web.dto.ApiError
|
||||
import ru.touchin.common.spring.web.dto.DefaultApiError
|
||||
|
||||
data class ExceptionResolverResult(
|
||||
val apiError: ApiError,
|
||||
val status: HttpStatus,
|
||||
val exception: Exception?,
|
||||
) {
|
||||
|
||||
companion object {
|
||||
fun createInternalError(errorMessage: String?): ExceptionResolverResult {
|
||||
return ExceptionResolverResult(
|
||||
apiError = DefaultApiError.createFailure(errorMessage),
|
||||
status = HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
exception = null
|
||||
)
|
||||
}
|
||||
|
||||
fun createInternalError(exception: Exception?): ExceptionResolverResult {
|
||||
return ExceptionResolverResult(
|
||||
apiError = DefaultApiError.createFailure(exception?.message),
|
||||
status = HttpStatus.INTERNAL_SERVER_ERROR,
|
||||
exception = exception
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
@file:Suppress("unused")
|
||||
package ru.touchin.exception.handler.spring
|
||||
|
||||
import org.springframework.context.annotation.Import
|
||||
import ru.touchin.exception.handler.spring.configurations.ExceptionHandlerConfiguration
|
||||
|
||||
@Import(value = [ExceptionHandlerConfiguration::class])
|
||||
annotation class EnableSpringExceptionHandler
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
package ru.touchin.exception.handler.spring.advices
|
||||
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice
|
||||
import ru.touchin.exception.handler.dto.ExceptionResolverResult
|
||||
import ru.touchin.exception.handler.spring.creators.ExceptionResponseBodyCreator
|
||||
import ru.touchin.exception.handler.spring.logger.Logger
|
||||
import ru.touchin.exception.handler.spring.resolvers.ExceptionResolver
|
||||
|
||||
@RestControllerAdvice
|
||||
class ExceptionHandlerAdvice(
|
||||
exceptionResolversList: List<ExceptionResolver>,
|
||||
private val logger: Logger,
|
||||
private val exceptionResponseBodyCreator: ExceptionResponseBodyCreator,
|
||||
) {
|
||||
|
||||
private val exceptionResolvers = exceptionResolversList.asSequence()
|
||||
|
||||
@ExceptionHandler(Exception::class)
|
||||
fun handleException(
|
||||
exception: Exception,
|
||||
): ResponseEntity<Any> {
|
||||
val result: ExceptionResolverResult = exceptionResolvers
|
||||
.mapNotNull { it.invoke(exception) }
|
||||
.firstOrNull()
|
||||
?: ExceptionResolverResult.createInternalError("Unexpected exception occurred: $exception")
|
||||
|
||||
logger.log(this::class.java, result)
|
||||
|
||||
val body = exceptionResponseBodyCreator(result.apiError)
|
||||
|
||||
return ResponseEntity(body, result.status)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package ru.touchin.exception.handler.spring.configurations
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.ComponentScan
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import ru.touchin.exception.handler.spring.creators.DefaultExceptionResponseBodyCreatorImpl
|
||||
import ru.touchin.exception.handler.spring.creators.ExceptionResponseBodyCreator
|
||||
import ru.touchin.exception.handler.spring.logger.FallbackLogger
|
||||
import ru.touchin.exception.handler.spring.logger.Logger
|
||||
|
||||
@Configuration
|
||||
@ComponentScan(
|
||||
"ru.touchin.exception.handler.spring.advices",
|
||||
"ru.touchin.exception.handler.spring.resolvers",
|
||||
)
|
||||
class ExceptionHandlerConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
fun exceptionResponseBodyCreator(): ExceptionResponseBodyCreator {
|
||||
return DefaultExceptionResponseBodyCreatorImpl()
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
fun logger(): Logger {
|
||||
return FallbackLogger()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
@file:Suppress("unused")
|
||||
package ru.touchin.exception.handler.spring.creators
|
||||
|
||||
import ru.touchin.common.spring.web.dto.ApiError
|
||||
import ru.touchin.common.spring.web.dto.DefaultApiError
|
||||
|
||||
class DefaultExceptionResponseBodyCreatorImpl : ExceptionResponseBodyCreator {
|
||||
|
||||
override fun invoke(apiError: ApiError): Any {
|
||||
return DefaultApiError(
|
||||
errorCode = apiError.errorCode,
|
||||
errorMessage = apiError.errorMessage
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package ru.touchin.exception.handler.spring.creators
|
||||
|
||||
import ru.touchin.common.spring.web.dto.ApiError
|
||||
|
||||
interface ExceptionResponseBodyCreator {
|
||||
|
||||
operator fun invoke(apiError: ApiError): Any?
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package ru.touchin.exception.handler.spring.logger
|
||||
|
||||
import ru.touchin.exception.handler.dto.ExceptionResolverResult
|
||||
|
||||
class FallbackLogger : Logger {
|
||||
|
||||
override fun log(clazz: Class<*>, exceptionResolverResult: ExceptionResolverResult) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package ru.touchin.exception.handler.spring.logger
|
||||
|
||||
import ru.touchin.exception.handler.dto.ExceptionResolverResult
|
||||
|
||||
interface Logger {
|
||||
|
||||
fun log(clazz: Class<*>, exceptionResolverResult: ExceptionResolverResult)
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package ru.touchin.exception.handler.spring.resolvers
|
||||
|
||||
import ru.touchin.exception.handler.dto.ExceptionResolverResult
|
||||
|
||||
interface ExceptionResolver {
|
||||
|
||||
operator fun invoke(exception: Exception): ExceptionResolverResult?
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package ru.touchin.exception.handler.spring.resolvers
|
||||
|
||||
import org.springframework.core.Ordered
|
||||
import org.springframework.core.annotation.Order
|
||||
import org.springframework.stereotype.Component
|
||||
import ru.touchin.exception.handler.dto.ExceptionResolverResult
|
||||
|
||||
@Order(Ordered.LOWEST_PRECEDENCE)
|
||||
@Component
|
||||
class FallbackExceptionResolver : ExceptionResolver {
|
||||
|
||||
override fun invoke(exception: Exception): ExceptionResolverResult {
|
||||
return ExceptionResolverResult.createInternalError(exception)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package ru.touchin.exception.handler
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||
import ru.touchin.exception.handler.spring.EnableSpringExceptionHandler
|
||||
|
||||
@SpringBootApplication
|
||||
@EnableSpringExceptionHandler
|
||||
class TestApplication
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
package ru.touchin.exception.handler.spring.advices
|
||||
|
||||
import com.nhaarman.mockitokotlin2.any
|
||||
import com.nhaarman.mockitokotlin2.atLeastOnce
|
||||
import com.nhaarman.mockitokotlin2.mock
|
||||
import com.nhaarman.mockitokotlin2.never
|
||||
import com.nhaarman.mockitokotlin2.verify
|
||||
import org.hamcrest.Matchers.`is`
|
||||
import org.hamcrest.Matchers.nullValue
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.Mockito.spy
|
||||
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.get
|
||||
import org.springframework.test.web.servlet.result.MockMvcResultHandlers.print
|
||||
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content
|
||||
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
|
||||
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import ru.touchin.exception.handler.spring.EnableSpringExceptionHandler
|
||||
import ru.touchin.exception.handler.spring.creators.ExceptionResponseBodyCreator
|
||||
import ru.touchin.exception.handler.spring.logger.Logger
|
||||
import ru.touchin.exception.handler.spring.resolvers.FallbackExceptionResolver
|
||||
import ru.touchin.exception.handler.spring.resolvers.IllegalStateExceptionResolver1
|
||||
import ru.touchin.exception.handler.spring.resolvers.IllegalStateExceptionResolver2
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/errors")
|
||||
class ErrorController {
|
||||
|
||||
|
||||
@GetMapping("/runtime")
|
||||
fun runtimeError() {
|
||||
throw RuntimeException("my runtime error")
|
||||
}
|
||||
|
||||
@GetMapping("/illegal")
|
||||
fun illegalError() {
|
||||
throw IllegalStateException("my illegal error")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ActiveProfiles("test")
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
internal class ExceptionHandlerAdviceMvcTest {
|
||||
|
||||
@Autowired
|
||||
lateinit var mockMvc: MockMvc
|
||||
|
||||
@Autowired
|
||||
lateinit var logger: Logger
|
||||
|
||||
@Test
|
||||
@DisplayName("Тест должен проходить")
|
||||
fun shouldBeWork() {
|
||||
assertTrue(true, "Not passed")
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Должна вернуться ошибка InternalServerError с кодом -1")
|
||||
fun shouldGetInternalServerError() {
|
||||
mockMvc
|
||||
.perform(get("/api/errors/runtime"))
|
||||
.andDo(print())
|
||||
.andExpect(status().isInternalServerError)
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE))
|
||||
.andExpect(jsonPath("$.errorCode", `is`(-1)))
|
||||
.andExpect(jsonPath("$.errorMessage", `is`("my runtime error")))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Должна вернуться ошибка BadRequest с кодом -2")
|
||||
fun shouldGetBadRequest() {
|
||||
mockMvc
|
||||
.perform(get("/api/errors/illegal"))
|
||||
.andDo(print())
|
||||
.andExpect(status().isBadRequest)
|
||||
.andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE))
|
||||
.andExpect(jsonPath("$.errorCode", `is`(-2)))
|
||||
.andExpect(jsonPath("$.errorMessage", `is`("my illegal error")))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Должен отработать только первый ExceptionResolver")
|
||||
fun shouldBeCorrectOrder() {
|
||||
val resolver1 = spy(IllegalStateExceptionResolver1::class.java)
|
||||
val resolver2 = spy(IllegalStateExceptionResolver2::class.java)
|
||||
val resolver3 = spy(FallbackExceptionResolver::class.java)
|
||||
|
||||
val resolvers = listOf(resolver1, resolver2, resolver3)
|
||||
val exceptionResponseBodyCreator: ExceptionResponseBodyCreator = mock { }
|
||||
|
||||
val exceptionHandlerAdvice = ExceptionHandlerAdvice(
|
||||
exceptionResolversList = resolvers,
|
||||
exceptionResponseBodyCreator = exceptionResponseBodyCreator,
|
||||
logger = logger
|
||||
)
|
||||
|
||||
exceptionHandlerAdvice.handleException(IllegalStateException("error"))
|
||||
|
||||
verify(resolver1, atLeastOnce()).invoke(any())
|
||||
verify(resolver2, never()).invoke(any())
|
||||
verify(resolver3, never()).invoke(any())
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
package ru.touchin.exception.handler.spring.advices
|
||||
|
||||
import com.nhaarman.mockitokotlin2.any
|
||||
import com.nhaarman.mockitokotlin2.mock
|
||||
import com.nhaarman.mockitokotlin2.never
|
||||
import com.nhaarman.mockitokotlin2.only
|
||||
import com.nhaarman.mockitokotlin2.verify
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.mockito.Mockito.spy
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.test.context.ActiveProfiles
|
||||
import ru.touchin.exception.handler.spring.creators.ExceptionResponseBodyCreator
|
||||
import ru.touchin.exception.handler.spring.logger.Logger
|
||||
import ru.touchin.exception.handler.spring.resolvers.FallbackExceptionResolver
|
||||
import ru.touchin.exception.handler.spring.resolvers.IllegalStateExceptionResolver1
|
||||
import ru.touchin.exception.handler.spring.resolvers.IllegalStateExceptionResolver2
|
||||
|
||||
@ActiveProfiles("test")
|
||||
@SpringBootTest
|
||||
internal class ExceptionHandlerAdviceTest {
|
||||
|
||||
private lateinit var resolver1: IllegalStateExceptionResolver1
|
||||
private lateinit var resolver2: IllegalStateExceptionResolver2
|
||||
private lateinit var resolver3: FallbackExceptionResolver
|
||||
|
||||
private lateinit var exceptionHandlerAdvice: ExceptionHandlerAdvice
|
||||
|
||||
@Autowired
|
||||
lateinit var logger: Logger
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
resolver1 = spy(IllegalStateExceptionResolver1::class.java)
|
||||
resolver2 = spy(IllegalStateExceptionResolver2::class.java)
|
||||
resolver3 = spy(FallbackExceptionResolver::class.java)
|
||||
|
||||
val resolvers = listOf(resolver1, resolver2, resolver3)
|
||||
val exceptionResponseBodyCreator: ExceptionResponseBodyCreator = mock { }
|
||||
|
||||
exceptionHandlerAdvice = ExceptionHandlerAdvice(
|
||||
exceptionResolversList = resolvers,
|
||||
exceptionResponseBodyCreator = exceptionResponseBodyCreator,
|
||||
logger = logger,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Должен отработать только первый ExceptionResolver")
|
||||
fun shouldBeExecuteOnlyResolver1() {
|
||||
exceptionHandlerAdvice.handleException(IllegalStateException("error"))
|
||||
|
||||
verify(resolver1, only()).invoke(any())
|
||||
verify(resolver2, never()).invoke(any())
|
||||
verify(resolver3, never()).invoke(any())
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Должны отработать все resolvers и поймать ошибку в FallbackResolver")
|
||||
fun shouldBeExecuteAllResolversAndResolvedInFallback() {
|
||||
exceptionHandlerAdvice.handleException(RuntimeException("error"))
|
||||
|
||||
verify(resolver1, only()).invoke(any())
|
||||
verify(resolver2, only()).invoke(any())
|
||||
verify(resolver3, only()).invoke(any())
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Должны отработать все resolvers и НЕ поймать ошибку в FallbackResolver")
|
||||
fun shouldVBeExecuteAllResolversWithoutResolve() {
|
||||
val resolvers = listOf(resolver1, resolver2)
|
||||
val exceptionResponseBodyCreator: ExceptionResponseBodyCreator = mock { }
|
||||
|
||||
exceptionHandlerAdvice = ExceptionHandlerAdvice(
|
||||
exceptionResolversList = resolvers,
|
||||
exceptionResponseBodyCreator = exceptionResponseBodyCreator,
|
||||
logger = logger,
|
||||
)
|
||||
|
||||
exceptionHandlerAdvice.handleException(RuntimeException("error"))
|
||||
|
||||
verify(resolver1, only()).invoke(any())
|
||||
verify(resolver2, only()).invoke(any())
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package ru.touchin.exception.handler.spring.resolvers
|
||||
|
||||
import org.springframework.core.annotation.Order
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.stereotype.Component
|
||||
import ru.touchin.common.spring.web.dto.DefaultApiError
|
||||
import ru.touchin.exception.handler.dto.ExceptionResolverResult
|
||||
|
||||
@Component
|
||||
@Order(1)
|
||||
class IllegalStateExceptionResolver1: ExceptionResolver {
|
||||
|
||||
override fun invoke(exception: Exception): ExceptionResolverResult? {
|
||||
return (exception as? IllegalStateException)?.let {
|
||||
ExceptionResolverResult(
|
||||
apiError = DefaultApiError(errorCode = -2, errorMessage = exception.message),
|
||||
status = HttpStatus.BAD_REQUEST,
|
||||
exception = exception,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package ru.touchin.exception.handler.spring.resolvers
|
||||
|
||||
import org.springframework.core.annotation.Order
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.stereotype.Component
|
||||
import ru.touchin.common.spring.web.dto.DefaultApiError
|
||||
import ru.touchin.exception.handler.dto.ExceptionResolverResult
|
||||
import java.lang.IllegalStateException
|
||||
|
||||
@Component
|
||||
@Order(2)
|
||||
class IllegalStateExceptionResolver2: ExceptionResolver {
|
||||
|
||||
override fun invoke(exception: Exception): ExceptionResolverResult? {
|
||||
return (exception as? IllegalStateException)?.let {
|
||||
ExceptionResolverResult(
|
||||
apiError = DefaultApiError(errorCode = -2, errorMessage = "Should not be executed"),
|
||||
status = HttpStatus.BAD_GATEWAY,
|
||||
exception = exception,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -28,3 +28,4 @@ include("common-spring-test-jpa")
|
|||
include("logger")
|
||||
include("logger-spring")
|
||||
include("logger-spring-web")
|
||||
include("exception-handler-spring-web")
|
||||
|
|
|
|||
Loading…
Reference in New Issue