001/* 002 * Copyright 2012-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 * http://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.boot.autoconfigure.web.servlet.error; 018 019import java.util.Date; 020import java.util.List; 021import java.util.Map; 022import java.util.stream.Collectors; 023 024import javax.servlet.Servlet; 025import javax.servlet.http.HttpServletRequest; 026import javax.servlet.http.HttpServletResponse; 027 028import org.apache.commons.logging.Log; 029import org.apache.commons.logging.LogFactory; 030 031import org.springframework.aop.framework.autoproxy.AutoProxyUtils; 032import org.springframework.beans.BeansException; 033import org.springframework.beans.factory.ObjectProvider; 034import org.springframework.beans.factory.config.BeanFactoryPostProcessor; 035import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; 036import org.springframework.boot.autoconfigure.AutoConfigureBefore; 037import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 038import org.springframework.boot.autoconfigure.condition.ConditionMessage; 039import org.springframework.boot.autoconfigure.condition.ConditionOutcome; 040import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; 041import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 042import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 043import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 044import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; 045import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; 046import org.springframework.boot.autoconfigure.condition.SearchStrategy; 047import org.springframework.boot.autoconfigure.condition.SpringBootCondition; 048import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider; 049import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders; 050import org.springframework.boot.autoconfigure.web.ResourceProperties; 051import org.springframework.boot.autoconfigure.web.ServerProperties; 052import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletPath; 053import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; 054import org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties; 055import org.springframework.boot.context.properties.EnableConfigurationProperties; 056import org.springframework.boot.web.server.ErrorPage; 057import org.springframework.boot.web.server.ErrorPageRegistrar; 058import org.springframework.boot.web.server.ErrorPageRegistry; 059import org.springframework.boot.web.server.WebServerFactoryCustomizer; 060import org.springframework.boot.web.servlet.error.DefaultErrorAttributes; 061import org.springframework.boot.web.servlet.error.ErrorAttributes; 062import org.springframework.boot.web.servlet.error.ErrorController; 063import org.springframework.context.ApplicationContext; 064import org.springframework.context.annotation.Bean; 065import org.springframework.context.annotation.ConditionContext; 066import org.springframework.context.annotation.Conditional; 067import org.springframework.context.annotation.Configuration; 068import org.springframework.core.Ordered; 069import org.springframework.core.type.AnnotatedTypeMetadata; 070import org.springframework.web.servlet.DispatcherServlet; 071import org.springframework.web.servlet.View; 072import org.springframework.web.servlet.view.BeanNameViewResolver; 073import org.springframework.web.util.HtmlUtils; 074 075/** 076 * {@link EnableAutoConfiguration Auto-configuration} to render errors via an MVC error 077 * controller. 078 * 079 * @author Dave Syer 080 * @author Andy Wilkinson 081 * @author Stephane Nicoll 082 * @author Brian Clozel 083 */ 084@Configuration 085@ConditionalOnWebApplication(type = Type.SERVLET) 086@ConditionalOnClass({ Servlet.class, DispatcherServlet.class }) 087// Load before the main WebMvcAutoConfiguration so that the error View is available 088@AutoConfigureBefore(WebMvcAutoConfiguration.class) 089@EnableConfigurationProperties({ ServerProperties.class, ResourceProperties.class, 090 WebMvcProperties.class }) 091public class ErrorMvcAutoConfiguration { 092 093 private final ServerProperties serverProperties; 094 095 private final DispatcherServletPath dispatcherServletPath; 096 097 private final List<ErrorViewResolver> errorViewResolvers; 098 099 public ErrorMvcAutoConfiguration(ServerProperties serverProperties, 100 DispatcherServletPath dispatcherServletPath, 101 ObjectProvider<ErrorViewResolver> errorViewResolvers) { 102 this.serverProperties = serverProperties; 103 this.dispatcherServletPath = dispatcherServletPath; 104 this.errorViewResolvers = errorViewResolvers.orderedStream() 105 .collect(Collectors.toList()); 106 } 107 108 @Bean 109 @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT) 110 public DefaultErrorAttributes errorAttributes() { 111 return new DefaultErrorAttributes( 112 this.serverProperties.getError().isIncludeException()); 113 } 114 115 @Bean 116 @ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT) 117 public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) { 118 return new BasicErrorController(errorAttributes, this.serverProperties.getError(), 119 this.errorViewResolvers); 120 } 121 122 @Bean 123 public ErrorPageCustomizer errorPageCustomizer() { 124 return new ErrorPageCustomizer(this.serverProperties, this.dispatcherServletPath); 125 } 126 127 @Bean 128 public static PreserveErrorControllerTargetClassPostProcessor preserveErrorControllerTargetClassPostProcessor() { 129 return new PreserveErrorControllerTargetClassPostProcessor(); 130 } 131 132 @Configuration 133 static class DefaultErrorViewResolverConfiguration { 134 135 private final ApplicationContext applicationContext; 136 137 private final ResourceProperties resourceProperties; 138 139 DefaultErrorViewResolverConfiguration(ApplicationContext applicationContext, 140 ResourceProperties resourceProperties) { 141 this.applicationContext = applicationContext; 142 this.resourceProperties = resourceProperties; 143 } 144 145 @Bean 146 @ConditionalOnBean(DispatcherServlet.class) 147 @ConditionalOnMissingBean 148 public DefaultErrorViewResolver conventionErrorViewResolver() { 149 return new DefaultErrorViewResolver(this.applicationContext, 150 this.resourceProperties); 151 } 152 153 } 154 155 @Configuration 156 @ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true) 157 @Conditional(ErrorTemplateMissingCondition.class) 158 protected static class WhitelabelErrorViewConfiguration { 159 160 private final StaticView defaultErrorView = new StaticView(); 161 162 @Bean(name = "error") 163 @ConditionalOnMissingBean(name = "error") 164 public View defaultErrorView() { 165 return this.defaultErrorView; 166 } 167 168 // If the user adds @EnableWebMvc then the bean name view resolver from 169 // WebMvcAutoConfiguration disappears, so add it back in to avoid disappointment. 170 @Bean 171 @ConditionalOnMissingBean 172 public BeanNameViewResolver beanNameViewResolver() { 173 BeanNameViewResolver resolver = new BeanNameViewResolver(); 174 resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10); 175 return resolver; 176 } 177 178 } 179 180 /** 181 * {@link SpringBootCondition} that matches when no error template view is detected. 182 */ 183 private static class ErrorTemplateMissingCondition extends SpringBootCondition { 184 185 @Override 186 public ConditionOutcome getMatchOutcome(ConditionContext context, 187 AnnotatedTypeMetadata metadata) { 188 ConditionMessage.Builder message = ConditionMessage 189 .forCondition("ErrorTemplate Missing"); 190 TemplateAvailabilityProviders providers = new TemplateAvailabilityProviders( 191 context.getClassLoader()); 192 TemplateAvailabilityProvider provider = providers.getProvider("error", 193 context.getEnvironment(), context.getClassLoader(), 194 context.getResourceLoader()); 195 if (provider != null) { 196 return ConditionOutcome 197 .noMatch(message.foundExactly("template from " + provider)); 198 } 199 return ConditionOutcome 200 .match(message.didNotFind("error template view").atAll()); 201 } 202 203 } 204 205 /** 206 * Simple {@link View} implementation that writes a default HTML error page. 207 */ 208 private static class StaticView implements View { 209 210 private static final Log logger = LogFactory.getLog(StaticView.class); 211 212 @Override 213 public void render(Map<String, ?> model, HttpServletRequest request, 214 HttpServletResponse response) throws Exception { 215 if (response.isCommitted()) { 216 String message = getMessage(model); 217 logger.error(message); 218 return; 219 } 220 StringBuilder builder = new StringBuilder(); 221 Date timestamp = (Date) model.get("timestamp"); 222 Object message = model.get("message"); 223 Object trace = model.get("trace"); 224 if (response.getContentType() == null) { 225 response.setContentType(getContentType()); 226 } 227 builder.append("<html><body><h1>Whitelabel Error Page</h1>").append( 228 "<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>") 229 .append("<div id='created'>").append(timestamp).append("</div>") 230 .append("<div>There was an unexpected error (type=") 231 .append(htmlEscape(model.get("error"))).append(", status=") 232 .append(htmlEscape(model.get("status"))).append(").</div>"); 233 if (message != null) { 234 builder.append("<div>").append(htmlEscape(message)).append("</div>"); 235 } 236 if (trace != null) { 237 builder.append("<div style='white-space:pre-wrap;'>") 238 .append(htmlEscape(trace)).append("</div>"); 239 } 240 builder.append("</body></html>"); 241 response.getWriter().append(builder.toString()); 242 } 243 244 private String htmlEscape(Object input) { 245 return (input != null) ? HtmlUtils.htmlEscape(input.toString()) : null; 246 } 247 248 private String getMessage(Map<String, ?> model) { 249 Object path = model.get("path"); 250 String message = "Cannot render error page for request [" + path + "]"; 251 if (model.get("message") != null) { 252 message += " and exception [" + model.get("message") + "]"; 253 } 254 message += " as the response has already been committed."; 255 message += " As a result, the response may have the wrong status code."; 256 return message; 257 } 258 259 @Override 260 public String getContentType() { 261 return "text/html"; 262 } 263 264 } 265 266 /** 267 * {@link WebServerFactoryCustomizer} that configures the server's error pages. 268 */ 269 private static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered { 270 271 private final ServerProperties properties; 272 273 private final DispatcherServletPath dispatcherServletPath; 274 275 protected ErrorPageCustomizer(ServerProperties properties, 276 DispatcherServletPath dispatcherServletPath) { 277 this.properties = properties; 278 this.dispatcherServletPath = dispatcherServletPath; 279 } 280 281 @Override 282 public void registerErrorPages(ErrorPageRegistry errorPageRegistry) { 283 ErrorPage errorPage = new ErrorPage(this.dispatcherServletPath 284 .getRelativePath(this.properties.getError().getPath())); 285 errorPageRegistry.addErrorPages(errorPage); 286 } 287 288 @Override 289 public int getOrder() { 290 return 0; 291 } 292 293 } 294 295 /** 296 * {@link BeanFactoryPostProcessor} to ensure that the target class of ErrorController 297 * MVC beans are preserved when using AOP. 298 */ 299 static class PreserveErrorControllerTargetClassPostProcessor 300 implements BeanFactoryPostProcessor { 301 302 @Override 303 public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) 304 throws BeansException { 305 String[] errorControllerBeans = beanFactory 306 .getBeanNamesForType(ErrorController.class, false, false); 307 for (String errorControllerBean : errorControllerBeans) { 308 try { 309 beanFactory.getBeanDefinition(errorControllerBean).setAttribute( 310 AutoProxyUtils.PRESERVE_TARGET_CLASS_ATTRIBUTE, Boolean.TRUE); 311 } 312 catch (Throwable ex) { 313 // Ignore 314 } 315 } 316 } 317 318 } 319 320}