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