001/* 002 * Copyright 2002-2018 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.web.method.annotation; 018 019import java.lang.reflect.Method; 020import java.util.ArrayList; 021import java.util.Arrays; 022import java.util.HashMap; 023import java.util.List; 024import java.util.Map; 025 026import org.springframework.core.ExceptionDepthComparator; 027import org.springframework.core.MethodIntrospector; 028import org.springframework.core.annotation.AnnotatedElementUtils; 029import org.springframework.lang.Nullable; 030import org.springframework.util.Assert; 031import org.springframework.util.ConcurrentReferenceHashMap; 032import org.springframework.util.ReflectionUtils.MethodFilter; 033import org.springframework.web.bind.annotation.ExceptionHandler; 034 035/** 036 * Discovers {@linkplain ExceptionHandler @ExceptionHandler} methods in a given class, 037 * including all of its superclasses, and helps to resolve a given {@link Exception} 038 * to the exception types supported by a given {@link Method}. 039 * 040 * @author Rossen Stoyanchev 041 * @author Juergen Hoeller 042 * @since 3.1 043 */ 044public class ExceptionHandlerMethodResolver { 045 046 /** 047 * A filter for selecting {@code @ExceptionHandler} methods. 048 */ 049 public static final MethodFilter EXCEPTION_HANDLER_METHODS = method -> 050 AnnotatedElementUtils.hasAnnotation(method, ExceptionHandler.class); 051 052 053 private final Map<Class<? extends Throwable>, Method> mappedMethods = new HashMap<>(16); 054 055 private final Map<Class<? extends Throwable>, Method> exceptionLookupCache = new ConcurrentReferenceHashMap<>(16); 056 057 058 /** 059 * A constructor that finds {@link ExceptionHandler} methods in the given type. 060 * @param handlerType the type to introspect 061 */ 062 public ExceptionHandlerMethodResolver(Class<?> handlerType) { 063 for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)) { 064 for (Class<? extends Throwable> exceptionType : detectExceptionMappings(method)) { 065 addExceptionMapping(exceptionType, method); 066 } 067 } 068 } 069 070 071 /** 072 * Extract exception mappings from the {@code @ExceptionHandler} annotation first, 073 * and then as a fallback from the method signature itself. 074 */ 075 @SuppressWarnings("unchecked") 076 private List<Class<? extends Throwable>> detectExceptionMappings(Method method) { 077 List<Class<? extends Throwable>> result = new ArrayList<>(); 078 detectAnnotationExceptionMappings(method, result); 079 if (result.isEmpty()) { 080 for (Class<?> paramType : method.getParameterTypes()) { 081 if (Throwable.class.isAssignableFrom(paramType)) { 082 result.add((Class<? extends Throwable>) paramType); 083 } 084 } 085 } 086 if (result.isEmpty()) { 087 throw new IllegalStateException("No exception types mapped to " + method); 088 } 089 return result; 090 } 091 092 private void detectAnnotationExceptionMappings(Method method, List<Class<? extends Throwable>> result) { 093 ExceptionHandler ann = AnnotatedElementUtils.findMergedAnnotation(method, ExceptionHandler.class); 094 Assert.state(ann != null, "No ExceptionHandler annotation"); 095 result.addAll(Arrays.asList(ann.value())); 096 } 097 098 private void addExceptionMapping(Class<? extends Throwable> exceptionType, Method method) { 099 Method oldMethod = this.mappedMethods.put(exceptionType, method); 100 if (oldMethod != null && !oldMethod.equals(method)) { 101 throw new IllegalStateException("Ambiguous @ExceptionHandler method mapped for [" + 102 exceptionType + "]: {" + oldMethod + ", " + method + "}"); 103 } 104 } 105 106 /** 107 * Whether the contained type has any exception mappings. 108 */ 109 public boolean hasExceptionMappings() { 110 return !this.mappedMethods.isEmpty(); 111 } 112 113 /** 114 * Find a {@link Method} to handle the given exception. 115 * Use {@link ExceptionDepthComparator} if more than one match is found. 116 * @param exception the exception 117 * @return a Method to handle the exception, or {@code null} if none found 118 */ 119 @Nullable 120 public Method resolveMethod(Exception exception) { 121 return resolveMethodByThrowable(exception); 122 } 123 124 /** 125 * Find a {@link Method} to handle the given Throwable. 126 * Use {@link ExceptionDepthComparator} if more than one match is found. 127 * @param exception the exception 128 * @return a Method to handle the exception, or {@code null} if none found 129 * @since 5.0 130 */ 131 @Nullable 132 public Method resolveMethodByThrowable(Throwable exception) { 133 Method method = resolveMethodByExceptionType(exception.getClass()); 134 if (method == null) { 135 Throwable cause = exception.getCause(); 136 if (cause != null) { 137 method = resolveMethodByExceptionType(cause.getClass()); 138 } 139 } 140 return method; 141 } 142 143 /** 144 * Find a {@link Method} to handle the given exception type. This can be 145 * useful if an {@link Exception} instance is not available (e.g. for tools). 146 * @param exceptionType the exception type 147 * @return a Method to handle the exception, or {@code null} if none found 148 */ 149 @Nullable 150 public Method resolveMethodByExceptionType(Class<? extends Throwable> exceptionType) { 151 Method method = this.exceptionLookupCache.get(exceptionType); 152 if (method == null) { 153 method = getMappedMethod(exceptionType); 154 this.exceptionLookupCache.put(exceptionType, method); 155 } 156 return method; 157 } 158 159 /** 160 * Return the {@link Method} mapped to the given exception type, or {@code null} if none. 161 */ 162 @Nullable 163 private Method getMappedMethod(Class<? extends Throwable> exceptionType) { 164 List<Class<? extends Throwable>> matches = new ArrayList<>(); 165 for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) { 166 if (mappedException.isAssignableFrom(exceptionType)) { 167 matches.add(mappedException); 168 } 169 } 170 if (!matches.isEmpty()) { 171 matches.sort(new ExceptionDepthComparator(exceptionType)); 172 return this.mappedMethods.get(matches.get(0)); 173 } 174 else { 175 return null; 176 } 177 } 178 179}