Open Source Repository

Home /spring/spring-web-servlet-3.0.5 | Repository Home



org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerExceptionResolver.java
/*
 * Copyright 2002-2010 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.servlet.mvc.annotation;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.Writer;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.springframework.core.ExceptionDepthComparator;
import org.springframework.core.GenericTypeResolver;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.xml.SourceHttpMessageConverter;
import org.springframework.http.converter.xml.XmlAwareFormHttpMessageConverter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.http.server.ServletServerHttpResponse;
import org.springframework.ui.Model;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.support.WebArgumentResolver;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver;
import org.springframework.web.servlet.support.RequestContextUtils;

/**
 * Implementation of the {@link org.springframework.web.servlet.HandlerExceptionResolver} interface that handles
 * exceptions through the {@link ExceptionHandler} annotation.
 *
 <p>This exception resolver is enabled by default in the {@link org.springframework.web.servlet.DispatcherServlet}.
 *
 @author Arjen Poutsma
 @author Juergen Hoeller
 @since 3.0
 */
public class AnnotationMethodHandlerExceptionResolver extends AbstractHandlerExceptionResolver {

  private WebArgumentResolver[] customArgumentResolvers;

  private HttpMessageConverter<?>[] messageConverters =
      new HttpMessageConverter[] {new ByteArrayHttpMessageConverter()new StringHttpMessageConverter(),
          new SourceHttpMessageConverter()new XmlAwareFormHttpMessageConverter()};


  /**
   * Set a custom ArgumentResolvers to use for special method parameter types.
   <p>Such a custom ArgumentResolver will kick in first, having a chance to resolve
   * an argument value before the standard argument handling kicks in.
   */
  public void setCustomArgumentResolver(WebArgumentResolver argumentResolver) {
    this.customArgumentResolvers = new WebArgumentResolver[]{argumentResolver};
  }

  /**
   * Set one or more custom ArgumentResolvers to use for special method parameter types.
   <p>Any such custom ArgumentResolver will kick in first, having a chance to resolve
   * an argument value before the standard argument handling kicks in.
   */
  public void setCustomArgumentResolvers(WebArgumentResolver[] argumentResolvers) {
    this.customArgumentResolvers = argumentResolvers;
  }

  /**
   * Set the message body converters to use.
   <p>These converters are used to convert from and to HTTP requests and responses.
   */
  public void setMessageConverters(HttpMessageConverter<?>[] messageConverters) {
    this.messageConverters = messageConverters;
  }


  @Override
  protected ModelAndView doResolveException(
      HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {

    if (handler != null) {
      Method handlerMethod = findBestExceptionHandlerMethod(handler, ex);
      if (handlerMethod != null) {
        ServletWebRequest webRequest = new ServletWebRequest(request, response);
        try {
          Object[] args = resolveHandlerArguments(handlerMethod, handler, webRequest, ex);
          if (logger.isDebugEnabled()) {
            logger.debug("Invoking request handler method: " + handlerMethod);
          }
          Object retVal = doInvokeMethod(handlerMethod, handler, args);
          return getModelAndView(handlerMethod, retVal, webRequest);
        }
        catch (Exception invocationEx) {
          logger.error("Invoking request method resulted in exception : " + handlerMethod, invocationEx);
        }
      }
    }
    return null;
  }

  /**
   * Finds the handler method that matches the thrown exception best.
   @param handler the handler object
   @param thrownException the exception to be handled
   @return the best matching method; or <code>null</code> if none is found
   */
  private Method findBestExceptionHandlerMethod(Object handler, final Exception thrownException) {
    final Class<?> handlerType = handler.getClass();
    final Class<? extends Throwable> thrownExceptionType = thrownException.getClass();
    final Map<Class<? extends Throwable>, Method> resolverMethods =
        new LinkedHashMap<Class<? extends Throwable>, Method>();

    ReflectionUtils.doWithMethods(handlerType, new ReflectionUtils.MethodCallback() {
      public void doWith(Method method) {
        method = ClassUtils.getMostSpecificMethod(method, handlerType);
        List<Class<? extends Throwable>> handledExceptions = getHandledExceptions(method);
        for (Class<? extends Throwable> handledException : handledExceptions) {
          if (handledException.isAssignableFrom(thrownExceptionType)) {
            if (!resolverMethods.containsKey(handledException)) {
              resolverMethods.put(handledException, method);
            }
            else {
              Method oldMappedMethod = resolverMethods.get(handledException);
              if (!oldMappedMethod.equals(method)) {
                throw new IllegalStateException(
                    "Ambiguous exception handler mapped for " + handledException + "]: {" +
                        oldMappedMethod + ", " + method + "}.");
              }
            }
          }
        }
      }
    });

    return getBestMatchingMethod(resolverMethods, thrownException);
  }

  /**
   * Returns all the exception classes handled by the given method.
   <p>The default implementation looks for exceptions in the {@linkplain ExceptionHandler#value() annotation},
   * or - if that annotation element is empty - any exceptions listed in the method parameters if the method
   * is annotated with {@code @ExceptionHandler}.
   @param method the method
   @return the handled exceptions
   */
  @SuppressWarnings("unchecked")
  protected List<Class<? extends Throwable>> getHandledExceptions(Method method) {
    List<Class<? extends Throwable>> result = new ArrayList<Class<? extends Throwable>>();
    ExceptionHandler exceptionHandler = AnnotationUtils.findAnnotation(method, ExceptionHandler.class);
    if (exceptionHandler != null) {
      if (!ObjectUtils.isEmpty(exceptionHandler.value())) {
        result.addAll(Arrays.asList(exceptionHandler.value()));
      }
      else {
        for (Class<?> param : method.getParameterTypes()) {
          if (Throwable.class.isAssignableFrom(param)) {
            result.add((Class<? extends Throwable>param);
          }
        }
      }
    }
    return result;
  }

  /**
   * Returns the best matching method. Uses the {@link DepthComparator}.
   */
  private Method getBestMatchingMethod(
      Map<Class<? extends Throwable>, Method> resolverMethods, Exception thrownException) {

    if (!resolverMethods.isEmpty()) {
      Class<? extends Throwable> closestMatch =
          ExceptionDepthComparator.findClosestMatch(resolverMethods.keySet(), thrownException);
      return resolverMethods.get(closestMatch);
    }
    else {
      return null;
    }
  }

  /**
   * Resolves the arguments for the given method. Delegates to {@link #resolveCommonArgument}.
   */
  private Object[] resolveHandlerArguments(Method handlerMethod, Object handler,
      NativeWebRequest webRequest, Exception thrownExceptionthrows Exception {

    Class[] paramTypes = handlerMethod.getParameterTypes();
    Object[] args = new Object[paramTypes.length];
    Class<?> handlerType = handler.getClass();
    for (int i = 0; i < args.length; i++) {
      MethodParameter methodParam = new MethodParameter(handlerMethod, i);
      GenericTypeResolver.resolveParameterType(methodParam, handlerType);
      Class paramType = methodParam.getParameterType();
      Object argValue = resolveCommonArgument(methodParam, webRequest, thrownException);
      if (argValue != WebArgumentResolver.UNRESOLVED) {
        args[i= argValue;
      }
      else {
        throw new IllegalStateException("Unsupported argument [" + paramType.getName() +
            "] for @ExceptionHandler method: " + handlerMethod);
      }
    }
    return args;
  }

  /**
   * Resolves common method arguments. Delegates to registered {@link #setCustomArgumentResolver(WebArgumentResolver)
   * argumentResolvers} first, then checking {@link #resolveStandardArgument}.
   @param methodParameter the method parameter
   @param webRequest the request
   @param thrownException the exception thrown
   @return the argument value, or {@link WebArgumentResolver#UNRESOLVED}
   */
  protected Object resolveCommonArgument(MethodParameter methodParameter, NativeWebRequest webRequest,
      Exception thrownExceptionthrows Exception {

    // Invoke custom argument resolvers if present...
    if (this.customArgumentResolvers != null) {
      for (WebArgumentResolver argumentResolver : this.customArgumentResolvers) {
        Object value = argumentResolver.resolveArgument(methodParameter, webRequest);
        if (value != WebArgumentResolver.UNRESOLVED) {
          return value;
        }
      }
    }

    // Resolution of standard parameter types...
    Class paramType = methodParameter.getParameterType();
    Object value = resolveStandardArgument(paramType, webRequest, thrownException);
    if (value != WebArgumentResolver.UNRESOLVED && !ClassUtils.isAssignableValue(paramType, value)) {
      throw new IllegalStateException(
          "Standard argument type [" + paramType.getName() "] resolved to incompatible value of type [" +
              (value != null ? value.getClass() null+
              "]. Consider declaring the argument type in a less specific fashion.");
    }
    return value;
  }

  /**
   * Resolves standard method arguments. The default implementation handles {@link NativeWebRequest},
   * {@link ServletRequest}, {@link ServletResponse}, {@link HttpSession}, {@link Principal},
   * {@link Locale}, request {@link InputStream}, request {@link Reader}, response {@link OutputStream},
   * response {@link Writer}, and the given {@code thrownException}.
   @param parameterType the method parameter type
   @param webRequest the request
   @param thrownException the exception thrown
   @return the argument value, or {@link WebArgumentResolver#UNRESOLVED}
   */
  protected Object resolveStandardArgument(Class parameterType, NativeWebRequest webRequest,
      Exception thrownExceptionthrows Exception {

    if (parameterType.isInstance(thrownException)) {
      return thrownException;
    }
    else if (WebRequest.class.isAssignableFrom(parameterType)) {
      return webRequest;
    }

    HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
    HttpServletResponse response = webRequest.getNativeResponse(HttpServletResponse.class);

    if (ServletRequest.class.isAssignableFrom(parameterType)) {
      return request;
    }
    else if (ServletResponse.class.isAssignableFrom(parameterType)) {
      return response;
    }
    else if (HttpSession.class.isAssignableFrom(parameterType)) {
      return request.getSession();
    }
    else if (Principal.class.isAssignableFrom(parameterType)) {
      return request.getUserPrincipal();
    }
    else if (Locale.class.equals(parameterType)) {
      return RequestContextUtils.getLocale(request);
    }
    else if (InputStream.class.isAssignableFrom(parameterType)) {
      return request.getInputStream();
    }
    else if (Reader.class.isAssignableFrom(parameterType)) {
      return request.getReader();
    }
    else if (OutputStream.class.isAssignableFrom(parameterType)) {
      return response.getOutputStream();
    }
    else if (Writer.class.isAssignableFrom(parameterType)) {
      return response.getWriter();
    }
    else {
      return WebArgumentResolver.UNRESOLVED;

    }
  }

  private Object doInvokeMethod(Method method, Object target, Object[] argsthrows Exception {
    ReflectionUtils.makeAccessible(method);
    try {
      return method.invoke(target, args);
    }
    catch (InvocationTargetException ex) {
      ReflectionUtils.rethrowException(ex.getTargetException());
    }
    throw new IllegalStateException("Should never get here");
  }

  @SuppressWarnings("unchecked")
  private ModelAndView getModelAndView(Method handlerMethod, Object returnValue, ServletWebRequest webRequest)
      throws Exception {

    ResponseStatus responseStatusAnn = AnnotationUtils.findAnnotation(handlerMethod, ResponseStatus.class);
    if (responseStatusAnn != null) {
      HttpStatus responseStatus = responseStatusAnn.value();
      String reason = responseStatusAnn.reason();
      if (!StringUtils.hasText(reason)) {
        webRequest.getResponse().setStatus(responseStatus.value());
      }
      else {
        webRequest.getResponse().sendError(responseStatus.value(), reason);
      }
    }

    if (returnValue != null && AnnotationUtils.findAnnotation(handlerMethod, ResponseBody.class!= null) {
      return handleResponseBody(returnValue, webRequest);
    }

    if (returnValue instanceof ModelAndView) {
      return (ModelAndViewreturnValue;
    }
    else if (returnValue instanceof Model) {
      return new ModelAndView().addAllObjects(((ModelreturnValue).asMap());
    }
    else if (returnValue instanceof Map) {
      return new ModelAndView().addAllObjects((MapreturnValue);
    }
    else if (returnValue instanceof View) {
      return new ModelAndView((ViewreturnValue);
    }
    else if (returnValue instanceof String) {
      return new ModelAndView((StringreturnValue);
    }
    else if (returnValue == null) {
      return new ModelAndView();
    }
    else {
      throw new IllegalArgumentException("Invalid handler method return value: " + returnValue);
    }
  }

  @SuppressWarnings("unchecked")
  private ModelAndView handleResponseBody(Object returnValue, ServletWebRequest webRequest)
      throws ServletException, IOException {

    HttpInputMessage inputMessage = new ServletServerHttpRequest(webRequest.getRequest());
    List<MediaType> acceptedMediaTypes = inputMessage.getHeaders().getAccept();
    if (acceptedMediaTypes.isEmpty()) {
      acceptedMediaTypes = Collections.singletonList(MediaType.ALL);
    }
    MediaType.sortByQualityValue(acceptedMediaTypes);
    HttpOutputMessage outputMessage = new ServletServerHttpResponse(webRequest.getResponse());
    Class<?> returnValueType = returnValue.getClass();
    if (this.messageConverters != null) {
      for (MediaType acceptedMediaType : acceptedMediaTypes) {
        for (HttpMessageConverter messageConverter : this.messageConverters) {
          if (messageConverter.canWrite(returnValueType, acceptedMediaType)) {
            messageConverter.write(returnValue, acceptedMediaType, outputMessage);
            return new ModelAndView();
          }
        }
      }
    }
    if (logger.isWarnEnabled()) {
      logger.warn("Could not find HttpMessageConverter that supports return type [" + returnValueType + "] and " +
          acceptedMediaTypes);
    }
    return null;
  }

}