如何全局处理 Spring WebFlux 中 WebFilter 抛出的错误?
How to globally handle errors thrown from WebFilter in Spring WebFlux?
如何在 WebFlux 中全局拦截和处理从 WebFilter
链抛出的错误?
很清楚如何处理控制器抛出的错误:@ControllerAdvice
和 @ExceptionHandler
帮助很大。
当 WebFilter
个组件抛出异常时,此方法不起作用。
在以下配置中 GET /first
和 GET /second
响应有意引发异常。尽管 @ExceptionHandler
方法 handleFirst
、handleSecond
相似,但从未调用 handleSecond
。我想那是因为 MyWebFilter
不会让 ServerWebExchange
进入可以应用 GlobalErrorHandlers
方法的阶段。
对的回应 GET /first
:
HTTP 500 "hello first" // expected
HTTP 500 "hello first" // actual
对的回应 GET /second
:
HTTP 404 "hello second" // expected
HTTP 500 {"path": "/second", "status": 500, "error": "Internal Server Error" } // actual
@RestController
class MyController {
@GetMapping("/first")
String first(){
throw new FirstException("hello first");
}
}
@Component
class MyWebFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange swe, WebFilterChain wfc) {
var path = swe.getRequest().getURI().getPath();
if (path.contains("second")){
throw new SecondException("hello second")
}
}
}
@ControllerAdvice
class GlobalErrorHandlers {
@ExceptionHandler(FirstException::class)
ResponseEntity<String> handleFirst(FirstException ex) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ex.message)
}
@ExceptionHandler(SecondException::class)
ResponseEntity<String> handleSecond(SecondException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.message)
}
}
需要三个步骤才能完全控制从应用程序端点处理代码抛出的所有异常:
- 实施
org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler
- 用
@ControllerAdvice
注释(或只是 @Component
)
- 将
@Priority
设置为小于 1
以让自定义处理程序 运行 在默认处理程序 (WebFluxResponseStatusExceptionHandler
) 之前
The tricky part is where we get an instance implementing
ServerResponse.Context
for passing to
ServerResponse.writeTo(exchange, context)
. I did not find the final
answer, and comments are welcome. In the internal Spring code they always create a new instance of context for each writeTo
invocation,
although in all cases (I've manged to find) the context instance is immutable.
That is why I ended up using the same ResponseContextInstance
for all responses.
At the moment no problems detected with this approach.
@ControllerAdvice
@Priority(0) /* should go before WebFluxResponseStatusExceptionHandler */
class CustomWebExceptionHandler : ErrorWebExceptionHandler {
private val log = logger(CustomWebExceptionHandler::class)
override fun handle(exchange: ServerWebExchange, ex: Throwable): Mono<Void> {
log.error("handled ${ex.javaClass.simpleName}", ex)
val sr = when (ex) {
is FirstException -> handleFirst(ex)
is SecondException -> handleSecond(ex)
else -> defaultException(ex)
}
return sr.flatMap { it.writeTo(exchange, ResponseContextInstance) }.then()
}
private fun handleFirst(ex: FirstException): Mono<ServerResponse> {
return ServerResponse
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.bodyValue("first")
}
private fun handleSecond(ex: SecondException): Mono<ServerResponse> {
return ServerResponse.status(HttpStatus.BAD_REQUEST).bodyValue("second")
}
private object ResponseContextInstance : ServerResponse.Context {
val strategies: HandlerStrategies = HandlerStrategies.withDefaults()
override fun messageWriters(): List<HttpMessageWriter<*>> {
return strategies.messageWriters()
}
override fun viewResolvers(): List<ViewResolver> {
return strategies.viewResolvers()
}
}
}
如何在 WebFlux 中全局拦截和处理从 WebFilter
链抛出的错误?
很清楚如何处理控制器抛出的错误:@ControllerAdvice
和 @ExceptionHandler
帮助很大。
当 WebFilter
个组件抛出异常时,此方法不起作用。
在以下配置中 GET /first
和 GET /second
响应有意引发异常。尽管 @ExceptionHandler
方法 handleFirst
、handleSecond
相似,但从未调用 handleSecond
。我想那是因为 MyWebFilter
不会让 ServerWebExchange
进入可以应用 GlobalErrorHandlers
方法的阶段。
对的回应 GET /first
:
HTTP 500 "hello first" // expected
HTTP 500 "hello first" // actual
对的回应 GET /second
:
HTTP 404 "hello second" // expected
HTTP 500 {"path": "/second", "status": 500, "error": "Internal Server Error" } // actual
@RestController
class MyController {
@GetMapping("/first")
String first(){
throw new FirstException("hello first");
}
}
@Component
class MyWebFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange swe, WebFilterChain wfc) {
var path = swe.getRequest().getURI().getPath();
if (path.contains("second")){
throw new SecondException("hello second")
}
}
}
@ControllerAdvice
class GlobalErrorHandlers {
@ExceptionHandler(FirstException::class)
ResponseEntity<String> handleFirst(FirstException ex) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ex.message)
}
@ExceptionHandler(SecondException::class)
ResponseEntity<String> handleSecond(SecondException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.message)
}
}
需要三个步骤才能完全控制从应用程序端点处理代码抛出的所有异常:
- 实施
org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler
- 用
@ControllerAdvice
注释(或只是@Component
) - 将
@Priority
设置为小于1
以让自定义处理程序 运行 在默认处理程序 (WebFluxResponseStatusExceptionHandler
) 之前
The tricky part is where we get an instance implementing
ServerResponse.Context
for passing toServerResponse.writeTo(exchange, context)
. I did not find the final answer, and comments are welcome. In the internal Spring code they always create a new instance of context for eachwriteTo
invocation, although in all cases (I've manged to find) the context instance is immutable. That is why I ended up using the sameResponseContextInstance
for all responses. At the moment no problems detected with this approach.
@ControllerAdvice
@Priority(0) /* should go before WebFluxResponseStatusExceptionHandler */
class CustomWebExceptionHandler : ErrorWebExceptionHandler {
private val log = logger(CustomWebExceptionHandler::class)
override fun handle(exchange: ServerWebExchange, ex: Throwable): Mono<Void> {
log.error("handled ${ex.javaClass.simpleName}", ex)
val sr = when (ex) {
is FirstException -> handleFirst(ex)
is SecondException -> handleSecond(ex)
else -> defaultException(ex)
}
return sr.flatMap { it.writeTo(exchange, ResponseContextInstance) }.then()
}
private fun handleFirst(ex: FirstException): Mono<ServerResponse> {
return ServerResponse
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.bodyValue("first")
}
private fun handleSecond(ex: SecondException): Mono<ServerResponse> {
return ServerResponse.status(HttpStatus.BAD_REQUEST).bodyValue("second")
}
private object ResponseContextInstance : ServerResponse.Context {
val strategies: HandlerStrategies = HandlerStrategies.withDefaults()
override fun messageWriters(): List<HttpMessageWriter<*>> {
return strategies.messageWriters()
}
override fun viewResolvers(): List<ViewResolver> {
return strategies.viewResolvers()
}
}
}