Support Content Negotiation with @ExceptionHandler
Prior to this commit, `@ExceptionHandler` annotated controller methods
could be mapped using the exception type declaration as an annotation
attribute, or as a method parameter.
While such methods support a wide variety of method arguments and return
types, it was not possible to declare the same exception type on
different methods (in the same controller/controller advice).
This commit adds a new `produces` attribute on `@ExceptionHandler`; with
that, applications can vary the HTTP response depending on the exception
type and the requested content-type by the client:
```
@ExceptionHandler(produces = "application/json")
public ResponseEntity<ErrorMessage> handleJson(IllegalArgumentException exc) {
return ResponseEntity.badRequest().body(new ErrorMessage(exc.getMessage(), 42));
}
@ExceptionHandler(produces = "text/html")
public String handle(IllegalArgumentException exc, Model model) {
model.addAttribute("error", new ErrorMessage(exc.getMessage(), 42));
return "errorView";
}
```
This commit implements support in both Spring MVC and Spring WebFlux.
Closes gh-31936
This commit is contained in:
@@ -7,43 +7,8 @@
|
||||
`@ExceptionHandler` methods to handle exceptions from controller methods. The following
|
||||
example includes such a handler method:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
@Controller
|
||||
public class SimpleController {
|
||||
|
||||
// ...
|
||||
|
||||
@ExceptionHandler // <1>
|
||||
public ResponseEntity<String> handle(IOException ex) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Declaring an `@ExceptionHandler`.
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
@Controller
|
||||
class SimpleController {
|
||||
|
||||
// ...
|
||||
|
||||
@ExceptionHandler // <1>
|
||||
fun handle(ex: IOException): ResponseEntity<String> {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Declaring an `@ExceptionHandler`.
|
||||
======
|
||||
|
||||
include-code::./SimpleController[indent=0]
|
||||
|
||||
|
||||
The exception can match against a top-level exception being propagated (that is, a direct
|
||||
@@ -65,6 +30,22 @@ Support for `@ExceptionHandler` methods in Spring WebFlux is provided by the
|
||||
`HandlerAdapter` for `@RequestMapping` methods. See xref:web/webflux/dispatcher-handler.adoc[`DispatcherHandler`]
|
||||
for more detail.
|
||||
|
||||
[[webflux-ann-exceptionhandler-media]]
|
||||
== Media Type Mapping
|
||||
[.small]#xref:web/webmvc/mvc-controller/ann-exceptionhandler.adoc#mvc-ann-exceptionhandler-media[See equivalent in the Servlet stack]#
|
||||
|
||||
In addition to exception types, `@ExceptionHandler` methods can also declare producible media types.
|
||||
This allows to refine error responses depending on the media types requested by HTTP clients, typically in the "Accept" HTTP request header.
|
||||
|
||||
Applications can declare producible media types directly on annotations, for the same exception type:
|
||||
|
||||
|
||||
include-code::./MediaTypeController[tag=mediatype,indent=0]
|
||||
|
||||
Here, methods handle the same exception type but will not be rejected as duplicates.
|
||||
Instead, API clients requesting "application/json" will receive a JSON error, and browsers will get an HTML error view.
|
||||
Each `@ExceptionHandler` annotation can declare several producible media types,
|
||||
the content negotiation during the error handling phase will decide which content type will be used.
|
||||
|
||||
|
||||
[[webflux-ann-exceptionhandler-args]]
|
||||
|
||||
@@ -6,40 +6,12 @@
|
||||
`@Controller` and xref:web/webmvc/mvc-controller/ann-advice.adoc[@ControllerAdvice] classes can have
|
||||
`@ExceptionHandler` methods to handle exceptions from controller methods, as the following example shows:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
@Controller
|
||||
public class SimpleController {
|
||||
|
||||
// ...
|
||||
include-code::./SimpleController[indent=0]
|
||||
|
||||
@ExceptionHandler
|
||||
public ResponseEntity<String> handle(IOException ex) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
@Controller
|
||||
class SimpleController {
|
||||
|
||||
// ...
|
||||
|
||||
@ExceptionHandler
|
||||
fun handle(ex: IOException): ResponseEntity<String> {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
[[mvc-ann-exceptionhandler-exc]]
|
||||
== Exception Mapping
|
||||
|
||||
The exception may match against a top-level exception being propagated (e.g. a direct
|
||||
`IOException` being thrown) or against a nested cause within a wrapper exception (e.g.
|
||||
@@ -54,54 +26,13 @@ is used to sort exceptions based on their depth from the thrown exception type.
|
||||
Alternatively, the annotation declaration may narrow the exception types to match,
|
||||
as the following example shows:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
@ExceptionHandler({FileSystemException.class, RemoteException.class})
|
||||
public ResponseEntity<String> handle(IOException ex) {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
@ExceptionHandler(FileSystemException::class, RemoteException::class)
|
||||
fun handle(ex: IOException): ResponseEntity<String> {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
======
|
||||
include-code::./ExceptionController[tag=narrow,indent=0]
|
||||
|
||||
You can even use a list of specific exception types with a very generic argument signature,
|
||||
as the following example shows:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||
----
|
||||
@ExceptionHandler({FileSystemException.class, RemoteException.class})
|
||||
public ResponseEntity<String> handle(Exception ex) {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
include-code::./ExceptionController[tag=general,indent=0]
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||
----
|
||||
@ExceptionHandler(FileSystemException::class, RemoteException::class)
|
||||
fun handle(ex: Exception): ResponseEntity<String> {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
@@ -143,6 +74,25 @@ Support for `@ExceptionHandler` methods in Spring MVC is built on the `Dispatche
|
||||
level, xref:web/webmvc/mvc-servlet/exceptionhandlers.adoc[HandlerExceptionResolver] mechanism.
|
||||
|
||||
|
||||
|
||||
[[mvc-ann-exceptionhandler-media]]
|
||||
== Media Type Mapping
|
||||
[.small]#xref:web/webflux/controller/ann-exceptions.adoc#webflux-ann-exceptionhandler-media[See equivalent in the Reactive stack]#
|
||||
|
||||
In addition to exception types, `@ExceptionHandler` methods can also declare producible media types.
|
||||
This allows to refine error responses depending on the media types requested by HTTP clients, typically in the "Accept" HTTP request header.
|
||||
|
||||
Applications can declare producible media types directly on annotations, for the same exception type:
|
||||
|
||||
|
||||
include-code::./MediaTypeController[tag=mediatype,indent=0]
|
||||
|
||||
Here, methods handle the same exception type but will not be rejected as duplicates.
|
||||
Instead, API clients requesting "application/json" will receive a JSON error, and browsers will get an HTML error view.
|
||||
Each `@ExceptionHandler` annotation can declare several producible media types,
|
||||
the content negotiation during the error handling phase will decide which content type will be used.
|
||||
|
||||
|
||||
[[mvc-ann-exceptionhandler-args]]
|
||||
== Method Arguments
|
||||
[.small]#xref:web/webflux/controller/ann-exceptions.adoc#webflux-ann-exceptionhandler-args[See equivalent in the Reactive stack]#
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.docs.web.webflux.controller.webfluxanncontrollerexceptions;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
|
||||
@Controller
|
||||
public class SimpleController {
|
||||
|
||||
@ExceptionHandler(IOException.class)
|
||||
public ResponseEntity<String> handle() {
|
||||
return ResponseEntity.internalServerError().body("Could not read file storage");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.docs.web.webflux.controller.webfluxannexceptionhandlermedia;
|
||||
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
|
||||
@Controller
|
||||
public class MediaTypeController {
|
||||
|
||||
// tag::mediatype[]
|
||||
@ExceptionHandler(produces = "application/json")
|
||||
public ResponseEntity<ErrorMessage> handleJson(IllegalArgumentException exc) {
|
||||
return ResponseEntity.badRequest().body(new ErrorMessage(exc.getMessage(), 42));
|
||||
}
|
||||
|
||||
@ExceptionHandler(produces = "text/html")
|
||||
public String handle(IllegalArgumentException exc, Model model) {
|
||||
model.addAttribute("error", new ErrorMessage(exc.getMessage(), 42));
|
||||
return "errorView";
|
||||
}
|
||||
// end::mediatype[]
|
||||
|
||||
static record ErrorMessage(String message, int code) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.docs.web.webmvc.mvccontroller.mvcannexceptionhandler;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
|
||||
@Controller
|
||||
public class SimpleController {
|
||||
|
||||
@ExceptionHandler(IOException.class)
|
||||
public ResponseEntity<String> handle() {
|
||||
return ResponseEntity.internalServerError().body("Could not read file storage");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.docs.web.webmvc.mvccontroller.mvcannexceptionhandlerexc;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileSystemException;
|
||||
import java.rmi.RemoteException;
|
||||
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
|
||||
@Controller
|
||||
public class ExceptionController {
|
||||
|
||||
// tag::narrow[]
|
||||
@ExceptionHandler({FileSystemException.class, RemoteException.class})
|
||||
public ResponseEntity<String> handleIoException(IOException ex) {
|
||||
return ResponseEntity.internalServerError().body(ex.getMessage());
|
||||
}
|
||||
// end::narrow[]
|
||||
|
||||
|
||||
// tag::general[]
|
||||
@ExceptionHandler({FileSystemException.class, RemoteException.class})
|
||||
public ResponseEntity<String> handleExceptions(Exception ex) {
|
||||
return ResponseEntity.internalServerError().body(ex.getMessage());
|
||||
}
|
||||
// end::general[]
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.docs.web.webmvc.mvccontroller.mvcannexceptionhandlermedia;
|
||||
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
|
||||
@Controller
|
||||
public class MediaTypeController {
|
||||
|
||||
// tag::mediatype[]
|
||||
@ExceptionHandler(produces = "application/json")
|
||||
public ResponseEntity<ErrorMessage> handleJson(IllegalArgumentException exc) {
|
||||
return ResponseEntity.badRequest().body(new ErrorMessage(exc.getMessage(), 42));
|
||||
}
|
||||
|
||||
@ExceptionHandler(produces = "text/html")
|
||||
public String handle(IllegalArgumentException exc, Model model) {
|
||||
model.addAttribute("error", new ErrorMessage(exc.getMessage(), 42));
|
||||
return "errorView";
|
||||
}
|
||||
// end::mediatype[]
|
||||
|
||||
static record ErrorMessage(String message, int code) {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.docs.web.webflux.controller.webfluxanncontrollerexceptions;
|
||||
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.stereotype.Controller
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler
|
||||
import java.io.IOException
|
||||
|
||||
@Controller
|
||||
class SimpleController {
|
||||
|
||||
@ExceptionHandler(IOException::class)
|
||||
fun handle() : ResponseEntity<String> {
|
||||
return ResponseEntity.internalServerError().body("Could not read file storage")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.docs.web.webflux.controller.webfluxannexceptionhandlermedia
|
||||
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.stereotype.Controller
|
||||
import org.springframework.ui.Model
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler
|
||||
|
||||
@Controller
|
||||
class MediaTypeController {
|
||||
|
||||
// tag::mediatype[]
|
||||
@ExceptionHandler(produces = ["application/json"])
|
||||
fun handleJson(exc: IllegalArgumentException): ResponseEntity<ErrorMessage> {
|
||||
return ResponseEntity.badRequest().body(ErrorMessage(exc.message, 42))
|
||||
}
|
||||
|
||||
@ExceptionHandler(produces = ["text/html"])
|
||||
fun handle(exc: IllegalArgumentException, model: Model): String {
|
||||
model.addAttribute("error", ErrorMessage(exc.message, 42))
|
||||
return "errorView"
|
||||
}
|
||||
// end::mediatype[]
|
||||
|
||||
@JvmRecord
|
||||
data class ErrorMessage(val message: String?, val code: Int)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.docs.web.webmvc.mvccontroller.mvcannexceptionhandler;
|
||||
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.stereotype.Controller
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler
|
||||
import java.io.IOException
|
||||
|
||||
@Controller
|
||||
class SimpleController {
|
||||
|
||||
@ExceptionHandler(IOException::class)
|
||||
fun handle() : ResponseEntity<String> {
|
||||
return ResponseEntity.internalServerError().body("Could not read file storage")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.docs.web.webmvc.mvccontroller.mvcannexceptionhandlerexc
|
||||
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.stereotype.Controller
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler
|
||||
import java.io.IOException
|
||||
import java.rmi.RemoteException
|
||||
|
||||
@Controller
|
||||
class ExceptionController {
|
||||
|
||||
// tag::narrow[]
|
||||
@ExceptionHandler(FileSystemException::class, RemoteException::class)
|
||||
fun handleIoException(ex: IOException): ResponseEntity<String> {
|
||||
return ResponseEntity.internalServerError().body(ex.message)
|
||||
}
|
||||
// end::narrow[]
|
||||
|
||||
|
||||
// tag::general[]
|
||||
@ExceptionHandler(FileSystemException::class, RemoteException::class)
|
||||
fun handleExceptions(ex: Exception): ResponseEntity<String> {
|
||||
return ResponseEntity.internalServerError().body(ex.message)
|
||||
}
|
||||
// end::general[]
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.docs.web.webmvc.mvccontroller.mvcannexceptionhandlermedia
|
||||
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.stereotype.Controller
|
||||
import org.springframework.ui.Model
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler
|
||||
|
||||
@Controller
|
||||
class MediaTypeController {
|
||||
|
||||
// tag::mediatype[]
|
||||
@ExceptionHandler(produces = ["application/json"])
|
||||
fun handleJson(exc: IllegalArgumentException): ResponseEntity<ErrorMessage> {
|
||||
return ResponseEntity.badRequest().body(ErrorMessage(exc.message, 42))
|
||||
}
|
||||
|
||||
@ExceptionHandler(produces = ["text/html"])
|
||||
fun handle(exc: IllegalArgumentException, model: Model): String {
|
||||
model.addAttribute("error", ErrorMessage(exc.message, 42))
|
||||
return "errorView"
|
||||
}
|
||||
// end::mediatype[]
|
||||
|
||||
@JvmRecord
|
||||
data class ErrorMessage(val message: String?, val code: Int)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -23,6 +23,7 @@ import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.aot.hint.annotation.Reflective;
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
|
||||
/**
|
||||
* Annotation for handling exceptions in specific handler classes and/or
|
||||
@@ -101,6 +102,7 @@ import org.springframework.aot.hint.annotation.Reflective;
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @author Juergen Hoeller
|
||||
* @author Brian Clozel
|
||||
* @since 3.0
|
||||
* @see ControllerAdvice
|
||||
* @see org.springframework.web.context.request.WebRequest
|
||||
@@ -112,9 +114,24 @@ import org.springframework.aot.hint.annotation.Reflective;
|
||||
public @interface ExceptionHandler {
|
||||
|
||||
/**
|
||||
* Exceptions handled by the annotated method. If empty, will default to any
|
||||
* exceptions listed in the method argument list.
|
||||
* Exceptions handled by the annotated method.
|
||||
* <p>This is an alias for {@link #exception}.
|
||||
*/
|
||||
@AliasFor("exception")
|
||||
Class<? extends Throwable>[] value() default {};
|
||||
|
||||
/**
|
||||
* Exceptions handled by the annotated method. If empty, will default to any
|
||||
* exceptions listed in the method argument list.
|
||||
* @since 6.2.0
|
||||
*/
|
||||
@AliasFor("value")
|
||||
Class<? extends Throwable>[] exception() default {};
|
||||
|
||||
/**
|
||||
* Media Types that can be produced by the annotated method.
|
||||
* @since 6.2.0
|
||||
*/
|
||||
String[] produces() default {};
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.method.annotation;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@code @ExceptionHandler} mapping information. It contains:
|
||||
* <ul>
|
||||
* <li>the supported exception types
|
||||
* <li>the producible media types, if any
|
||||
* <li>the method in charge of handling the exception
|
||||
* </ul>
|
||||
* @author Brian Clozel
|
||||
* @since 6.2.0
|
||||
*/
|
||||
public class ExceptionHandlerMappingInfo {
|
||||
|
||||
private final Set<Class<? extends Throwable>> exceptionTypes;
|
||||
|
||||
private final Set<MediaType> producibleTypes;
|
||||
|
||||
private final Method handlerMethod;
|
||||
|
||||
ExceptionHandlerMappingInfo(Set<Class<? extends Throwable>> exceptionTypes, Set<MediaType> producibleMediaTypes, Method handlerMethod) {
|
||||
Assert.notNull(exceptionTypes, "exceptionTypes should not be null");
|
||||
Assert.notNull(producibleMediaTypes, "producibleMediaTypes should not be null");
|
||||
Assert.notNull(handlerMethod, "handlerMethod should not be null");
|
||||
this.exceptionTypes = exceptionTypes;
|
||||
this.producibleTypes = producibleMediaTypes;
|
||||
this.handlerMethod = handlerMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the method responsible for handling the exception.
|
||||
*/
|
||||
public Method getHandlerMethod() {
|
||||
return this.handlerMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the exception types supported by this handler.
|
||||
*/
|
||||
public Set<Class<? extends Throwable>> getExceptionTypes() {
|
||||
return this.exceptionTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the producible media types by this handler. Can be empty.
|
||||
*/
|
||||
public Set<MediaType> getProducibleTypes() {
|
||||
return this.producibleTypes;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -19,27 +19,42 @@ package org.springframework.web.method.annotation;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.core.ExceptionDepthComparator;
|
||||
import org.springframework.core.MethodIntrospector;
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.http.InvalidMediaTypeException;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ConcurrentReferenceHashMap;
|
||||
import org.springframework.util.ConcurrentLruCache;
|
||||
import org.springframework.util.MimeType;
|
||||
import org.springframework.util.ReflectionUtils.MethodFilter;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
|
||||
/**
|
||||
* Discovers {@linkplain ExceptionHandler @ExceptionHandler} methods in a given class,
|
||||
* including all of its superclasses, and helps to resolve a given {@link Exception}
|
||||
* to the exception types supported by a given {@link Method}.
|
||||
* and {@link MediaType} requested by clients to combinations supported by a given {@link Method}.
|
||||
* <p>This resolver will use the exception information declared as {@code @ExceptionHandler}
|
||||
* annotation attributes, or as a method argument as a fallback. This will throw
|
||||
* {@code IllegalStateException} instances if:
|
||||
* <ul>
|
||||
* <li>No Exception information could be found for a method
|
||||
* <li>An invalid {@link MediaType} has been declared as {@code @ExceptionHandler} attribute
|
||||
* <li>Multiple handlers declare the same exception + media type mapping
|
||||
* </ul>
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Juergen Hoeller
|
||||
* @author Sam Brannen
|
||||
* @author Brian Clozel
|
||||
* @since 3.1
|
||||
*/
|
||||
public class ExceptionHandlerMethodResolver {
|
||||
@@ -47,35 +62,42 @@ public class ExceptionHandlerMethodResolver {
|
||||
/**
|
||||
* A filter for selecting {@code @ExceptionHandler} methods.
|
||||
*/
|
||||
public static final MethodFilter EXCEPTION_HANDLER_METHODS = method ->
|
||||
private static final MethodFilter EXCEPTION_HANDLER_METHODS = method ->
|
||||
AnnotatedElementUtils.hasAnnotation(method, ExceptionHandler.class);
|
||||
|
||||
private static final Method NO_MATCHING_EXCEPTION_HANDLER_METHOD;
|
||||
private static final ExceptionHandlerMappingInfo NO_MATCHING_EXCEPTION_HANDLER;
|
||||
|
||||
static {
|
||||
try {
|
||||
NO_MATCHING_EXCEPTION_HANDLER_METHOD =
|
||||
ExceptionHandlerMethodResolver.class.getDeclaredMethod("noMatchingExceptionHandler");
|
||||
NO_MATCHING_EXCEPTION_HANDLER = new ExceptionHandlerMappingInfo(Set.of(), Set.of(),
|
||||
ExceptionHandlerMethodResolver.class.getDeclaredMethod("noMatchingExceptionHandler"));
|
||||
}
|
||||
catch (NoSuchMethodException ex) {
|
||||
throw new IllegalStateException("Expected method not found: " + ex);
|
||||
}
|
||||
}
|
||||
|
||||
private final Map<ExceptionMapping, ExceptionHandlerMappingInfo> mappedMethods = new HashMap<>(16);
|
||||
|
||||
private final Map<Class<? extends Throwable>, Method> mappedMethods = new HashMap<>(16);
|
||||
|
||||
private final Map<Class<? extends Throwable>, Method> exceptionLookupCache = new ConcurrentReferenceHashMap<>(16);
|
||||
private final ConcurrentLruCache<ExceptionMapping, ExceptionHandlerMappingInfo> lookupCache = new ConcurrentLruCache<>(24,
|
||||
cacheKey -> getMappedMethod(cacheKey.exceptionType(), cacheKey.mediaType()));
|
||||
|
||||
|
||||
/**
|
||||
* A constructor that finds {@link ExceptionHandler} methods in the given type.
|
||||
* @param handlerType the type to introspect
|
||||
* @throws IllegalStateException in case of invalid or ambiguous exception mapping declarations
|
||||
*/
|
||||
public ExceptionHandlerMethodResolver(Class<?> handlerType) {
|
||||
for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)) {
|
||||
for (Class<? extends Throwable> exceptionType : detectExceptionMappings(method)) {
|
||||
addExceptionMapping(exceptionType, method);
|
||||
ExceptionHandlerMappingInfo mappingInfo = detectExceptionMappings(method);
|
||||
for (Class<? extends Throwable> exceptionType : mappingInfo.getExceptionTypes()) {
|
||||
for (MediaType producibleType : mappingInfo.getProducibleTypes()) {
|
||||
addExceptionMapping(new ExceptionMapping(exceptionType, producibleType), mappingInfo);
|
||||
}
|
||||
if (mappingInfo.getProducibleTypes().isEmpty()) {
|
||||
addExceptionMapping(new ExceptionMapping(exceptionType, MediaType.ALL), mappingInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -86,33 +108,42 @@ public class ExceptionHandlerMethodResolver {
|
||||
* and then as a fallback from the method signature itself.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<Class<? extends Throwable>> detectExceptionMappings(Method method) {
|
||||
List<Class<? extends Throwable>> result = new ArrayList<>();
|
||||
detectAnnotationExceptionMappings(method, result);
|
||||
if (result.isEmpty()) {
|
||||
private ExceptionHandlerMappingInfo detectExceptionMappings(Method method) {
|
||||
ExceptionHandler exceptionHandler = readExceptionHandlerAnnotation(method);
|
||||
List<Class<? extends Throwable>> exceptions = new ArrayList<>(Arrays.asList(exceptionHandler.exception()));
|
||||
if (exceptions.isEmpty()) {
|
||||
for (Class<?> paramType : method.getParameterTypes()) {
|
||||
if (Throwable.class.isAssignableFrom(paramType)) {
|
||||
result.add((Class<? extends Throwable>) paramType);
|
||||
exceptions.add((Class<? extends Throwable>) paramType);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (result.isEmpty()) {
|
||||
if (exceptions.isEmpty()) {
|
||||
throw new IllegalStateException("No exception types mapped to " + method);
|
||||
}
|
||||
return result;
|
||||
Set<MediaType> mediaTypes = new HashSet<>();
|
||||
for (String mediaType : exceptionHandler.produces()) {
|
||||
try {
|
||||
mediaTypes.add(MediaType.parseMediaType(mediaType));
|
||||
}
|
||||
catch (InvalidMediaTypeException exc) {
|
||||
throw new IllegalStateException("Invalid media type [" + mediaType + "] declared on @ExceptionHandler for " + method, exc);
|
||||
}
|
||||
}
|
||||
return new ExceptionHandlerMappingInfo(Set.copyOf(exceptions), mediaTypes, method);
|
||||
}
|
||||
|
||||
private void detectAnnotationExceptionMappings(Method method, List<Class<? extends Throwable>> result) {
|
||||
private ExceptionHandler readExceptionHandlerAnnotation(Method method) {
|
||||
ExceptionHandler ann = AnnotatedElementUtils.findMergedAnnotation(method, ExceptionHandler.class);
|
||||
Assert.state(ann != null, "No ExceptionHandler annotation");
|
||||
result.addAll(Arrays.asList(ann.value()));
|
||||
return ann;
|
||||
}
|
||||
|
||||
private void addExceptionMapping(Class<? extends Throwable> exceptionType, Method method) {
|
||||
Method oldMethod = this.mappedMethods.put(exceptionType, method);
|
||||
if (oldMethod != null && !oldMethod.equals(method)) {
|
||||
private void addExceptionMapping(ExceptionMapping mapping, ExceptionHandlerMappingInfo mappingInfo) {
|
||||
ExceptionHandlerMappingInfo oldMapping = this.mappedMethods.put(mapping, mappingInfo);
|
||||
if (oldMapping != null && !oldMapping.getHandlerMethod().equals(mappingInfo.getHandlerMethod())) {
|
||||
throw new IllegalStateException("Ambiguous @ExceptionHandler method mapped for [" +
|
||||
exceptionType + "]: {" + oldMethod + ", " + method + "}");
|
||||
mapping + "]: {" + oldMapping.getHandlerMethod() + ", " + mappingInfo.getHandlerMethod() + "}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,7 +162,8 @@ public class ExceptionHandlerMethodResolver {
|
||||
*/
|
||||
@Nullable
|
||||
public Method resolveMethod(Exception exception) {
|
||||
return resolveMethodByThrowable(exception);
|
||||
ExceptionHandlerMappingInfo mappingInfo = resolveExceptionMapping(exception, MediaType.ALL);
|
||||
return (mappingInfo != null) ? mappingInfo.getHandlerMethod() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -143,14 +175,29 @@ public class ExceptionHandlerMethodResolver {
|
||||
*/
|
||||
@Nullable
|
||||
public Method resolveMethodByThrowable(Throwable exception) {
|
||||
Method method = resolveMethodByExceptionType(exception.getClass());
|
||||
if (method == null) {
|
||||
ExceptionHandlerMappingInfo mappingInfo = resolveExceptionMapping(exception, MediaType.ALL);
|
||||
return (mappingInfo != null) ? mappingInfo.getHandlerMethod() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a {@link Method} to handle the given Throwable for the requested {@link MediaType}.
|
||||
* <p>Uses {@link ExceptionDepthComparator} and {@link MediaType#isMoreSpecific(MimeType)}
|
||||
* if more than one match is found.
|
||||
* @param exception the exception
|
||||
* @param mediaType the media type requested by the HTTP client
|
||||
* @return a Method to handle the exception, or {@code null} if none found
|
||||
* @since 6.2.0
|
||||
*/
|
||||
@Nullable
|
||||
public ExceptionHandlerMappingInfo resolveExceptionMapping(Throwable exception, MediaType mediaType) {
|
||||
ExceptionHandlerMappingInfo mappingInfo = resolveExceptionMappingByExceptionType(exception.getClass(), mediaType);
|
||||
if (mappingInfo == null) {
|
||||
Throwable cause = exception.getCause();
|
||||
if (cause != null) {
|
||||
method = resolveMethodByThrowable(cause);
|
||||
mappingInfo = resolveExceptionMapping(cause, mediaType);
|
||||
}
|
||||
}
|
||||
return method;
|
||||
return mappingInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -162,42 +209,92 @@ public class ExceptionHandlerMethodResolver {
|
||||
*/
|
||||
@Nullable
|
||||
public Method resolveMethodByExceptionType(Class<? extends Throwable> exceptionType) {
|
||||
Method method = this.exceptionLookupCache.get(exceptionType);
|
||||
if (method == null) {
|
||||
method = getMappedMethod(exceptionType);
|
||||
this.exceptionLookupCache.put(exceptionType, method);
|
||||
}
|
||||
return (method != NO_MATCHING_EXCEPTION_HANDLER_METHOD ? method : null);
|
||||
ExceptionHandlerMappingInfo mappingInfo = resolveExceptionMappingByExceptionType(exceptionType, MediaType.ALL);
|
||||
return (mappingInfo != null) ? mappingInfo.getHandlerMethod() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a {@link Method} to handle the given exception type and media type.
|
||||
* This can be useful if an {@link Exception} instance is not available (e.g. for tools).
|
||||
* @param exceptionType the exception type
|
||||
* @param mediaType the media type requested by the HTTP client
|
||||
* @return a Method to handle the exception, or {@code null} if none found
|
||||
*/
|
||||
@Nullable
|
||||
public ExceptionHandlerMappingInfo resolveExceptionMappingByExceptionType(Class<? extends Throwable> exceptionType, MediaType mediaType) {
|
||||
ExceptionHandlerMappingInfo mappingInfo = this.lookupCache.get(new ExceptionMapping(exceptionType, mediaType));
|
||||
return (mappingInfo != NO_MATCHING_EXCEPTION_HANDLER ? mappingInfo : null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the {@link Method} mapped to the given exception type, or
|
||||
* {@link #NO_MATCHING_EXCEPTION_HANDLER_METHOD} if none.
|
||||
* {@link #NO_MATCHING_EXCEPTION_HANDLER} if none.
|
||||
*/
|
||||
@Nullable
|
||||
private Method getMappedMethod(Class<? extends Throwable> exceptionType) {
|
||||
List<Class<? extends Throwable>> matches = new ArrayList<>();
|
||||
for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) {
|
||||
if (mappedException.isAssignableFrom(exceptionType)) {
|
||||
matches.add(mappedException);
|
||||
private ExceptionHandlerMappingInfo getMappedMethod(Class<? extends Throwable> exceptionType, MediaType mediaType) {
|
||||
List<ExceptionMapping> matches = new ArrayList<>();
|
||||
for (ExceptionMapping mappingInfo : this.mappedMethods.keySet()) {
|
||||
if (mappingInfo.exceptionType().isAssignableFrom(exceptionType) && mappingInfo.mediaType().isCompatibleWith(mediaType)) {
|
||||
matches.add(mappingInfo);
|
||||
}
|
||||
}
|
||||
if (!matches.isEmpty()) {
|
||||
if (matches.size() > 1) {
|
||||
matches.sort(new ExceptionDepthComparator(exceptionType));
|
||||
matches.sort(new ExceptionMapingComparator(exceptionType, mediaType));
|
||||
}
|
||||
return this.mappedMethods.get(matches.get(0));
|
||||
}
|
||||
else {
|
||||
return NO_MATCHING_EXCEPTION_HANDLER_METHOD;
|
||||
return NO_MATCHING_EXCEPTION_HANDLER;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For the {@link #NO_MATCHING_EXCEPTION_HANDLER_METHOD} constant.
|
||||
* For the {@link #NO_MATCHING_EXCEPTION_HANDLER} constant.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
private void noMatchingExceptionHandler() {
|
||||
}
|
||||
|
||||
private record ExceptionMapping(Class<? extends Throwable> exceptionType, MediaType mediaType) {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ExceptionHandler{" +
|
||||
"exceptionType=" + this.exceptionType.getCanonicalName() +
|
||||
", mediaType=" + this.mediaType +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
private static class ExceptionMapingComparator implements Comparator<ExceptionMapping> {
|
||||
|
||||
private final ExceptionDepthComparator exceptionDepthComparator;
|
||||
|
||||
private final MediaType mediaType;
|
||||
|
||||
public ExceptionMapingComparator(Class<? extends Throwable> exceptionType, MediaType mediaType) {
|
||||
this.exceptionDepthComparator = new ExceptionDepthComparator(exceptionType);
|
||||
this.mediaType = mediaType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compare(ExceptionMapping o1, ExceptionMapping o2) {
|
||||
int result = this.exceptionDepthComparator.compare(o1.exceptionType(), o2.exceptionType());
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
if(o1.mediaType.equals(this.mediaType)) {
|
||||
return -1;
|
||||
}
|
||||
if(o2.mediaType.equals(this.mediaType)) {
|
||||
return 1;
|
||||
}
|
||||
if (o1.mediaType.equals(o2.mediaType)) {
|
||||
return 0;
|
||||
}
|
||||
return (o1.mediaType.isMoreSpecific(o2.mediaType)) ? -1 : 1;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
@@ -33,28 +34,29 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
|
||||
/**
|
||||
* Test fixture for {@link ExceptionHandlerMethodResolver} tests.
|
||||
* Tests for {@link ExceptionHandlerMethodResolver}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
class ExceptionHandlerMethodResolverTests {
|
||||
|
||||
@Test
|
||||
void resolveMethodFromAnnotation() {
|
||||
void shouldResolveMethodFromAnnotationAttribute() {
|
||||
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(ExceptionController.class);
|
||||
IOException exception = new IOException();
|
||||
assertThat(resolver.resolveMethod(exception).getName()).isEqualTo("handleIOException");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveMethodFromArgument() {
|
||||
void shouldResolveMethodFromMethodArgument() {
|
||||
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(ExceptionController.class);
|
||||
IllegalArgumentException exception = new IllegalArgumentException();
|
||||
assertThat(resolver.resolveMethod(exception).getName()).isEqualTo("handleIllegalArgumentException");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveMethodExceptionSubType() {
|
||||
void shouldResolveMethodWithExceptionSubType() {
|
||||
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(ExceptionController.class);
|
||||
IOException ioException = new FileNotFoundException();
|
||||
assertThat(resolver.resolveMethod(ioException).getName()).isEqualTo("handleIOException");
|
||||
@@ -63,14 +65,14 @@ class ExceptionHandlerMethodResolverTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveMethodBestMatch() {
|
||||
void shouldResolveMethodWithExceptionBestMatch() {
|
||||
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(ExceptionController.class);
|
||||
SocketException exception = new SocketException();
|
||||
assertThat(resolver.resolveMethod(exception).getName()).isEqualTo("handleSocketException");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveMethodNoMatch() {
|
||||
void shouldNotResolveMethodWhenExceptionNoMatch() {
|
||||
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(ExceptionController.class);
|
||||
Exception exception = new Exception();
|
||||
assertThat(resolver.resolveMethod(exception)).as("1st lookup").isNull();
|
||||
@@ -78,7 +80,7 @@ class ExceptionHandlerMethodResolverTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveMethodExceptionCause() {
|
||||
void ShouldResolveMethodWithExceptionCause() {
|
||||
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(ExceptionController.class);
|
||||
|
||||
SocketException bindException = new BindException();
|
||||
@@ -90,24 +92,65 @@ class ExceptionHandlerMethodResolverTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveMethodInherited() {
|
||||
void shouldResolveMethodFromSuperClass() {
|
||||
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(InheritedController.class);
|
||||
IOException exception = new IOException();
|
||||
assertThat(resolver.resolveMethod(exception).getName()).isEqualTo("handleIOException");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ambiguousExceptionMapping() {
|
||||
void shouldThrowExceptionWhenAmbiguousExceptionMapping() {
|
||||
assertThatIllegalStateException().isThrownBy(() ->
|
||||
new ExceptionHandlerMethodResolver(AmbiguousController.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void noExceptionMapping() {
|
||||
void shouldThrowExceptionWhenNoExceptionMapping() {
|
||||
assertThatIllegalStateException().isThrownBy(() ->
|
||||
new ExceptionHandlerMethodResolver(NoExceptionController.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveMethodWithMediaType() {
|
||||
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(MediaTypeController.class);
|
||||
assertThat(resolver.resolveExceptionMapping(new IllegalArgumentException(), MediaType.APPLICATION_JSON).getHandlerMethod().getName()).isEqualTo("handleJson");
|
||||
assertThat(resolver.resolveExceptionMapping(new IllegalArgumentException(), MediaType.TEXT_HTML).getHandlerMethod().getName()).isEqualTo("handleHtml");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveMethodWithCompatibleMediaType() {
|
||||
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(MediaTypeController.class);
|
||||
assertThat(resolver.resolveExceptionMapping(new IllegalArgumentException(), MediaType.parseMediaType("application/*")).getHandlerMethod().getName()).isEqualTo("handleJson");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFavorMethodWithExplicitAcceptAll() {
|
||||
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(MediaTypeController.class);
|
||||
assertThat(resolver.resolveExceptionMapping(new IllegalArgumentException(), MediaType.ALL).getHandlerMethod().getName()).isEqualTo("handleHtml");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowExceptionWhenInvalidMediaTypeMapping() {
|
||||
assertThatIllegalStateException().isThrownBy(() ->
|
||||
new ExceptionHandlerMethodResolver(InvalidMediaTypeController.class))
|
||||
.withMessageContaining("Invalid media type [invalid-mediatype] declared on @ExceptionHandler");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldThrowExceptionWhenAmbiguousMediaTypeMapping() {
|
||||
assertThatIllegalStateException().isThrownBy(() ->
|
||||
new ExceptionHandlerMethodResolver(AmbiguousMediaTypeController.class))
|
||||
.withMessageContaining("Ambiguous @ExceptionHandler method mapped for [ExceptionHandler{exceptionType=java.lang.IllegalArgumentException, mediaType=application/json}]")
|
||||
.withMessageContaining("AmbiguousMediaTypeController.handleJson()")
|
||||
.withMessageContaining("AmbiguousMediaTypeController.handleJsonToo()");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldResolveMethodWithMediaTypeFallback() {
|
||||
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(MixedController.class);
|
||||
assertThat(resolver.resolveExceptionMapping(new IllegalArgumentException(), MediaType.TEXT_HTML).getHandlerMethod().getName()).isEqualTo("handleOther");
|
||||
}
|
||||
|
||||
|
||||
@Controller
|
||||
static class ExceptionController {
|
||||
@@ -162,4 +205,58 @@ class ExceptionHandlerMethodResolverTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Controller
|
||||
static class MediaTypeController {
|
||||
|
||||
@ExceptionHandler(exception = {IllegalArgumentException.class}, produces = "application/json")
|
||||
public void handleJson() {
|
||||
|
||||
}
|
||||
|
||||
@ExceptionHandler(exception = {IllegalArgumentException.class}, produces = {"text/html", "*/*"})
|
||||
public void handleHtml() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Controller
|
||||
static class AmbiguousMediaTypeController {
|
||||
|
||||
@ExceptionHandler(exception = {IllegalArgumentException.class}, produces = "application/json")
|
||||
public void handleJson() {
|
||||
|
||||
}
|
||||
|
||||
@ExceptionHandler(exception = {IllegalArgumentException.class}, produces = "application/json")
|
||||
public void handleJsonToo() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Controller
|
||||
static class MixedController {
|
||||
|
||||
@ExceptionHandler(exception = {IllegalArgumentException.class}, produces = "application/json")
|
||||
public void handleJson() {
|
||||
|
||||
}
|
||||
|
||||
@ExceptionHandler(IllegalArgumentException.class)
|
||||
public void handleOther() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Controller
|
||||
static class InvalidMediaTypeController {
|
||||
|
||||
@ExceptionHandler(exception = {IllegalArgumentException.class}, produces = "invalid-mediatype")
|
||||
public void handle() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -283,12 +283,14 @@ public class WebFluxConfigurationSupport implements ApplicationContextAware {
|
||||
@Qualifier("webFluxAdapterRegistry") ReactiveAdapterRegistry reactiveAdapterRegistry,
|
||||
ServerCodecConfigurer serverCodecConfigurer,
|
||||
@Qualifier("webFluxConversionService") FormattingConversionService conversionService,
|
||||
@Qualifier("webFluxContentTypeResolver") RequestedContentTypeResolver contentTypeResolver,
|
||||
@Qualifier("webFluxValidator") Validator validator) {
|
||||
|
||||
RequestMappingHandlerAdapter adapter = createRequestMappingHandlerAdapter();
|
||||
adapter.setMessageReaders(serverCodecConfigurer.getReaders());
|
||||
adapter.setWebBindingInitializer(getConfigurableWebBindingInitializer(conversionService, validator));
|
||||
adapter.setReactiveAdapterRegistry(reactiveAdapterRegistry);
|
||||
adapter.setContentTypeResolver(contentTypeResolver);
|
||||
|
||||
BlockingExecutionConfigurer executorConfigurer = getBlockingExecutionConfigurer();
|
||||
if (executorConfigurer.getExecutor() != null) {
|
||||
|
||||
@@ -38,6 +38,7 @@ import org.springframework.core.MethodIntrospector;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.ReactiveAdapterRegistry;
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.HttpMessageReader;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -50,12 +51,16 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.support.WebBindingInitializer;
|
||||
import org.springframework.web.method.ControllerAdviceBean;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.method.annotation.ExceptionHandlerMappingInfo;
|
||||
import org.springframework.web.method.annotation.ExceptionHandlerMethodResolver;
|
||||
import org.springframework.web.method.annotation.HandlerMethodValidator;
|
||||
import org.springframework.web.reactive.HandlerMapping;
|
||||
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
|
||||
import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.reactive.result.method.InvocableHandlerMethod;
|
||||
import org.springframework.web.reactive.result.method.SyncHandlerMethodArgumentResolver;
|
||||
import org.springframework.web.reactive.result.method.SyncInvocableHandlerMethod;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/**
|
||||
* Package-private class to assist {@link RequestMappingHandlerAdapter} with
|
||||
@@ -104,6 +109,8 @@ class ControllerMethodResolver {
|
||||
|
||||
private final ReactiveAdapterRegistry reactiveAdapterRegistry;
|
||||
|
||||
private final RequestedContentTypeResolver contentTypeResolver;
|
||||
|
||||
@Nullable
|
||||
private final Scheduler invocationScheduler;
|
||||
|
||||
@@ -129,16 +136,17 @@ class ControllerMethodResolver {
|
||||
private final Map<Class<?>, SessionAttributesHandler> sessionAttributesHandlerCache = new ConcurrentHashMap<>(64);
|
||||
|
||||
|
||||
|
||||
ControllerMethodResolver(
|
||||
ArgumentResolverConfigurer customResolvers, ReactiveAdapterRegistry adapterRegistry,
|
||||
ConfigurableApplicationContext context, List<HttpMessageReader<?>> readers,
|
||||
@Nullable WebBindingInitializer webBindingInitializer,
|
||||
@Nullable Scheduler invocationScheduler,
|
||||
@Nullable Predicate<? super HandlerMethod> blockingMethodPredicate) {
|
||||
ConfigurableApplicationContext context, RequestedContentTypeResolver contentTypeResolver,
|
||||
List<HttpMessageReader<?>> readers, @Nullable WebBindingInitializer webBindingInitializer,
|
||||
@Nullable Scheduler invocationScheduler, @Nullable Predicate<? super HandlerMethod> blockingMethodPredicate) {
|
||||
|
||||
Assert.notNull(customResolvers, "ArgumentResolverConfigurer is required");
|
||||
Assert.notNull(adapterRegistry, "ReactiveAdapterRegistry is required");
|
||||
Assert.notNull(context, "ApplicationContext is required");
|
||||
Assert.notNull(contentTypeResolver, "RequestedContentTypeResolver is required");
|
||||
Assert.notNull(readers, "HttpMessageReader List is required");
|
||||
|
||||
this.initBinderResolvers = initBinderResolvers(customResolvers, adapterRegistry, context);
|
||||
@@ -146,6 +154,7 @@ class ControllerMethodResolver {
|
||||
this.requestMappingResolvers = requestMappingResolvers(customResolvers, adapterRegistry, context, readers);
|
||||
this.exceptionHandlerResolvers = exceptionHandlerResolvers(customResolvers, adapterRegistry, context);
|
||||
this.reactiveAdapterRegistry = adapterRegistry;
|
||||
this.contentTypeResolver = contentTypeResolver;
|
||||
this.invocationScheduler = invocationScheduler;
|
||||
this.blockingMethodPredicate = blockingMethodPredicate;
|
||||
|
||||
@@ -398,44 +407,53 @@ class ControllerMethodResolver {
|
||||
* controller method, and also within {@code @ControllerAdvice} classes that
|
||||
* are applicable to the class of the given controller method.
|
||||
* @param ex the exception to find a handler for
|
||||
* @param handlerMethod the controller method that raised the exception, or
|
||||
* if {@code null}, check only {@code @ControllerAdvice} classes.
|
||||
* @param exchange the current HTTP exchange
|
||||
* @param handlerMethod the controller method that raised the exception,
|
||||
* or if {@code null}, check only {@code @ControllerAdvice} classes.
|
||||
*/
|
||||
@Nullable
|
||||
@SuppressWarnings("NullAway")
|
||||
public InvocableHandlerMethod getExceptionHandlerMethod(Throwable ex, @Nullable HandlerMethod handlerMethod) {
|
||||
public InvocableHandlerMethod getExceptionHandlerMethod(Throwable ex, ServerWebExchange exchange, @Nullable HandlerMethod handlerMethod) {
|
||||
|
||||
Class<?> handlerType = (handlerMethod != null ? handlerMethod.getBeanType() : null);
|
||||
Object exceptionHandlerObject = null;
|
||||
Method exceptionHandlerMethod = null;
|
||||
List<MediaType> requestedMediaTypes = this.contentTypeResolver.resolveMediaTypes(exchange);
|
||||
|
||||
// Controller-local first
|
||||
if (handlerType != null) {
|
||||
// Controller-local first...
|
||||
exceptionHandlerObject = handlerMethod.getBean();
|
||||
exceptionHandlerMethod = this.exceptionHandlerCache
|
||||
.computeIfAbsent(handlerType, ExceptionHandlerMethodResolver::new)
|
||||
.resolveMethodByThrowable(ex);
|
||||
for (MediaType mediaType : requestedMediaTypes) {
|
||||
ExceptionHandlerMappingInfo mappingInfo = this.exceptionHandlerCache
|
||||
.computeIfAbsent(handlerType, ExceptionHandlerMethodResolver::new)
|
||||
.resolveExceptionMapping(ex, mediaType);
|
||||
if (mappingInfo != null) {
|
||||
if (!mappingInfo.getProducibleTypes().isEmpty()) {
|
||||
exchange.getAttributes().put(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, mappingInfo.getProducibleTypes());
|
||||
}
|
||||
return createInvocableHandlerMethod(handlerMethod.getBean(), mappingInfo.getHandlerMethod());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (exceptionHandlerMethod == null) {
|
||||
// Global exception handlers...
|
||||
// Global exception handlers
|
||||
for (MediaType mediaType : requestedMediaTypes) {
|
||||
for (Map.Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry : this.exceptionHandlerAdviceCache.entrySet()) {
|
||||
ControllerAdviceBean advice = entry.getKey();
|
||||
if (advice.isApplicableToBeanType(handlerType)) {
|
||||
exceptionHandlerMethod = entry.getValue().resolveMethodByThrowable(ex);
|
||||
if (exceptionHandlerMethod != null) {
|
||||
exceptionHandlerObject = advice.resolveBean();
|
||||
break;
|
||||
ExceptionHandlerMappingInfo mappingInfo = entry.getValue().resolveExceptionMapping(ex, mediaType);
|
||||
if (mappingInfo != null) {
|
||||
if (!mappingInfo.getProducibleTypes().isEmpty()) {
|
||||
exchange.getAttributes().put(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, mappingInfo.getProducibleTypes());
|
||||
}
|
||||
return createInvocableHandlerMethod(advice.resolveBean(), mappingInfo.getHandlerMethod());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (exceptionHandlerObject == null || exceptionHandlerMethod == null) {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
InvocableHandlerMethod invocable = new InvocableHandlerMethod(exceptionHandlerObject, exceptionHandlerMethod);
|
||||
private InvocableHandlerMethod createInvocableHandlerMethod(Object bean, Method method) {
|
||||
InvocableHandlerMethod invocable = new InvocableHandlerMethod(bean, method);
|
||||
invocable.setArgumentResolvers(this.exceptionHandlerResolvers);
|
||||
return invocable;
|
||||
}
|
||||
|
||||
@@ -46,6 +46,8 @@ import org.springframework.web.reactive.DispatchExceptionHandler;
|
||||
import org.springframework.web.reactive.HandlerAdapter;
|
||||
import org.springframework.web.reactive.HandlerMapping;
|
||||
import org.springframework.web.reactive.HandlerResult;
|
||||
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
|
||||
import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder;
|
||||
import org.springframework.web.reactive.result.method.InvocableHandlerMethod;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.util.DisconnectedClientHelper;
|
||||
@@ -56,6 +58,7 @@ import org.springframework.web.util.DisconnectedClientHelper;
|
||||
* handler methods.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
* @since 5.0
|
||||
*/
|
||||
public class RequestMappingHandlerAdapter
|
||||
@@ -82,6 +85,8 @@ public class RequestMappingHandlerAdapter
|
||||
@Nullable
|
||||
private ArgumentResolverConfigurer argumentResolverConfigurer;
|
||||
|
||||
private RequestedContentTypeResolver contentTypeResolver = new RequestedContentTypeResolverBuilder().build();
|
||||
|
||||
@Nullable
|
||||
private Scheduler scheduler;
|
||||
|
||||
@@ -148,6 +153,24 @@ public class RequestMappingHandlerAdapter
|
||||
return this.argumentResolverConfigurer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link RequestedContentTypeResolver} to use to determine requested
|
||||
* media types. If not set, the default constructor is used.
|
||||
* @since 6.2.0
|
||||
*/
|
||||
public void setContentTypeResolver(RequestedContentTypeResolver contentTypeResolver) {
|
||||
Assert.notNull(contentTypeResolver, "'contentTypeResolver' must not be null");
|
||||
this.contentTypeResolver = contentTypeResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the configured {@link RequestedContentTypeResolver}.
|
||||
* @since 6.2.0
|
||||
*/
|
||||
public RequestedContentTypeResolver getContentTypeResolver() {
|
||||
return this.contentTypeResolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure an executor to invoke blocking controller methods with.
|
||||
* <p>By default, this is not set in which case controller methods are
|
||||
@@ -225,7 +248,7 @@ public class RequestMappingHandlerAdapter
|
||||
|
||||
this.methodResolver = new ControllerMethodResolver(
|
||||
this.argumentResolverConfigurer, this.reactiveAdapterRegistry, this.applicationContext,
|
||||
this.messageReaders, this.webBindingInitializer,
|
||||
this.contentTypeResolver, this.messageReaders, this.webBindingInitializer,
|
||||
this.scheduler, this.blockingMethodPredicate);
|
||||
|
||||
this.modelInitializer = new ModelInitializer(this.methodResolver, this.reactiveAdapterRegistry);
|
||||
@@ -280,7 +303,7 @@ public class RequestMappingHandlerAdapter
|
||||
exchange.getResponse().getHeaders().clearContentHeaders();
|
||||
|
||||
InvocableHandlerMethod invocable =
|
||||
this.methodResolver.getExceptionHandlerMethod(exception, handlerMethod);
|
||||
this.methodResolver.getExceptionHandlerMethod(exception, exchange, handlerMethod);
|
||||
|
||||
if (invocable != null) {
|
||||
ArrayList<Throwable> exceptions = new ArrayList<>();
|
||||
|
||||
@@ -37,6 +37,7 @@ import org.springframework.validation.Validator;
|
||||
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
|
||||
import org.springframework.web.ErrorResponse;
|
||||
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer;
|
||||
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
|
||||
import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder;
|
||||
import org.springframework.web.reactive.result.method.annotation.ResponseBodyResultHandler;
|
||||
import org.springframework.web.reactive.socket.server.WebSocketService;
|
||||
@@ -98,11 +99,12 @@ public class DelegatingWebFluxConfigurationTests {
|
||||
ReactiveAdapterRegistry reactiveAdapterRegistry = delegatingConfig.webFluxAdapterRegistry();
|
||||
ServerCodecConfigurer serverCodecConfigurer = delegatingConfig.serverCodecConfigurer();
|
||||
FormattingConversionService formattingConversionService = delegatingConfig.webFluxConversionService();
|
||||
RequestedContentTypeResolver requestedContentTypeResolver = delegatingConfig.webFluxContentTypeResolver();
|
||||
Validator validator = delegatingConfig.webFluxValidator();
|
||||
|
||||
ConfigurableWebBindingInitializer initializer = (ConfigurableWebBindingInitializer)
|
||||
this.delegatingConfig.requestMappingHandlerAdapter(reactiveAdapterRegistry, serverCodecConfigurer,
|
||||
formattingConversionService, validator).getWebBindingInitializer();
|
||||
formattingConversionService, requestedContentTypeResolver, validator).getWebBindingInitializer();
|
||||
|
||||
verify(webFluxConfigurer).configureHttpMessageCodecs(codecsConfigurer.capture());
|
||||
verify(webFluxConfigurer).getValidator();
|
||||
|
||||
@@ -18,6 +18,7 @@ package org.springframework.web.reactive.result.method.annotation;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@@ -30,6 +31,7 @@ import org.springframework.core.ReactiveAdapterRegistry;
|
||||
import org.springframework.core.codec.ByteArrayDecoder;
|
||||
import org.springframework.core.codec.ByteBufferDecoder;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.codec.ServerCodecConfigurer;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
@@ -39,13 +41,17 @@ import org.springframework.web.bind.annotation.InitBinder;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.reactive.BindingContext;
|
||||
import org.springframework.web.reactive.HandlerMapping;
|
||||
import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder;
|
||||
import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver;
|
||||
import org.springframework.web.reactive.result.method.InvocableHandlerMethod;
|
||||
import org.springframework.web.reactive.result.method.SyncHandlerMethodArgumentResolver;
|
||||
import org.springframework.web.reactive.result.method.SyncInvocableHandlerMethod;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.web.testfixture.method.ResolvableMethod;
|
||||
import org.springframework.web.testfixture.server.MockServerWebExchange;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -77,7 +83,8 @@ class ControllerMethodResolverTests {
|
||||
|
||||
this.methodResolver = new ControllerMethodResolver(
|
||||
resolvers, ReactiveAdapterRegistry.getSharedInstance(), applicationContext,
|
||||
codecs.getReaders(), null, null, null);
|
||||
new RequestedContentTypeResolverBuilder().build(), codecs.getReaders(),
|
||||
null, null, null);
|
||||
|
||||
Method method = ResolvableMethod.on(TestController.class).mockCall(TestController::handle).method();
|
||||
this.handlerMethod = new HandlerMethod(new TestController(), method);
|
||||
@@ -191,8 +198,9 @@ class ControllerMethodResolverTests {
|
||||
|
||||
@Test
|
||||
void exceptionHandlerArgumentResolvers() {
|
||||
MockServerWebExchange serverWebExchange = MockServerWebExchange.builder(MockServerHttpRequest.get("/test").build()).build();
|
||||
InvocableHandlerMethod invocable = this.methodResolver.getExceptionHandlerMethod(
|
||||
new ResponseStatusException(HttpStatus.BAD_REQUEST, "reason"), this.handlerMethod);
|
||||
new ResponseStatusException(HttpStatus.BAD_REQUEST, "reason"), serverWebExchange, this.handlerMethod);
|
||||
|
||||
assertThat(invocable).as("No match").isNotNull();
|
||||
assertThat(invocable.getBeanType()).isEqualTo(TestController.class);
|
||||
@@ -226,13 +234,30 @@ class ControllerMethodResolverTests {
|
||||
|
||||
@Test
|
||||
void exceptionHandlerFromControllerAdvice() {
|
||||
MockServerWebExchange serverWebExchange = MockServerWebExchange.builder(MockServerHttpRequest.get("/test").build()).build();
|
||||
InvocableHandlerMethod invocable = this.methodResolver.getExceptionHandlerMethod(
|
||||
new IllegalStateException("reason"), this.handlerMethod);
|
||||
new IllegalStateException("reason"), serverWebExchange, this.handlerMethod);
|
||||
|
||||
assertThat(invocable).isNotNull();
|
||||
assertThat(invocable.getBeanType()).isEqualTo(TestControllerAdvice.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void exceptionHandlerWithMediaType() {
|
||||
Method method = ResolvableMethod.on(ExceptionHandlerController.class).mockCall(ExceptionHandlerController::handle).method();
|
||||
this.handlerMethod = new HandlerMethod(new ExceptionHandlerController(), method);
|
||||
MockServerHttpRequest httpRequest = MockServerHttpRequest.get("/test").accept(MediaType.APPLICATION_JSON).build();
|
||||
MockServerWebExchange serverWebExchange = MockServerWebExchange.builder(httpRequest).build();
|
||||
InvocableHandlerMethod invocable = this.methodResolver.getExceptionHandlerMethod(
|
||||
new ResponseStatusException(HttpStatus.BAD_REQUEST, "reason"), serverWebExchange, this.handlerMethod);
|
||||
|
||||
assertThat(invocable).as("No match").isNotNull();
|
||||
assertThat(invocable.getBeanType()).isEqualTo(ExceptionHandlerController.class);
|
||||
assertThat(invocable.getMethod().getName()).isEqualTo("handleExceptionJson");
|
||||
Set<MediaType> producibleMediaTypes = serverWebExchange.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
|
||||
assertThat(producibleMediaTypes).isNotEmpty().contains(MediaType.APPLICATION_JSON);
|
||||
}
|
||||
|
||||
|
||||
private static HandlerMethodArgumentResolver next(
|
||||
List<? extends HandlerMethodArgumentResolver> resolvers, AtomicInteger index) {
|
||||
@@ -273,6 +298,20 @@ class ControllerMethodResolverTests {
|
||||
|
||||
}
|
||||
|
||||
@Controller
|
||||
static class ExceptionHandlerController {
|
||||
|
||||
@GetMapping
|
||||
void handle() {}
|
||||
|
||||
@ExceptionHandler(produces = "text/html")
|
||||
void handleExceptionHtml(ResponseStatusException ex) {}
|
||||
|
||||
@ExceptionHandler(produces = "application/json")
|
||||
void handleExceptionJson(ResponseStatusException ex) {}
|
||||
|
||||
}
|
||||
|
||||
|
||||
static class CustomArgumentResolver implements HandlerMethodArgumentResolver {
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ import org.springframework.web.bind.support.ConfigurableWebBindingInitializer;
|
||||
import org.springframework.web.bind.support.WebBindingInitializer;
|
||||
import org.springframework.web.bind.support.WebExchangeDataBinder;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder;
|
||||
import org.springframework.web.reactive.result.method.SyncInvocableHandlerMethod;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebSession;
|
||||
@@ -80,7 +81,8 @@ class ModelInitializerTests {
|
||||
|
||||
ControllerMethodResolver methodResolver = new ControllerMethodResolver(
|
||||
resolverConfigurer, adapterRegistry, new StaticApplicationContext(),
|
||||
Collections.emptyList(), null, null, null);
|
||||
new RequestedContentTypeResolverBuilder().build(), Collections.emptyList(),
|
||||
null, null, null);
|
||||
|
||||
this.modelInitializer = new ModelInitializer(methodResolver, adapterRegistry);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.ModelAttribute
|
||||
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer
|
||||
import org.springframework.web.method.HandlerMethod
|
||||
import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder
|
||||
import org.springframework.web.server.ServerWebExchange
|
||||
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest
|
||||
import org.springframework.web.testfixture.method.ResolvableMethod
|
||||
@@ -52,8 +53,10 @@ class ModelInitializerKotlinTests {
|
||||
val adapterRegistry = ReactiveAdapterRegistry.getSharedInstance()
|
||||
val resolverConfigurer = ArgumentResolverConfigurer()
|
||||
resolverConfigurer.addCustomResolver(ModelMethodArgumentResolver(adapterRegistry))
|
||||
val methodResolver = ControllerMethodResolver(resolverConfigurer, adapterRegistry, StaticApplicationContext(),
|
||||
emptyList(), null, null, null)
|
||||
val methodResolver = ControllerMethodResolver(
|
||||
resolverConfigurer, adapterRegistry, StaticApplicationContext(),
|
||||
RequestedContentTypeResolverBuilder().build(), emptyList(), null, null, null
|
||||
)
|
||||
modelInitializer = ModelInitializer(methodResolver, adapterRegistry)
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
package org.springframework.web.servlet.mvc.method.annotation;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
@@ -32,7 +31,9 @@ import org.springframework.aop.support.AopUtils;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.StringHttpMessageConverter;
|
||||
@@ -40,11 +41,14 @@ import org.springframework.http.converter.support.AllEncompassingFormHttpMessage
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.ui.ModelMap;
|
||||
import org.springframework.web.ErrorResponse;
|
||||
import org.springframework.web.HttpMediaTypeNotAcceptableException;
|
||||
import org.springframework.web.accept.ContentNegotiationManager;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.context.request.RequestAttributes;
|
||||
import org.springframework.web.context.request.ServletWebRequest;
|
||||
import org.springframework.web.method.ControllerAdviceBean;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.method.annotation.ExceptionHandlerMappingInfo;
|
||||
import org.springframework.web.method.annotation.ExceptionHandlerMethodResolver;
|
||||
import org.springframework.web.method.annotation.MapMethodProcessor;
|
||||
import org.springframework.web.method.annotation.ModelMethodProcessor;
|
||||
@@ -53,6 +57,7 @@ import org.springframework.web.method.support.HandlerMethodArgumentResolverCompo
|
||||
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
|
||||
import org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite;
|
||||
import org.springframework.web.method.support.ModelAndViewContainer;
|
||||
import org.springframework.web.servlet.HandlerMapping;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
import org.springframework.web.servlet.View;
|
||||
import org.springframework.web.servlet.handler.AbstractHandlerMethodExceptionResolver;
|
||||
@@ -73,6 +78,7 @@ import org.springframework.web.util.DisconnectedClientHelper;
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Juergen Hoeller
|
||||
* @author Sebastien Deleuze
|
||||
* @author Brian Clozel
|
||||
* @since 3.1
|
||||
*/
|
||||
public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver
|
||||
@@ -425,7 +431,9 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce
|
||||
protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request,
|
||||
HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception exception) {
|
||||
|
||||
ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
|
||||
ServletWebRequest webRequest = new ServletWebRequest(request, response);
|
||||
ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception, webRequest);
|
||||
|
||||
if (exceptionHandlerMethod == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -437,7 +445,6 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce
|
||||
exceptionHandlerMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
|
||||
}
|
||||
|
||||
ServletWebRequest webRequest = new ServletWebRequest(request, response);
|
||||
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
|
||||
|
||||
ArrayList<Throwable> exceptions = new ArrayList<>();
|
||||
@@ -497,11 +504,22 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce
|
||||
* Spring-managed beans were detected.
|
||||
* @param handlerMethod the method where the exception was raised (may be {@code null})
|
||||
* @param exception the raised exception
|
||||
* @param webRequest the original web request that resulted in a handler error
|
||||
* @return a method to handle the exception, or {@code null} if none
|
||||
*/
|
||||
@Nullable
|
||||
protected ServletInvocableHandlerMethod getExceptionHandlerMethod(
|
||||
@Nullable HandlerMethod handlerMethod, Exception exception) {
|
||||
@Nullable HandlerMethod handlerMethod, Exception exception, ServletWebRequest webRequest) {
|
||||
|
||||
List<MediaType> acceptedMediaTypes = List.of(MediaType.ALL);
|
||||
try {
|
||||
acceptedMediaTypes = this.contentNegotiationManager.resolveMediaTypes(webRequest);
|
||||
}
|
||||
catch (HttpMediaTypeNotAcceptableException mediaTypeExc) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Could not resolve accepted media types for @ExceptionHandler [" + webRequest.getHeader(HttpHeaders.ACCEPT) + "]", mediaTypeExc);
|
||||
}
|
||||
}
|
||||
|
||||
Class<?> handlerType = null;
|
||||
|
||||
@@ -511,9 +529,15 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce
|
||||
handlerType = handlerMethod.getBeanType();
|
||||
ExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.computeIfAbsent(
|
||||
handlerType, ExceptionHandlerMethodResolver::new);
|
||||
Method method = resolver.resolveMethod(exception);
|
||||
if (method != null) {
|
||||
return new ServletInvocableHandlerMethod(handlerMethod.getBean(), method, this.applicationContext);
|
||||
|
||||
for (MediaType mediaType : acceptedMediaTypes) {
|
||||
ExceptionHandlerMappingInfo mappingInfo = resolver.resolveExceptionMapping(exception, mediaType);
|
||||
if (mappingInfo != null) {
|
||||
if (!mappingInfo.getProducibleTypes().isEmpty()) {
|
||||
webRequest.setAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, mappingInfo.getProducibleTypes(), RequestAttributes.SCOPE_REQUEST);
|
||||
}
|
||||
return new ServletInvocableHandlerMethod(handlerMethod.getBean(), mappingInfo.getHandlerMethod(), this.applicationContext);
|
||||
}
|
||||
}
|
||||
// For advice applicability check below (involving base packages, assignable types
|
||||
// and annotation presence), use target class instead of interface-based proxy.
|
||||
@@ -526,9 +550,14 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce
|
||||
ControllerAdviceBean advice = entry.getKey();
|
||||
if (advice.isApplicableToBeanType(handlerType)) {
|
||||
ExceptionHandlerMethodResolver resolver = entry.getValue();
|
||||
Method method = resolver.resolveMethod(exception);
|
||||
if (method != null) {
|
||||
return new ServletInvocableHandlerMethod(advice.resolveBean(), method, this.applicationContext);
|
||||
for (MediaType mediaType : acceptedMediaTypes) {
|
||||
ExceptionHandlerMappingInfo mappingInfo = resolver.resolveExceptionMapping(exception, mediaType);
|
||||
if (mappingInfo != null) {
|
||||
if (!mappingInfo.getProducibleTypes().isEmpty()) {
|
||||
webRequest.setAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, mappingInfo.getProducibleTypes(), RequestAttributes.SCOPE_REQUEST);
|
||||
}
|
||||
return new ServletInvocableHandlerMethod(advice.resolveBean(), mappingInfo.getHandlerMethod(), this.applicationContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Test fixture with {@link ExceptionHandlerExceptionResolver}.
|
||||
* Tests for {@link ExceptionHandlerExceptionResolver}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Arjen Poutsma
|
||||
@@ -419,6 +419,41 @@ class ExceptionHandlerExceptionResolverTests {
|
||||
assertThat(mav.isEmpty()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveExceptionJsonMediaType() throws UnsupportedEncodingException, NoSuchMethodException {
|
||||
IllegalArgumentException ex = new IllegalArgumentException();
|
||||
HandlerMethod handlerMethod = new HandlerMethod(new MediaTypeController(), "handle");
|
||||
this.resolver.afterPropertiesSet();
|
||||
this.request.addHeader("Accept", "application/json");
|
||||
ModelAndView mav = this.resolver.resolveException(this.request, this.response, handlerMethod, ex);
|
||||
|
||||
assertExceptionHandledAsBody(mav, "jsonBody");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveExceptionHtmlMediaType() throws NoSuchMethodException {
|
||||
IllegalArgumentException ex = new IllegalArgumentException();
|
||||
HandlerMethod handlerMethod = new HandlerMethod(new MediaTypeController(), "handle");
|
||||
this.resolver.afterPropertiesSet();
|
||||
this.request.addHeader("Accept", "text/html");
|
||||
ModelAndView mav = this.resolver.resolveException(this.request, this.response, handlerMethod, ex);
|
||||
|
||||
assertThat(mav).isNotNull();
|
||||
assertThat(mav.getViewName()).isEqualTo("htmlView");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveExceptionDefaultMediaType() throws NoSuchMethodException {
|
||||
IllegalArgumentException ex = new IllegalArgumentException();
|
||||
HandlerMethod handlerMethod = new HandlerMethod(new MediaTypeController(), "handle");
|
||||
this.resolver.afterPropertiesSet();
|
||||
this.request.addHeader("Accept", "*/*");
|
||||
ModelAndView mav = this.resolver.resolveException(this.request, this.response, handlerMethod, ex);
|
||||
|
||||
assertThat(mav).isNotNull();
|
||||
assertThat(mav.getViewName()).isEqualTo("htmlView");
|
||||
}
|
||||
|
||||
|
||||
private void assertMethodProcessorCount(int resolverCount, int handlerCount) {
|
||||
assertThat(this.resolver.getArgumentResolvers().getResolvers()).hasSize(resolverCount);
|
||||
@@ -650,4 +685,21 @@ class ExceptionHandlerExceptionResolverTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Controller
|
||||
static class MediaTypeController {
|
||||
|
||||
public void handle() {}
|
||||
|
||||
@ExceptionHandler(exception = IllegalArgumentException.class, produces = "application/json")
|
||||
@ResponseBody
|
||||
public String handleExceptionJson() {
|
||||
return "jsonBody";
|
||||
}
|
||||
|
||||
@ExceptionHandler(exception = IllegalArgumentException.class, produces = {"text/html", "*/*"})
|
||||
public String handleExceptionHtml() {
|
||||
return "htmlView";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user