Add out-of-the-box support for status error pages
Allow convention based status error pages. Static HTML or templates can be used by placing the appropriately named file under a `/error` folder. For example: /src/main/resource/templates/error/404.ftl or /src/main/resource/public/error/404.html Pages can also be named after the status series (5xx or 4xx). Fixes gh-2691
This commit is contained in:
parent
0bd246a3ed
commit
0bf025af7b
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2012-2014 the original author or authors.
|
||||
* Copyright 2012-2016 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.
|
||||
|
|
@ -16,15 +16,20 @@
|
|||
|
||||
package org.springframework.boot.autoconfigure.web;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.context.request.RequestAttributes;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
/**
|
||||
* Abstract base class for error {@link Controller} implementations.
|
||||
|
|
@ -38,9 +43,27 @@ public abstract class AbstractErrorController implements ErrorController {
|
|||
|
||||
private final ErrorAttributes errorAttributes;
|
||||
|
||||
private final List<ErrorViewResolver> errorViewResolvers;
|
||||
|
||||
public AbstractErrorController(ErrorAttributes errorAttributes) {
|
||||
this(errorAttributes, null);
|
||||
}
|
||||
|
||||
public AbstractErrorController(ErrorAttributes errorAttributes,
|
||||
List<ErrorViewResolver> errorViewResolvers) {
|
||||
Assert.notNull(errorAttributes, "ErrorAttributes must not be null");
|
||||
this.errorAttributes = errorAttributes;
|
||||
this.errorViewResolvers = sortErrorViewResolvers(errorViewResolvers);
|
||||
}
|
||||
|
||||
private List<ErrorViewResolver> sortErrorViewResolvers(
|
||||
List<ErrorViewResolver> resolvers) {
|
||||
List<ErrorViewResolver> sorted = new ArrayList<ErrorViewResolver>();
|
||||
if (resolvers != null) {
|
||||
sorted.addAll(resolvers);
|
||||
AnnotationAwareOrderComparator.sortIfNecessary(sorted);
|
||||
}
|
||||
return sorted;
|
||||
}
|
||||
|
||||
protected Map<String, Object> getErrorAttributes(HttpServletRequest request,
|
||||
|
|
@ -72,4 +95,26 @@ public abstract class AbstractErrorController implements ErrorController {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve any specific error views. By default this method delegates to
|
||||
* {@link ErrorViewResolver ErrorViewResolvers}.
|
||||
* @param request the request
|
||||
* @param response the response
|
||||
* @param status the HTTP status
|
||||
* @param model the suggested model
|
||||
* @return a specific {@link ModelAndView} or {@code null} if the default should be
|
||||
* used
|
||||
* @since 1.4.0
|
||||
*/
|
||||
protected ModelAndView resolveErrorView(HttpServletRequest request,
|
||||
HttpServletResponse response, HttpStatus status, Map<String, Object> model) {
|
||||
for (ErrorViewResolver resolver : this.errorViewResolvers) {
|
||||
ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
|
||||
if (modelAndView != null) {
|
||||
return modelAndView;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@
|
|||
|
||||
package org.springframework.boot.autoconfigure.web;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
|
@ -58,7 +60,19 @@ public class BasicErrorController extends AbstractErrorController {
|
|||
*/
|
||||
public BasicErrorController(ErrorAttributes errorAttributes,
|
||||
ErrorProperties errorProperties) {
|
||||
super(errorAttributes);
|
||||
this(errorAttributes, errorProperties,
|
||||
Collections.<ErrorViewResolver>emptyList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link BasicErrorController} instance.
|
||||
* @param errorAttributes the error attributes
|
||||
* @param errorProperties configuration properties
|
||||
* @param errorViewResolvers error view resolvers
|
||||
*/
|
||||
public BasicErrorController(ErrorAttributes errorAttributes,
|
||||
ErrorProperties errorProperties, List<ErrorViewResolver> errorViewResolvers) {
|
||||
super(errorAttributes, errorViewResolvers);
|
||||
Assert.notNull(errorProperties, "ErrorProperties must not be null");
|
||||
this.errorProperties = errorProperties;
|
||||
}
|
||||
|
|
@ -71,10 +85,12 @@ public class BasicErrorController extends AbstractErrorController {
|
|||
@RequestMapping(produces = "text/html")
|
||||
public ModelAndView errorHtml(HttpServletRequest request,
|
||||
HttpServletResponse response) {
|
||||
response.setStatus(getStatus(request).value());
|
||||
Map<String, Object> model = getErrorAttributes(request,
|
||||
isIncludeStackTrace(request, MediaType.TEXT_HTML));
|
||||
return new ModelAndView("error", model);
|
||||
HttpStatus status = getStatus(request);
|
||||
Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
|
||||
request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
|
||||
response.setStatus(status.value());
|
||||
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
|
||||
return (modelAndView == null ? new ModelAndView("error", model) : modelAndView);
|
||||
}
|
||||
|
||||
@RequestMapping
|
||||
|
|
|
|||
|
|
@ -0,0 +1,180 @@
|
|||
/*
|
||||
* Copyright 2012-2016 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.boot.autoconfigure.web;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.support.SpringFactoriesLoader;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.HttpStatus.Series;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
import org.springframework.web.servlet.View;
|
||||
|
||||
/**
|
||||
* Default {@link ErrorViewResolver} implementation that attempts to resolve error views
|
||||
* using well known conventions. Will search for templates and static assets under
|
||||
* {@code '/error'} using the {@link HttpStatus status code} and the
|
||||
* {@link HttpStatus#series() status series}.
|
||||
* <p>
|
||||
* For example, an {@code HTTP 404} will search (in the specific order):
|
||||
* <ul>
|
||||
* <li>{@code '/<templates>/error/404.<ext>'}</li>
|
||||
* <li>{@code '/<static>/error/404.html'}</li>
|
||||
* <li>{@code '/<templates>/error/4xx.<ext>'}</li>
|
||||
* <li>{@code '/<static>/error/4xx.html'}</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 1.4.0
|
||||
*/
|
||||
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
|
||||
|
||||
private static final Map<Series, String> SERIES_VIEWS;
|
||||
|
||||
static {
|
||||
Map<Series, String> views = new HashMap<Series, String>();
|
||||
views.put(Series.CLIENT_ERROR, "4xx");
|
||||
views.put(Series.SERVER_ERROR, "5xx");
|
||||
SERIES_VIEWS = Collections.unmodifiableMap(views);
|
||||
}
|
||||
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
private final ResourceProperties resourceProperties;
|
||||
|
||||
private final List<TemplateAvailabilityProvider> templateAvailabilityProviders;
|
||||
|
||||
private int order = Ordered.LOWEST_PRECEDENCE;
|
||||
|
||||
/**
|
||||
* Create a new {@link DefaultErrorViewResolver} instance.
|
||||
* @param applicationContext the source application context
|
||||
* @param resourceProperties resource properties
|
||||
*/
|
||||
public DefaultErrorViewResolver(ApplicationContext applicationContext,
|
||||
ResourceProperties resourceProperties) {
|
||||
this(applicationContext, resourceProperties,
|
||||
loadTemplateAvailabilityProviders(applicationContext));
|
||||
}
|
||||
|
||||
private static List<TemplateAvailabilityProvider> loadTemplateAvailabilityProviders(
|
||||
ApplicationContext applicationContext) {
|
||||
return SpringFactoriesLoader.loadFactories(TemplateAvailabilityProvider.class,
|
||||
applicationContext == null ? null : applicationContext.getClassLoader());
|
||||
}
|
||||
|
||||
DefaultErrorViewResolver(ApplicationContext applicationContext,
|
||||
ResourceProperties resourceProperties,
|
||||
List<TemplateAvailabilityProvider> templateAvailabilityProviders) {
|
||||
Assert.notNull(applicationContext, "ApplicationContext must not be null");
|
||||
Assert.notNull(resourceProperties, "ResourceProperties must not be null");
|
||||
this.applicationContext = applicationContext;
|
||||
this.resourceProperties = resourceProperties;
|
||||
this.templateAvailabilityProviders = templateAvailabilityProviders;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
|
||||
Map<String, Object> model) {
|
||||
ModelAndView modelAndView = resolve(String.valueOf(status), model);
|
||||
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
|
||||
modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
|
||||
}
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
private ModelAndView resolve(String viewName, Map<String, Object> model) {
|
||||
ModelAndView modelAndView = resolveTemplate(viewName, model);
|
||||
if (modelAndView == null) {
|
||||
modelAndView = resolveResource(viewName, model);
|
||||
}
|
||||
return modelAndView;
|
||||
}
|
||||
|
||||
private ModelAndView resolveTemplate(String viewName, Map<String, Object> model) {
|
||||
for (TemplateAvailabilityProvider templateAvailabilityProvider : this.templateAvailabilityProviders) {
|
||||
if (templateAvailabilityProvider.isTemplateAvailable("error/" + viewName,
|
||||
this.applicationContext.getEnvironment(),
|
||||
this.applicationContext.getClassLoader(), this.applicationContext)) {
|
||||
return new ModelAndView("error/" + viewName, model);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
|
||||
for (String location : this.resourceProperties.getStaticLocations()) {
|
||||
try {
|
||||
Resource resource = this.applicationContext.getResource(location);
|
||||
resource = resource.createRelative("error/" + viewName + ".html");
|
||||
if (resource.exists()) {
|
||||
return new ModelAndView(new HtmlResourceView(resource), model);
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return this.order;
|
||||
}
|
||||
|
||||
public void setOrder(int order) {
|
||||
this.order = order;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link View} backed by a HTML resource.
|
||||
*/
|
||||
private static class HtmlResourceView implements View {
|
||||
|
||||
private Resource resource;
|
||||
|
||||
HtmlResourceView(Resource resource) {
|
||||
this.resource = resource;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getContentType() {
|
||||
return MediaType.TEXT_HTML_VALUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(Map<String, ?> model, HttpServletRequest request,
|
||||
HttpServletResponse response) throws Exception {
|
||||
FileCopyUtils.copy(this.resource.getInputStream(),
|
||||
response.getOutputStream());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -27,11 +27,13 @@ import javax.servlet.http.HttpServletResponse;
|
|||
|
||||
import org.springframework.aop.framework.autoproxy.AutoProxyUtils;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
|
|
@ -40,9 +42,11 @@ import org.springframework.boot.autoconfigure.condition.SearchStrategy;
|
|||
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
|
||||
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider;
|
||||
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.web.servlet.ErrorPage;
|
||||
import org.springframework.boot.web.servlet.ErrorPageRegistrar;
|
||||
import org.springframework.boot.web.servlet.ErrorPageRegistry;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ConditionContext;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
|
|
@ -69,18 +73,28 @@ import org.springframework.web.util.HtmlUtils;
|
|||
* @author Andy Wilkinson
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
|
||||
@ConditionalOnWebApplication
|
||||
// Ensure this loads before the main WebMvcAutoConfiguration so that the error View is
|
||||
// available
|
||||
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
|
||||
@Configuration
|
||||
@ConditionalOnWebApplication
|
||||
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
|
||||
// Load before the main WebMvcAutoConfiguration so that the error View is available
|
||||
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
|
||||
@EnableConfigurationProperties(ResourceProperties.class)
|
||||
public class ErrorMvcAutoConfiguration {
|
||||
|
||||
private final ServerProperties properties;
|
||||
private final ApplicationContext applicationContext;
|
||||
|
||||
public ErrorMvcAutoConfiguration(ServerProperties properties) {
|
||||
this.properties = properties;
|
||||
private final ServerProperties serverProperties;
|
||||
|
||||
private final ResourceProperties resourceProperties;
|
||||
|
||||
@Autowired(required = false)
|
||||
private List<ErrorViewResolver> errorViewResolvers;
|
||||
|
||||
public ErrorMvcAutoConfiguration(ApplicationContext applicationContext,
|
||||
ServerProperties serverProperties, ResourceProperties resourceProperties) {
|
||||
this.applicationContext = applicationContext;
|
||||
this.serverProperties = serverProperties;
|
||||
this.resourceProperties = resourceProperties;
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
|
@ -92,12 +106,21 @@ public class ErrorMvcAutoConfiguration {
|
|||
@Bean
|
||||
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
|
||||
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) {
|
||||
return new BasicErrorController(errorAttributes, this.properties.getError());
|
||||
return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
|
||||
this.errorViewResolvers);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ErrorPageCustomizer errorPageCustomizer() {
|
||||
return new ErrorPageCustomizer(this.properties);
|
||||
return new ErrorPageCustomizer(this.serverProperties);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBean(DispatcherServlet.class)
|
||||
@ConditionalOnMissingBean
|
||||
public DefaultErrorViewResolver conventionErrorViewResolver() {
|
||||
return new DefaultErrorViewResolver(this.applicationContext,
|
||||
this.resourceProperties);
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright 2012-2016 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.boot.autoconfigure.web;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
/**
|
||||
* Interface that can be implemented by beans that resolve error views.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 1.4.0
|
||||
*/
|
||||
public interface ErrorViewResolver {
|
||||
|
||||
/**
|
||||
* Resolve an error view for the specified details.
|
||||
* @param request the source request
|
||||
* @param status the http status of the error
|
||||
* @param model the suggested model to be used with the view
|
||||
* @return a resolved {@link ModelAndView} or {@code null}
|
||||
*/
|
||||
ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
|
||||
Map<String, Object> model);
|
||||
|
||||
}
|
||||
|
|
@ -32,6 +32,7 @@ import org.junit.Test;
|
|||
import org.junit.runner.RunWith;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.web.BasicErrorControllerMockMvcTests.MinimalWebConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
||||
|
|
@ -39,6 +40,7 @@ import org.springframework.boot.test.web.client.TestRestTemplate;
|
|||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.RequestEntity;
|
||||
|
|
@ -173,6 +175,17 @@ public class BasicErrorControllerIntegrationTests {
|
|||
assertThat(resp).contains(MethodArgumentNotValidException.class.getName());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConventionTemplateMapping() throws Exception {
|
||||
load();
|
||||
RequestEntity<?> request = RequestEntity.get(URI.create(createUrl("/noStorage")))
|
||||
.accept(MediaType.TEXT_HTML).build();
|
||||
ResponseEntity<String> entity = new TestRestTemplate().exchange(request,
|
||||
String.class);
|
||||
String resp = entity.getBody().toString();
|
||||
assertThat(resp).contains("We are out of storage");
|
||||
}
|
||||
|
||||
private void assertErrorAttributes(Map<?, ?> content, String status, String error,
|
||||
Class<?> exception, String message, String path) {
|
||||
assertThat(content.get("status")).as("Wrong status").isEqualTo(status);
|
||||
|
|
@ -201,6 +214,7 @@ public class BasicErrorControllerIntegrationTests {
|
|||
|
||||
@Configuration
|
||||
@MinimalWebConfiguration
|
||||
@Import(FreeMarkerAutoConfiguration.class)
|
||||
public static class TestConfiguration {
|
||||
|
||||
// For manual testing
|
||||
|
|
@ -254,12 +268,22 @@ public class BasicErrorControllerIntegrationTests {
|
|||
return body.content;
|
||||
}
|
||||
|
||||
@RequestMapping(path = "/noStorage")
|
||||
public String noStorage() {
|
||||
throw new InsufficientStorageException();
|
||||
}
|
||||
|
||||
@ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "Expected!")
|
||||
@SuppressWarnings("serial")
|
||||
private static class ExpectedException extends RuntimeException {
|
||||
|
||||
}
|
||||
|
||||
@ResponseStatus(HttpStatus.INSUFFICIENT_STORAGE)
|
||||
private static class InsufficientStorageException extends RuntimeException {
|
||||
|
||||
}
|
||||
|
||||
@ResponseStatus(HttpStatus.NOT_ACCEPTABLE)
|
||||
@SuppressWarnings("serial")
|
||||
private static class NoReasonExpectedException extends RuntimeException {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,214 @@
|
|||
/*
|
||||
* Copyright 2012-2016 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.boot.autoconfigure.web;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.mock.web.MockHttpServletResponse;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
|
||||
/**
|
||||
* Tests for {@link DefaultErrorViewResolver}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
public class DefaultErrorViewResolverTests {
|
||||
|
||||
@Rule
|
||||
public ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
private DefaultErrorViewResolver resolver;
|
||||
|
||||
@Mock
|
||||
private TemplateAvailabilityProvider templateAvailabilityProvider;
|
||||
|
||||
private ResourceProperties resourceProperties;
|
||||
|
||||
private Map<String, Object> model = new HashMap<String, Object>();
|
||||
|
||||
private HttpServletRequest request = new MockHttpServletRequest();
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
|
||||
applicationContext.refresh();
|
||||
this.resourceProperties = new ResourceProperties();
|
||||
List<TemplateAvailabilityProvider> templateAvailabilityProviders = Collections
|
||||
.singletonList(this.templateAvailabilityProvider);
|
||||
this.resolver = new DefaultErrorViewResolver(applicationContext,
|
||||
this.resourceProperties, templateAvailabilityProviders);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createWhenApplicationContextIsNullShouldThrowException()
|
||||
throws Exception {
|
||||
this.thrown.expect(IllegalArgumentException.class);
|
||||
this.thrown.expectMessage("ApplicationContext must not be null");
|
||||
new DefaultErrorViewResolver(null, new ResourceProperties());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createWhenResourcePropertiesIsNullShouldThrowException()
|
||||
throws Exception {
|
||||
this.thrown.expect(IllegalArgumentException.class);
|
||||
this.thrown.expectMessage("ResourceProperties must not be null");
|
||||
new DefaultErrorViewResolver(mock(ApplicationContext.class), null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveWhenNoMatchShouldReturnNull() throws Exception {
|
||||
ModelAndView resolved = this.resolver.resolveErrorView(this.request,
|
||||
HttpStatus.NOT_FOUND, this.model);
|
||||
assertThat(resolved).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveWhenExactTemplateMatchShouldReturnTemplate() throws Exception {
|
||||
given(this.templateAvailabilityProvider.isTemplateAvailable(eq("error/404"),
|
||||
any(Environment.class), any(ClassLoader.class),
|
||||
any(ResourceLoader.class))).willReturn(true);
|
||||
ModelAndView resolved = this.resolver.resolveErrorView(this.request,
|
||||
HttpStatus.NOT_FOUND, this.model);
|
||||
assertThat(resolved).isNotNull();
|
||||
assertThat(resolved.getViewName()).isEqualTo("error/404");
|
||||
verify(this.templateAvailabilityProvider).isTemplateAvailable(eq("error/404"),
|
||||
any(Environment.class), any(ClassLoader.class),
|
||||
any(ResourceLoader.class));
|
||||
verifyNoMoreInteractions(this.templateAvailabilityProvider);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveWhenSeries5xxTemplateMatchShouldReturnTemplate() throws Exception {
|
||||
given(this.templateAvailabilityProvider.isTemplateAvailable(eq("error/5xx"),
|
||||
any(Environment.class), any(ClassLoader.class),
|
||||
any(ResourceLoader.class))).willReturn(true);
|
||||
ModelAndView resolved = this.resolver.resolveErrorView(this.request,
|
||||
HttpStatus.SERVICE_UNAVAILABLE, this.model);
|
||||
assertThat(resolved.getViewName()).isEqualTo("error/5xx");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveWhenSeries4xxTemplateMatchShouldReturnTemplate() throws Exception {
|
||||
given(this.templateAvailabilityProvider.isTemplateAvailable(eq("error/4xx"),
|
||||
any(Environment.class), any(ClassLoader.class),
|
||||
any(ResourceLoader.class))).willReturn(true);
|
||||
ModelAndView resolved = this.resolver.resolveErrorView(this.request,
|
||||
HttpStatus.NOT_FOUND, this.model);
|
||||
assertThat(resolved.getViewName()).isEqualTo("error/4xx");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveWhenExactResourceMatchShouldReturnResource() throws Exception {
|
||||
setResourceLocation("/exact");
|
||||
ModelAndView resolved = this.resolver.resolveErrorView(this.request,
|
||||
HttpStatus.NOT_FOUND, this.model);
|
||||
assertThat(render(resolved)).isEqualTo("exact/404");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveWhenSeries4xxResourceMatchShouldReturnResource() throws Exception {
|
||||
setResourceLocation("/4xx");
|
||||
ModelAndView resolved = this.resolver.resolveErrorView(this.request,
|
||||
HttpStatus.NOT_FOUND, this.model);
|
||||
assertThat(render(resolved)).isEqualTo("4xx/4xx");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveWhenSeries5xxResourceMatchShouldReturnResource() throws Exception {
|
||||
setResourceLocation("/5xx");
|
||||
ModelAndView resolved = this.resolver.resolveErrorView(this.request,
|
||||
HttpStatus.INTERNAL_SERVER_ERROR, this.model);
|
||||
assertThat(render(resolved)).isEqualTo("5xx/5xx");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveWhenTemplateAndResourceMatchShouldFavorTemplate()
|
||||
throws Exception {
|
||||
setResourceLocation("/exact");
|
||||
given(this.templateAvailabilityProvider.isTemplateAvailable(eq("error/404"),
|
||||
any(Environment.class), any(ClassLoader.class),
|
||||
any(ResourceLoader.class))).willReturn(true);
|
||||
ModelAndView resolved = this.resolver.resolveErrorView(this.request,
|
||||
HttpStatus.NOT_FOUND, this.model);
|
||||
assertThat(resolved.getViewName()).isEqualTo("error/404");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void resolveWhenExactResourceMatchAndSeriesTemplateMatchShouldFavorResource()
|
||||
throws Exception {
|
||||
setResourceLocation("/exact");
|
||||
given(this.templateAvailabilityProvider.isTemplateAvailable(eq("error/4xx"),
|
||||
any(Environment.class), any(ClassLoader.class),
|
||||
any(ResourceLoader.class))).willReturn(true);
|
||||
ModelAndView resolved = this.resolver.resolveErrorView(this.request,
|
||||
HttpStatus.NOT_FOUND, this.model);
|
||||
assertThat(render(resolved)).isEqualTo("exact/404");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void orderShouldBeLowest() throws Exception {
|
||||
assertThat(this.resolver.getOrder()).isEqualTo(Ordered.LOWEST_PRECEDENCE);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void setOrderShouldChangeOrder() throws Exception {
|
||||
this.resolver.setOrder(123);
|
||||
assertThat(this.resolver.getOrder()).isEqualTo(123);
|
||||
}
|
||||
|
||||
private void setResourceLocation(String path) {
|
||||
String packageName = getClass().getPackage().getName();
|
||||
this.resourceProperties.setStaticLocations(new String[] {
|
||||
"classpath:" + packageName.replace(".", "/") + path + "/" });
|
||||
}
|
||||
|
||||
private String render(ModelAndView modelAndView) throws Exception {
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
modelAndView.getView().render(this.model, this.request, response);
|
||||
return response.getContentAsString().trim();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
4xx/402
|
||||
|
|
@ -0,0 +1 @@
|
|||
4xx/4xx
|
||||
|
|
@ -0,0 +1 @@
|
|||
5xx/4xx
|
||||
|
|
@ -0,0 +1 @@
|
|||
5xx/5xx
|
||||
|
|
@ -0,0 +1 @@
|
|||
exact/404
|
||||
|
|
@ -0,0 +1 @@
|
|||
exact/4xx
|
||||
|
|
@ -0,0 +1 @@
|
|||
We are out of storage
|
||||
|
|
@ -1713,34 +1713,98 @@ In the example above, if `YourException` is thrown by a controller defined in th
|
|||
package as `FooController`, a json representation of the `CustomerErrorType` POJO will be
|
||||
used instead of the `ErrorAttributes` representation.
|
||||
|
||||
If you want more specific error pages for some conditions, the embedded servlet containers
|
||||
support a uniform Java DSL for customizing the error handling. Assuming that you have a
|
||||
mapping for `/400`:
|
||||
|
||||
|
||||
[[boot-features-error-handling-custom-error-pages]]
|
||||
===== Custom error pages
|
||||
If you want to display a custom HTML error page for a given status code, you add a file to
|
||||
an `/error` folder. Error pages can either be static HTML (i.e. added under any of the
|
||||
static resource folders) or built using templates. The name of the file should be the
|
||||
exact status code or a series mask.
|
||||
|
||||
For example, to map `404` to a static HTML file, your folder structure would look like
|
||||
this:
|
||||
|
||||
[source,indent=0,subs="verbatim,quotes,attributes"]
|
||||
----
|
||||
src/
|
||||
+- main/
|
||||
+- java/
|
||||
| + <source code>
|
||||
+- resources/
|
||||
+- public/
|
||||
+- error/
|
||||
| +- 404.html
|
||||
+- <other public assets>
|
||||
----
|
||||
|
||||
To map all `5xx` errors using a freemarker template, you'd have a structure like this:
|
||||
|
||||
[source,indent=0,subs="verbatim,quotes,attributes"]
|
||||
----
|
||||
src/
|
||||
+- main/
|
||||
+- java/
|
||||
| + <source code>
|
||||
+- resources/
|
||||
+- template/
|
||||
+- error/
|
||||
| +- 5xx.ftl
|
||||
+- <other templates>
|
||||
----
|
||||
|
||||
For more complex mappings you can also add beans that implement the `ErrorViewResolver`
|
||||
interface.
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes,attributes"]
|
||||
----
|
||||
@Bean
|
||||
public EmbeddedServletContainerCustomizer containerCustomizer(){
|
||||
return new MyCustomizer();
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
private static class MyCustomizer implements EmbeddedServletContainerCustomizer {
|
||||
public class MyErrorViewResolver implements ErrorViewResolver {
|
||||
|
||||
@Override
|
||||
public void customize(ConfigurableEmbeddedServletContainer container) {
|
||||
container.addErrorPages(new ErrorPage(HttpStatus.BAD_REQUEST, "/400"));
|
||||
public ModelAndView resolveErrorView(HttpServletRequest request,
|
||||
HttpStatus status, Map<String, Object> model) {
|
||||
// Use the request or status to optionally return a ModelAndView
|
||||
return ...
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
You can also use regular Spring MVC features like
|
||||
{spring-reference}/#mvc-exceptionhandlers[`@ExceptionHandler` methods] and
|
||||
{spring-reference}/#mvc-ann-controller-advice[`@ControllerAdvice`]. The `ErrorController`
|
||||
will then pick up any unhandled exceptions.
|
||||
|
||||
|
||||
|
||||
[[boot-features-error-handling-mapping-error-pages-without-mvc]]
|
||||
===== Mapping error pages outside of Spring MVC
|
||||
For applications that aren't using Spring MVC, you can use the `ErrorPageRegistrar`
|
||||
interface to directly register `ErrorPages`. This abstraction works directly with the
|
||||
underlying embedded servlet container and will work even if you don't have a Spring MVC
|
||||
`DispatcherServlet`
|
||||
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes,attributes"]
|
||||
----
|
||||
@Bean
|
||||
public ErrorPageRegistrar errorPageRegistrar(){
|
||||
return new MyErrorPageRegistrar();
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
private static class MyErrorPageRegistrar implements ErrorPageRegistrar {
|
||||
|
||||
@Override
|
||||
public void registerErrorPages(ErrorPageRegistry registry) {
|
||||
registry.addErrorPages(new ErrorPage(HttpStatus.BAD_REQUEST, "/400"));
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
N.B. if you register an `ErrorPage` with a path that will end up being handled by a
|
||||
`Filter` (e.g. as is common with some non-Spring web frameworks, like Jersey and Wicket),
|
||||
then the `Filter` has to be explicitly registered as an `ERROR` dispatcher, e.g.
|
||||
|
|
|
|||
Loading…
Reference in New Issue