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:
Brian Clozel
2024-05-20 17:22:30 +02:00
parent 991be14847
commit 4d4b343815
25 changed files with 984 additions and 212 deletions

View File

@@ -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]]

View File

@@ -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]#

View File

@@ -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");
}
}

View File

@@ -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) {
}
}

View File

@@ -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");
}
}

View File

@@ -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[]
}

View File

@@ -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) {
}
}

View File

@@ -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")
}
}

View File

@@ -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)
}

View File

@@ -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")
}
}

View File

@@ -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[]
}

View File

@@ -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)
}

View File

@@ -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 {};
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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() {
}
}
}

View File

@@ -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) {

View File

@@ -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;
}

View File

@@ -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<>();

View File

@@ -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();

View File

@@ -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 {

View File

@@ -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);
}

View File

@@ -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)
}

View File

@@ -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);
}
}
}
}

View File

@@ -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";
}
}
}