001/*
002 * Copyright 2002-2019 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.servlet.mvc.method.annotation;
018
019import java.lang.reflect.Method;
020import java.lang.reflect.Proxy;
021import java.util.ArrayList;
022import java.util.Collections;
023import java.util.LinkedHashMap;
024import java.util.List;
025import java.util.Map;
026import java.util.concurrent.ConcurrentHashMap;
027
028import javax.servlet.http.HttpServletRequest;
029import javax.servlet.http.HttpServletResponse;
030
031import org.springframework.aop.support.AopUtils;
032import org.springframework.beans.factory.InitializingBean;
033import org.springframework.context.ApplicationContext;
034import org.springframework.context.ApplicationContextAware;
035import org.springframework.http.HttpStatus;
036import org.springframework.http.converter.ByteArrayHttpMessageConverter;
037import org.springframework.http.converter.HttpMessageConverter;
038import org.springframework.http.converter.StringHttpMessageConverter;
039import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
040import org.springframework.http.converter.xml.SourceHttpMessageConverter;
041import org.springframework.lang.Nullable;
042import org.springframework.ui.ModelMap;
043import org.springframework.web.accept.ContentNegotiationManager;
044import org.springframework.web.bind.annotation.ControllerAdvice;
045import org.springframework.web.context.request.ServletWebRequest;
046import org.springframework.web.method.ControllerAdviceBean;
047import org.springframework.web.method.HandlerMethod;
048import org.springframework.web.method.annotation.ExceptionHandlerMethodResolver;
049import org.springframework.web.method.annotation.MapMethodProcessor;
050import org.springframework.web.method.annotation.ModelAttributeMethodProcessor;
051import org.springframework.web.method.annotation.ModelMethodProcessor;
052import org.springframework.web.method.support.HandlerMethodArgumentResolver;
053import org.springframework.web.method.support.HandlerMethodArgumentResolverComposite;
054import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
055import org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite;
056import org.springframework.web.method.support.ModelAndViewContainer;
057import org.springframework.web.servlet.ModelAndView;
058import org.springframework.web.servlet.View;
059import org.springframework.web.servlet.handler.AbstractHandlerMethodExceptionResolver;
060import org.springframework.web.servlet.mvc.support.RedirectAttributes;
061import org.springframework.web.servlet.support.RequestContextUtils;
062
063/**
064 * An {@link AbstractHandlerMethodExceptionResolver} that resolves exceptions
065 * through {@code @ExceptionHandler} methods.
066 *
067 * <p>Support for custom argument and return value types can be added via
068 * {@link #setCustomArgumentResolvers} and {@link #setCustomReturnValueHandlers}.
069 * Or alternatively to re-configure all argument and return value types use
070 * {@link #setArgumentResolvers} and {@link #setReturnValueHandlers(List)}.
071 *
072 * @author Rossen Stoyanchev
073 * @author Juergen Hoeller
074 * @since 3.1
075 */
076public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver
077                implements ApplicationContextAware, InitializingBean {
078
079        @Nullable
080        private List<HandlerMethodArgumentResolver> customArgumentResolvers;
081
082        @Nullable
083        private HandlerMethodArgumentResolverComposite argumentResolvers;
084
085        @Nullable
086        private List<HandlerMethodReturnValueHandler> customReturnValueHandlers;
087
088        @Nullable
089        private HandlerMethodReturnValueHandlerComposite returnValueHandlers;
090
091        private List<HttpMessageConverter<?>> messageConverters;
092
093        private ContentNegotiationManager contentNegotiationManager = new ContentNegotiationManager();
094
095        private final List<Object> responseBodyAdvice = new ArrayList<>();
096
097        @Nullable
098        private ApplicationContext applicationContext;
099
100        private final Map<Class<?>, ExceptionHandlerMethodResolver> exceptionHandlerCache =
101                        new ConcurrentHashMap<>(64);
102
103        private final Map<ControllerAdviceBean, ExceptionHandlerMethodResolver> exceptionHandlerAdviceCache =
104                        new LinkedHashMap<>();
105
106
107        public ExceptionHandlerExceptionResolver() {
108                this.messageConverters = new ArrayList<>();
109                this.messageConverters.add(new ByteArrayHttpMessageConverter());
110                this.messageConverters.add(new StringHttpMessageConverter());
111                try {
112                        this.messageConverters.add(new SourceHttpMessageConverter<>());
113                }
114                catch (Error err) {
115                        // Ignore when no TransformerFactory implementation is available
116                }
117                this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());
118        }
119
120
121        /**
122         * Provide resolvers for custom argument types. Custom resolvers are ordered
123         * after built-in ones. To override the built-in support for argument
124         * resolution use {@link #setArgumentResolvers} instead.
125         */
126        public void setCustomArgumentResolvers(@Nullable List<HandlerMethodArgumentResolver> argumentResolvers) {
127                this.customArgumentResolvers = argumentResolvers;
128        }
129
130        /**
131         * Return the custom argument resolvers, or {@code null}.
132         */
133        @Nullable
134        public List<HandlerMethodArgumentResolver> getCustomArgumentResolvers() {
135                return this.customArgumentResolvers;
136        }
137
138        /**
139         * Configure the complete list of supported argument types thus overriding
140         * the resolvers that would otherwise be configured by default.
141         */
142        public void setArgumentResolvers(@Nullable List<HandlerMethodArgumentResolver> argumentResolvers) {
143                if (argumentResolvers == null) {
144                        this.argumentResolvers = null;
145                }
146                else {
147                        this.argumentResolvers = new HandlerMethodArgumentResolverComposite();
148                        this.argumentResolvers.addResolvers(argumentResolvers);
149                }
150        }
151
152        /**
153         * Return the configured argument resolvers, or possibly {@code null} if
154         * not initialized yet via {@link #afterPropertiesSet()}.
155         */
156        @Nullable
157        public HandlerMethodArgumentResolverComposite getArgumentResolvers() {
158                return this.argumentResolvers;
159        }
160
161        /**
162         * Provide handlers for custom return value types. Custom handlers are
163         * ordered after built-in ones. To override the built-in support for
164         * return value handling use {@link #setReturnValueHandlers}.
165         */
166        public void setCustomReturnValueHandlers(@Nullable List<HandlerMethodReturnValueHandler> returnValueHandlers) {
167                this.customReturnValueHandlers = returnValueHandlers;
168        }
169
170        /**
171         * Return the custom return value handlers, or {@code null}.
172         */
173        @Nullable
174        public List<HandlerMethodReturnValueHandler> getCustomReturnValueHandlers() {
175                return this.customReturnValueHandlers;
176        }
177
178        /**
179         * Configure the complete list of supported return value types thus
180         * overriding handlers that would otherwise be configured by default.
181         */
182        public void setReturnValueHandlers(@Nullable List<HandlerMethodReturnValueHandler> returnValueHandlers) {
183                if (returnValueHandlers == null) {
184                        this.returnValueHandlers = null;
185                }
186                else {
187                        this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite();
188                        this.returnValueHandlers.addHandlers(returnValueHandlers);
189                }
190        }
191
192        /**
193         * Return the configured handlers, or possibly {@code null} if not
194         * initialized yet via {@link #afterPropertiesSet()}.
195         */
196        @Nullable
197        public HandlerMethodReturnValueHandlerComposite getReturnValueHandlers() {
198                return this.returnValueHandlers;
199        }
200
201        /**
202         * Set the message body converters to use.
203         * <p>These converters are used to convert from and to HTTP requests and responses.
204         */
205        public void setMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
206                this.messageConverters = messageConverters;
207        }
208
209        /**
210         * Return the configured message body converters.
211         */
212        public List<HttpMessageConverter<?>> getMessageConverters() {
213                return this.messageConverters;
214        }
215
216        /**
217         * Set the {@link ContentNegotiationManager} to use to determine requested media types.
218         * If not set, the default constructor is used.
219         */
220        public void setContentNegotiationManager(ContentNegotiationManager contentNegotiationManager) {
221                this.contentNegotiationManager = contentNegotiationManager;
222        }
223
224        /**
225         * Return the configured {@link ContentNegotiationManager}.
226         */
227        public ContentNegotiationManager getContentNegotiationManager() {
228                return this.contentNegotiationManager;
229        }
230
231        /**
232         * Add one or more components to be invoked after the execution of a controller
233         * method annotated with {@code @ResponseBody} or returning {@code ResponseEntity}
234         * but before the body is written to the response with the selected
235         * {@code HttpMessageConverter}.
236         */
237        public void setResponseBodyAdvice(@Nullable List<ResponseBodyAdvice<?>> responseBodyAdvice) {
238                this.responseBodyAdvice.clear();
239                if (responseBodyAdvice != null) {
240                        this.responseBodyAdvice.addAll(responseBodyAdvice);
241                }
242        }
243
244        @Override
245        public void setApplicationContext(@Nullable ApplicationContext applicationContext) {
246                this.applicationContext = applicationContext;
247        }
248
249        @Nullable
250        public ApplicationContext getApplicationContext() {
251                return this.applicationContext;
252        }
253
254
255        @Override
256        public void afterPropertiesSet() {
257                // Do this first, it may add ResponseBodyAdvice beans
258                initExceptionHandlerAdviceCache();
259
260                if (this.argumentResolvers == null) {
261                        List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
262                        this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
263                }
264                if (this.returnValueHandlers == null) {
265                        List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
266                        this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
267                }
268        }
269
270        private void initExceptionHandlerAdviceCache() {
271                if (getApplicationContext() == null) {
272                        return;
273                }
274
275                List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
276                for (ControllerAdviceBean adviceBean : adviceBeans) {
277                        Class<?> beanType = adviceBean.getBeanType();
278                        if (beanType == null) {
279                                throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
280                        }
281                        ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
282                        if (resolver.hasExceptionMappings()) {
283                                this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
284                        }
285                        if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
286                                this.responseBodyAdvice.add(adviceBean);
287                        }
288                }
289
290                if (logger.isDebugEnabled()) {
291                        int handlerSize = this.exceptionHandlerAdviceCache.size();
292                        int adviceSize = this.responseBodyAdvice.size();
293                        if (handlerSize == 0 && adviceSize == 0) {
294                                logger.debug("ControllerAdvice beans: none");
295                        }
296                        else {
297                                logger.debug("ControllerAdvice beans: " +
298                                                handlerSize + " @ExceptionHandler, " + adviceSize + " ResponseBodyAdvice");
299                        }
300                }
301        }
302
303        /**
304         * Return an unmodifiable Map with the {@link ControllerAdvice @ControllerAdvice}
305         * beans discovered in the ApplicationContext. The returned map will be empty if
306         * the method is invoked before the bean has been initialized via
307         * {@link #afterPropertiesSet()}.
308         */
309        public Map<ControllerAdviceBean, ExceptionHandlerMethodResolver> getExceptionHandlerAdviceCache() {
310                return Collections.unmodifiableMap(this.exceptionHandlerAdviceCache);
311        }
312
313        /**
314         * Return the list of argument resolvers to use including built-in resolvers
315         * and custom resolvers provided via {@link #setCustomArgumentResolvers}.
316         */
317        protected List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
318                List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>();
319
320                // Annotation-based argument resolution
321                resolvers.add(new SessionAttributeMethodArgumentResolver());
322                resolvers.add(new RequestAttributeMethodArgumentResolver());
323
324                // Type-based argument resolution
325                resolvers.add(new ServletRequestMethodArgumentResolver());
326                resolvers.add(new ServletResponseMethodArgumentResolver());
327                resolvers.add(new RedirectAttributesMethodArgumentResolver());
328                resolvers.add(new ModelMethodProcessor());
329
330                // Custom arguments
331                if (getCustomArgumentResolvers() != null) {
332                        resolvers.addAll(getCustomArgumentResolvers());
333                }
334
335                return resolvers;
336        }
337
338        /**
339         * Return the list of return value handlers to use including built-in and
340         * custom handlers provided via {@link #setReturnValueHandlers}.
341         */
342        protected List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers() {
343                List<HandlerMethodReturnValueHandler> handlers = new ArrayList<>();
344
345                // Single-purpose return value types
346                handlers.add(new ModelAndViewMethodReturnValueHandler());
347                handlers.add(new ModelMethodProcessor());
348                handlers.add(new ViewMethodReturnValueHandler());
349                handlers.add(new HttpEntityMethodProcessor(
350                                getMessageConverters(), this.contentNegotiationManager, this.responseBodyAdvice));
351
352                // Annotation-based return value types
353                handlers.add(new ModelAttributeMethodProcessor(false));
354                handlers.add(new RequestResponseBodyMethodProcessor(
355                                getMessageConverters(), this.contentNegotiationManager, this.responseBodyAdvice));
356
357                // Multi-purpose return value types
358                handlers.add(new ViewNameMethodReturnValueHandler());
359                handlers.add(new MapMethodProcessor());
360
361                // Custom return value types
362                if (getCustomReturnValueHandlers() != null) {
363                        handlers.addAll(getCustomReturnValueHandlers());
364                }
365
366                // Catch-all
367                handlers.add(new ModelAttributeMethodProcessor(true));
368
369                return handlers;
370        }
371
372
373        /**
374         * Find an {@code @ExceptionHandler} method and invoke it to handle the raised exception.
375         */
376        @Override
377        @Nullable
378        protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request,
379                        HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception exception) {
380
381                ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
382                if (exceptionHandlerMethod == null) {
383                        return null;
384                }
385
386                if (this.argumentResolvers != null) {
387                        exceptionHandlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
388                }
389                if (this.returnValueHandlers != null) {
390                        exceptionHandlerMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
391                }
392
393                ServletWebRequest webRequest = new ServletWebRequest(request, response);
394                ModelAndViewContainer mavContainer = new ModelAndViewContainer();
395
396                try {
397                        if (logger.isDebugEnabled()) {
398                                logger.debug("Using @ExceptionHandler " + exceptionHandlerMethod);
399                        }
400                        Throwable cause = exception.getCause();
401                        if (cause != null) {
402                                // Expose cause as provided argument as well
403                                exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, cause, handlerMethod);
404                        }
405                        else {
406                                // Otherwise, just the given exception as-is
407                                exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, handlerMethod);
408                        }
409                }
410                catch (Throwable invocationEx) {
411                        // Any other than the original exception (or its cause) is unintended here,
412                        // probably an accident (e.g. failed assertion or the like).
413                        if (invocationEx != exception && invocationEx != exception.getCause() && logger.isWarnEnabled()) {
414                                logger.warn("Failure in @ExceptionHandler " + exceptionHandlerMethod, invocationEx);
415                        }
416                        // Continue with default processing of the original exception...
417                        return null;
418                }
419
420                if (mavContainer.isRequestHandled()) {
421                        return new ModelAndView();
422                }
423                else {
424                        ModelMap model = mavContainer.getModel();
425                        HttpStatus status = mavContainer.getStatus();
426                        ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, status);
427                        mav.setViewName(mavContainer.getViewName());
428                        if (!mavContainer.isViewReference()) {
429                                mav.setView((View) mavContainer.getView());
430                        }
431                        if (model instanceof RedirectAttributes) {
432                                Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes();
433                                RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes);
434                        }
435                        return mav;
436                }
437        }
438
439        /**
440         * Find an {@code @ExceptionHandler} method for the given exception. The default
441         * implementation searches methods in the class hierarchy of the controller first
442         * and if not found, it continues searching for additional {@code @ExceptionHandler}
443         * methods assuming some {@linkplain ControllerAdvice @ControllerAdvice}
444         * Spring-managed beans were detected.
445         * @param handlerMethod the method where the exception was raised (may be {@code null})
446         * @param exception the raised exception
447         * @return a method to handle the exception, or {@code null} if none
448         */
449        @Nullable
450        protected ServletInvocableHandlerMethod getExceptionHandlerMethod(
451                        @Nullable HandlerMethod handlerMethod, Exception exception) {
452
453                Class<?> handlerType = null;
454
455                if (handlerMethod != null) {
456                        // Local exception handler methods on the controller class itself.
457                        // To be invoked through the proxy, even in case of an interface-based proxy.
458                        handlerType = handlerMethod.getBeanType();
459                        ExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(handlerType);
460                        if (resolver == null) {
461                                resolver = new ExceptionHandlerMethodResolver(handlerType);
462                                this.exceptionHandlerCache.put(handlerType, resolver);
463                        }
464                        Method method = resolver.resolveMethod(exception);
465                        if (method != null) {
466                                return new ServletInvocableHandlerMethod(handlerMethod.getBean(), method);
467                        }
468                        // For advice applicability check below (involving base packages, assignable types
469                        // and annotation presence), use target class instead of interface-based proxy.
470                        if (Proxy.isProxyClass(handlerType)) {
471                                handlerType = AopUtils.getTargetClass(handlerMethod.getBean());
472                        }
473                }
474
475                for (Map.Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry : this.exceptionHandlerAdviceCache.entrySet()) {
476                        ControllerAdviceBean advice = entry.getKey();
477                        if (advice.isApplicableToBeanType(handlerType)) {
478                                ExceptionHandlerMethodResolver resolver = entry.getValue();
479                                Method method = resolver.resolveMethod(exception);
480                                if (method != null) {
481                                        return new ServletInvocableHandlerMethod(advice.resolveBean(), method);
482                                }
483                        }
484                }
485
486                return null;
487        }
488
489}