/*
 * Copyright 2002-2018 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
 *
 *      http://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.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.springframework.core.ExceptionDepthComparator;
import org.springframework.core.MethodIntrospector;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.ReflectionUtils.MethodFilter;
import org.springframework.web.bind.annotation.ExceptionHandler;

Discovers @ExceptionHandler methods in a given class, including all of its superclasses, and helps to resolve a given Exception to the exception types supported by a given Method.
Author:Rossen Stoyanchev, Juergen Hoeller
Since:3.1
/** * 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}. * * @author Rossen Stoyanchev * @author Juergen Hoeller * @since 3.1 */
public class ExceptionHandlerMethodResolver {
A filter for selecting @ExceptionHandler methods.
/** * A filter for selecting {@code @ExceptionHandler} methods. */
public static final MethodFilter EXCEPTION_HANDLER_METHODS = method -> AnnotatedElementUtils.hasAnnotation(method, ExceptionHandler.class); private final Map<Class<? extends Throwable>, Method> mappedMethods = new HashMap<>(16); private final Map<Class<? extends Throwable>, Method> exceptionLookupCache = new ConcurrentReferenceHashMap<>(16);
A constructor that finds ExceptionHandler methods in the given type.
Params:
  • handlerType – the type to introspect
/** * A constructor that finds {@link ExceptionHandler} methods in the given type. * @param handlerType the type to introspect */
public ExceptionHandlerMethodResolver(Class<?> handlerType) { for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)) { for (Class<? extends Throwable> exceptionType : detectExceptionMappings(method)) { addExceptionMapping(exceptionType, method); } } }
Extract exception mappings from the @ExceptionHandler annotation first, and then as a fallback from the method signature itself.
/** * Extract exception mappings from the {@code @ExceptionHandler} annotation first, * 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()) { for (Class<?> paramType : method.getParameterTypes()) { if (Throwable.class.isAssignableFrom(paramType)) { result.add((Class<? extends Throwable>) paramType); } } } if (result.isEmpty()) { throw new IllegalStateException("No exception types mapped to " + method); } return result; } private void detectAnnotationExceptionMappings(Method method, List<Class<? extends Throwable>> result) { ExceptionHandler ann = AnnotatedElementUtils.findMergedAnnotation(method, ExceptionHandler.class); Assert.state(ann != null, "No ExceptionHandler annotation"); result.addAll(Arrays.asList(ann.value())); } private void addExceptionMapping(Class<? extends Throwable> exceptionType, Method method) { Method oldMethod = this.mappedMethods.put(exceptionType, method); if (oldMethod != null && !oldMethod.equals(method)) { throw new IllegalStateException("Ambiguous @ExceptionHandler method mapped for [" + exceptionType + "]: {" + oldMethod + ", " + method + "}"); } }
Whether the contained type has any exception mappings.
/** * Whether the contained type has any exception mappings. */
public boolean hasExceptionMappings() { return !this.mappedMethods.isEmpty(); }
Find a Method to handle the given exception. Use ExceptionDepthComparator if more than one match is found.
Params:
  • exception – the exception
Returns:a Method to handle the exception, or null if none found
/** * Find a {@link Method} to handle the given exception. * Use {@link ExceptionDepthComparator} if more than one match is found. * @param exception the exception * @return a Method to handle the exception, or {@code null} if none found */
@Nullable public Method resolveMethod(Exception exception) { return resolveMethodByThrowable(exception); }
Find a Method to handle the given Throwable. Use ExceptionDepthComparator if more than one match is found.
Params:
  • exception – the exception
Returns:a Method to handle the exception, or null if none found
Since:5.0
/** * Find a {@link Method} to handle the given Throwable. * Use {@link ExceptionDepthComparator} if more than one match is found. * @param exception the exception * @return a Method to handle the exception, or {@code null} if none found * @since 5.0 */
@Nullable public Method resolveMethodByThrowable(Throwable exception) { Method method = resolveMethodByExceptionType(exception.getClass()); if (method == null) { Throwable cause = exception.getCause(); if (cause != null) { method = resolveMethodByExceptionType(cause.getClass()); } } return method; }
Find a Method to handle the given exception type. This can be useful if an Exception instance is not available (e.g. for tools).
Params:
  • exceptionType – the exception type
Returns:a Method to handle the exception, or null if none found
/** * Find a {@link Method} to handle the given exception type. This can be * useful if an {@link Exception} instance is not available (e.g. for tools). * @param exceptionType the exception type * @return a Method to handle the exception, or {@code null} if none found */
@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; }
Return the Method mapped to the given exception type, or null if none.
/** * Return the {@link Method} mapped to the given exception type, or {@code null} 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); } } if (!matches.isEmpty()) { matches.sort(new ExceptionDepthComparator(exceptionType)); return this.mappedMethods.get(matches.get(0)); } else { return null; } } }