From d5f095b05c68b3cf143138faf6a084298cf9de36 Mon Sep 17 00:00:00 2001 From: Kai Moritz Date: Mon, 20 Jun 2016 17:33:15 +0200 Subject: [PATCH] Resolving of ServletExceptions via the configured HandlerExceptionResolvers Implemented the class ExceptionResolverErrorController, tries to resolve the root-cause of a ServletException via the configured HandlerExceptionResolvers. To do so, the class mimics the initialization of the configured HandlerExceptionResolvers, that the DispatcherServlet executes, because, unfortunatly, the DispatcherServlet does not make the configured resolvers available. --- .../java/de/juplo/thymeproxy/Application.java | 85 ++--- .../ExceptionResolverErrorController.java | 296 ++++++++++++++++++ 2 files changed, 319 insertions(+), 62 deletions(-) create mode 100644 src/main/java/de/juplo/thymeproxy/ExceptionResolverErrorController.java diff --git a/src/main/java/de/juplo/thymeproxy/Application.java b/src/main/java/de/juplo/thymeproxy/Application.java index 1c33ce2..9311cf8 100644 --- a/src/main/java/de/juplo/thymeproxy/Application.java +++ b/src/main/java/de/juplo/thymeproxy/Application.java @@ -2,33 +2,25 @@ package de.juplo.thymeproxy; import de.juplo.thymeleaf.JuploDialect; import java.util.HashMap; -import java.util.Locale; import java.util.Map; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import org.apache.http.HttpStatus; +import java.util.Properties; import org.apache.http.impl.client.CloseableHttpClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.autoconfigure.web.BasicErrorController; import org.springframework.boot.autoconfigure.web.ErrorAttributes; import org.springframework.boot.autoconfigure.web.ErrorController; -import org.springframework.boot.autoconfigure.web.ErrorProperties; import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.core.Ordered; import org.springframework.core.env.Environment; -import org.springframework.http.MediaType; -import org.springframework.web.servlet.ModelAndView; -import org.springframework.web.servlet.View; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; +import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver; import org.springframework.web.servlet.mvc.UrlFilenameViewController; -import org.thymeleaf.exceptions.TemplateInputException; import org.thymeleaf.resourceresolver.IResourceResolver; import org.thymeleaf.resourceresolver.UrlResourceResolver; -import org.thymeleaf.spring4.view.ThymeleafViewResolver; import org.thymeleaf.templateresolver.TemplateResolver; @@ -121,15 +113,31 @@ public class Application extends WebMvcConfigurerAdapter } @Bean - public View error(ThymeleafViewResolver resolver) throws Exception + public SimpleMappingExceptionResolver simpleMappingExceptionResolver() { - return resolver.resolveViewName("templates/500", Locale.getDefault()); + SimpleMappingExceptionResolver resolver = + new SimpleMappingExceptionResolver(); + + Properties mappings = new Properties(); + mappings.setProperty("TemplateInputException", "templates/404"); + + resolver.setExceptionMappings(mappings); + resolver.setDefaultErrorView("templates/500"); + resolver.setWarnLogCategory("exception"); + return resolver; } @Bean - public ErrorController errorController(ErrorAttributes errorAttributes) + public ErrorController errorController( + ApplicationContext context, + ErrorAttributes errorAttributes + ) { - return new CustomErrorController(errorAttributes, properties.getError()); + return new ExceptionResolverErrorController( + context, + errorAttributes, + properties.getError() + ); } @@ -144,51 +152,4 @@ public class Application extends WebMvcConfigurerAdapter { SpringApplication.run(Application.class, args); } - - - static class CustomErrorController extends BasicErrorController - { - public final static String TEMPLATE_INPUT_EXCEPTION = - TemplateInputException.class.getCanonicalName(); - - - CustomErrorController( - ErrorAttributes errorAttributes, - ErrorProperties errorProperties - ) - { - super(errorAttributes, errorProperties); - } - - - @Override - public ModelAndView errorHtml( - HttpServletRequest request, - HttpServletResponse response - ) - { - Map model = - getErrorAttributes( - request, - isIncludeStackTrace(request, MediaType.TEXT_HTML) - ); - - String view; - - switch ((String)model.get("exception")) - { - case "org.thymeleaf.exceptions.TemplateInputException": - response.setStatus(HttpStatus.SC_NOT_FOUND); - view = "templates/404"; - break; - - default: - response.setStatus(getStatus(request).value()); - view = "templates/500"; - break; - } - - return new ModelAndView(view, model); - } - } } \ No newline at end of file diff --git a/src/main/java/de/juplo/thymeproxy/ExceptionResolverErrorController.java b/src/main/java/de/juplo/thymeproxy/ExceptionResolverErrorController.java new file mode 100644 index 0000000..6315100 --- /dev/null +++ b/src/main/java/de/juplo/thymeproxy/ExceptionResolverErrorController.java @@ -0,0 +1,296 @@ +package de.juplo.thymeproxy; + + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.BeanInitializationException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.boot.autoconfigure.web.BasicErrorController; +import org.springframework.boot.autoconfigure.web.ErrorAttributes; +import org.springframework.boot.autoconfigure.web.ErrorProperties; +import org.springframework.context.ApplicationContext; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.support.PropertiesLoaderUtils; +import org.springframework.http.MediaType; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.servlet.DispatcherServlet; +import static org.springframework.web.servlet.DispatcherServlet.HANDLER_EXCEPTION_RESOLVER_BEAN_NAME; +import org.springframework.web.servlet.HandlerExceptionResolver; +import org.springframework.web.servlet.ModelAndView; + + + +/** + * + * @author kai + */ +public class ExceptionResolverErrorController extends BasicErrorController +{ + private final static Logger LOG = + LoggerFactory.getLogger(ExceptionResolverErrorController.class); + + public final static String EXCEPTION_ATTRIBUTE = + "javax.servlet.error.exception"; + + + /** + * Name of the class path resource (relative to the DispatcherServlet class) + * that defines DispatcherServlet's default strategy names. + */ + private static final String DEFAULT_STRATEGIES_PATH = + "DispatcherServlet.properties"; + private static final Properties DEFAULT_STRATEGIES; + + static + { + // Load default strategy implementations from properties file. + // This is currently strictly internal and not meant to be customized + // by application developers. + try + { + ClassPathResource resource = + new ClassPathResource( + DEFAULT_STRATEGIES_PATH, + DispatcherServlet.class + ); + DEFAULT_STRATEGIES = PropertiesLoaderUtils.loadProperties(resource); + } + catch (IOException ex) + { + throw new IllegalStateException( + "Could not load 'DispatcherServlet.properties': " + + ex.getMessage() + ); + } + } + + + /** List of HandlerExceptionResolvers used by this servlet */ + private List handlerExceptionResolvers; + + /** Detect all HandlerExceptionResolvers or just expect "handlerExceptionResolver" bean? */ + private boolean detectAllHandlerExceptionResolvers = true; + + + ExceptionResolverErrorController( + ApplicationContext context, + ErrorAttributes errorAttributes, + ErrorProperties errorProperties + ) + { + super(errorAttributes, errorProperties); + initHandlerExceptionResolvers(context); + } + + + @Override + public ModelAndView errorHtml( + HttpServletRequest request, + HttpServletResponse response + ) + { + Map model = + getErrorAttributes( + request, + isIncludeStackTrace(request, MediaType.TEXT_HTML) + ); + + ModelAndView view = null; + + Exception e = (Exception)request.getAttribute(EXCEPTION_ATTRIBUTE); + if (e != null) + { + if (e instanceof ServletException ) + { + ServletException n = (ServletException)e; + Throwable t = n.getRootCause(); + if (t != null && t instanceof Exception) + e = (Exception)t; + } + + for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) + { + view = resolver.resolveException(request, response, null, e); + if (view != null) + break; + } + + if (view != null) + { + view.addAllObjects(model); + return view; + } + } + + return new ModelAndView("error", model); + } + + + /** + * Initialize the HandlerExceptionResolver used by this class. + *

+ * If no bean is defined with the given name in the BeanFactory for this + * namespace, + * we default to no exception resolver. + */ + private void initHandlerExceptionResolvers(ApplicationContext context) + { + this.handlerExceptionResolvers = null; + + if (this.detectAllHandlerExceptionResolvers) + { + // Find all HandlerExceptionResolvers in the ApplicationContext, including ancestor contexts. + Map matchingBeans = + BeanFactoryUtils.beansOfTypeIncludingAncestors( + context, + HandlerExceptionResolver.class, + true, + false + ); + if (!matchingBeans.isEmpty()) + { + this.handlerExceptionResolvers = new ArrayList<>(matchingBeans.values()); + // We keep HandlerExceptionResolvers in sorted order. + AnnotationAwareOrderComparator.sort(this.handlerExceptionResolvers); + } + } + else + { + try + { + HandlerExceptionResolver her = + context.getBean( + HANDLER_EXCEPTION_RESOLVER_BEAN_NAME, + HandlerExceptionResolver.class + ); + this.handlerExceptionResolvers = Collections.singletonList(her); + } + catch (NoSuchBeanDefinitionException e) + { + // Ignore, no HandlerExceptionResolver is fine too. + } + } + + // Ensure we have at least some HandlerExceptionResolvers, by registering + // default HandlerExceptionResolvers if no other resolvers are found. + if (this.handlerExceptionResolvers == null) + { + this.handlerExceptionResolvers = + getDefaultStrategies(context, HandlerExceptionResolver.class); + LOG.debug("No HandlerExceptionResolvers found: using default"); + } + } + + /** + * Create a List of default strategy objects for the given strategy interface. + *

+ * The default implementation uses the "DispatcherServlet.properties" file (in + * the same + * package as the DispatcherServlet class) to determine the class names. It + * instantiates + * the strategy objects through the context's BeanFactory. + * + * @param context the current WebApplicationContext + * @param strategyInterface the strategy interface + * @return the List of corresponding strategy objects + */ + @SuppressWarnings("unchecked") + protected List getDefaultStrategies( + ApplicationContext context, + Class strategyInterface + ) + { + String key = strategyInterface.getName(); + String value = DEFAULT_STRATEGIES.getProperty(key); + if (value != null) + { + String[] classNames = StringUtils.commaDelimitedListToStringArray(value); + List strategies = new ArrayList<>(classNames.length); + for (String className : classNames) + { + try + { + Class clazz = + ClassUtils.forName( + className, + DispatcherServlet.class.getClassLoader() + ); + Object strategy = createDefaultStrategy(context, clazz); + strategies.add((T) strategy); + } + catch (ClassNotFoundException ex) + { + throw new BeanInitializationException( + "Could not find DispatcherServlet's default strategy class [" + + className + "] for interface [" + key + "]", + ex + ); + } + catch (LinkageError err) + { + throw new BeanInitializationException( + "Error loading DispatcherServlet's default strategy class [" + + className + "] for interface [" + key + + "]: problem with class file or dependent class", + err + ); + } + } + return strategies; + } + else + { + return new LinkedList<>(); + } + } + + /** + * Create a default strategy. + *

+ * The default implementation uses + * {@link org.springframework.beans.factory.config.AutowireCapableBeanFactory#createBean}. + * + * @param context the current WebApplicationContext + * @param clazz the strategy implementation class to instantiate + * @return the fully configured strategy instance + * @see + * org.springframework.context.ApplicationContext#getAutowireCapableBeanFactory() + * @see + * org.springframework.beans.factory.config.AutowireCapableBeanFactory#createBean + */ + protected Object createDefaultStrategy( + ApplicationContext context, + Class clazz + ) + { + return context.getAutowireCapableBeanFactory().createBean(clazz); + } + + + /** + * Set whether to detect all HandlerExceptionResolver beans in this servlet's + * context. Otherwise, + * just a single bean with name "handlerExceptionResolver" will be expected. + *

+ * Default is "true". Turn this off if you want this servlet to use a single + * HandlerExceptionResolver, despite multiple HandlerExceptionResolver beans + * being defined in the context. + */ + public void setDetectAllHandlerExceptionResolvers(boolean resolvers) + { + detectAllHandlerExceptionResolvers = resolvers; + } +} -- 2.20.1