Remove @ControllerAdvice from BasicErrorController

Update the BasicErrorController so that it no longer needs to implement
@ControllerAdvice or have an @ExceptionHandler method.

A new ErrorAttributes interface is now used to obtain error details,
the DefaultErrorAttributes implementation uses a
HandlerExceptionResolver to obtain root exception details if the
`javax.servlet.error.*` attributes are missing.

This change also removes the need for the extract(...) method on
ErrorController as classes such as WebRequestTraceFilter can
now use the ErrorAttributes interface directly.

See gh-839, gh-538
Fixes gh-843
This commit is contained in:
Phillip Webb 2014-05-12 17:19:51 +01:00
parent 5dd77a725c
commit 30ac768cbf
15 changed files with 573 additions and 217 deletions

View File

@ -34,7 +34,7 @@ import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoints;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.SearchStrategy; import org.springframework.boot.autoconfigure.condition.SearchStrategy;
import org.springframework.boot.autoconfigure.web.ErrorController; import org.springframework.boot.autoconfigure.web.ErrorAttributes;
import org.springframework.boot.autoconfigure.web.HttpMessageConverters; import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer; import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer;
import org.springframework.boot.context.embedded.EmbeddedServletContainer; import org.springframework.boot.context.embedded.EmbeddedServletContainer;
@ -125,12 +125,11 @@ public class EndpointWebMvcChildContextConfiguration {
/* /*
* The error controller is present but not mapped as an endpoint in this context * The error controller is present but not mapped as an endpoint in this context
* because of the DispatcherServlet having had it's HandlerMapping explicitly * because of the DispatcherServlet having had it's HandlerMapping explicitly
* disabled. So this tiny shim exposes the same feature but only for machine * disabled. So we expose the same feature but only for machine endpoints.
* endpoints.
*/ */
@Bean @Bean
public ManagementErrorEndpoint errorEndpoint(final ErrorController controller) { public ManagementErrorEndpoint errorEndpoint(final ErrorAttributes errorAttributes) {
return new ManagementErrorEndpoint(this.errorPath, controller); return new ManagementErrorEndpoint(this.errorPath, errorAttributes);
} }
@Configuration @Configuration

View File

@ -27,7 +27,7 @@ import org.springframework.boot.actuate.trace.WebRequestTraceFilter;
import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.web.BasicErrorController; import org.springframework.boot.autoconfigure.web.ErrorAttributes;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.DispatcherServlet;
@ -45,7 +45,7 @@ public class TraceWebFilterAutoConfiguration {
private TraceRepository traceRepository; private TraceRepository traceRepository;
@Autowired(required = false) @Autowired(required = false)
private BasicErrorController errorController; private ErrorAttributes errorAttributes;
@Value("${management.dump_requests:false}") @Value("${management.dump_requests:false}")
private boolean dumpRequests; private boolean dumpRequests;
@ -54,8 +54,8 @@ public class TraceWebFilterAutoConfiguration {
public WebRequestTraceFilter webRequestLoggingFilter(BeanFactory beanFactory) { public WebRequestTraceFilter webRequestLoggingFilter(BeanFactory beanFactory) {
WebRequestTraceFilter filter = new WebRequestTraceFilter(this.traceRepository); WebRequestTraceFilter filter = new WebRequestTraceFilter(this.traceRepository);
filter.setDumpRequests(this.dumpRequests); filter.setDumpRequests(this.dumpRequests);
if (this.errorController != null) { if (this.errorAttributes != null) {
filter.setErrorController(this.errorController); filter.setErrorAttributes(this.errorAttributes);
} }
return filter; return filter;
} }

View File

@ -19,12 +19,12 @@ package org.springframework.boot.actuate.endpoint.mvc;
import java.util.Map; import java.util.Map;
import org.springframework.boot.actuate.endpoint.Endpoint; import org.springframework.boot.actuate.endpoint.Endpoint;
import org.springframework.boot.autoconfigure.web.ErrorAttributes;
import org.springframework.boot.autoconfigure.web.ErrorController; import org.springframework.boot.autoconfigure.web.ErrorController;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.RequestContextHolder;
/** /**
@ -37,21 +37,21 @@ import org.springframework.web.context.request.RequestContextHolder;
@ConfigurationProperties(prefix = "error") @ConfigurationProperties(prefix = "error")
public class ManagementErrorEndpoint implements MvcEndpoint { public class ManagementErrorEndpoint implements MvcEndpoint {
private final ErrorController controller; private final ErrorAttributes errorAttributes;
private final String path; private final String path;
public ManagementErrorEndpoint(String path, ErrorController controller) { public ManagementErrorEndpoint(String path, ErrorAttributes errorAttributes) {
Assert.notNull(controller, "Controller must not be null"); Assert.notNull(errorAttributes, "ErrorAttributes must not be null");
this.path = path; this.path = path;
this.controller = controller; this.errorAttributes = errorAttributes;
} }
@RequestMapping @RequestMapping
@ResponseBody @ResponseBody
public Map<String, Object> invoke() { public Map<String, Object> invoke() {
RequestAttributes attributes = RequestContextHolder.currentRequestAttributes(); return this.errorAttributes.getErrorAttributes(
return this.controller.extract(attributes, false, true); RequestContextHolder.currentRequestAttributes(), false);
} }
@Override @Override

View File

@ -34,8 +34,9 @@ import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.springframework.boot.autoconfigure.web.BasicErrorController; import org.springframework.boot.autoconfigure.web.ErrorAttributes;
import org.springframework.core.Ordered; import org.springframework.core.Ordered;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.context.request.ServletRequestAttributes;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
@ -58,7 +59,7 @@ public class WebRequestTraceFilter implements Filter, Ordered {
private final ObjectMapper objectMapper = new ObjectMapper(); private final ObjectMapper objectMapper = new ObjectMapper();
private BasicErrorController errorController; private ErrorAttributes errorAttributes;
/** /**
* @param traceRepository * @param traceRepository
@ -156,13 +157,13 @@ public class WebRequestTraceFilter implements Filter, Ordered {
trace.put("method", request.getMethod()); trace.put("method", request.getMethod());
trace.put("path", request.getRequestURI()); trace.put("path", request.getRequestURI());
trace.put("headers", allHeaders); trace.put("headers", allHeaders);
Throwable error = (Throwable) request Throwable exception = (Throwable) request
.getAttribute("javax.servlet.error.exception"); .getAttribute("javax.servlet.error.exception");
if (error != null) { if (exception != null && this.errorAttributes != null) {
if (this.errorController != null) { RequestAttributes requestAttributes = new ServletRequestAttributes(request);
trace.put("error", this.errorController.extract( Map<String, Object> error = this.errorAttributes.getErrorAttributes(
new ServletRequestAttributes(request), true, false)); requestAttributes, true);
} trace.put("error", error);
} }
return trace; return trace;
} }
@ -175,8 +176,8 @@ public class WebRequestTraceFilter implements Filter, Ordered {
public void destroy() { public void destroy() {
} }
public void setErrorController(BasicErrorController errorController) { public void setErrorAttributes(ErrorAttributes errorAttributes) {
this.errorController = errorController; this.errorAttributes = errorAttributes;
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2013 the original author or authors. * Copyright 2012-2014 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -19,7 +19,7 @@ package org.springframework.boot.actuate.trace;
import java.util.Map; import java.util.Map;
import org.junit.Test; import org.junit.Test;
import org.springframework.boot.autoconfigure.web.BasicErrorController; import org.springframework.boot.autoconfigure.web.DefaulErrorAttributes;
import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockHttpServletResponse;
@ -76,7 +76,7 @@ public class WebRequestTraceFilterTests {
@Test @Test
public void filterHasError() { public void filterHasError() {
this.filter.setErrorController(new BasicErrorController()); this.filter.setErrorAttributes(new DefaulErrorAttributes());
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo"); MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
MockHttpServletResponse response = new MockHttpServletResponse(); MockHttpServletResponse response = new MockHttpServletResponse();
response.setStatus(500); response.setStatus(500);

View File

@ -20,9 +20,6 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
@ -35,9 +32,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AuthenticationEventPublisher; import org.springframework.security.authentication.AuthenticationEventPublisher;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.DefaultAuthenticationEventPublisher; import org.springframework.security.authentication.DefaultAuthenticationEventPublisher;
@ -51,13 +46,10 @@ import org.springframework.security.config.annotation.web.configuration.WebSecur
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer; import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity; import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint; import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint;
import org.springframework.security.web.header.writers.HstsHeaderWriter; import org.springframework.security.web.header.writers.HstsHeaderWriter;
import org.springframework.security.web.util.matcher.AnyRequestMatcher; import org.springframework.security.web.util.matcher.AnyRequestMatcher;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.support.RequestDataValueProcessor; import org.springframework.web.servlet.support.RequestDataValueProcessor;
/** /**
@ -109,18 +101,6 @@ public class SpringBootWebSecurityConfiguration {
return new IgnoredPathsWebSecurityConfigurerAdapter(); return new IgnoredPathsWebSecurityConfigurerAdapter();
} }
@ControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE + 10)
protected static class SecurityExceptionRethrowingAdvice {
@ExceptionHandler({ AccessDeniedException.class, AuthenticationException.class })
public void handle(HttpServletRequest request, HttpServletResponse response,
Exception e) throws Exception {
throw e;
}
}
public static void configureHeaders(HeadersConfigurer<?> configurer, public static void configureHeaders(HeadersConfigurer<?> configurer,
SecurityProperties.Headers headers) throws Exception { SecurityProperties.Headers headers) throws Exception {
if (headers.getHsts() != Headers.HSTS.none) { if (headers.getHsts() != Headers.HSTS.none) {

View File

@ -16,188 +16,89 @@
package org.springframework.boot.autoconfigure.web; package org.springframework.boot.autoconfigure.web;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.embedded.AbstractEmbeddedServletContainerFactory; import org.springframework.boot.context.embedded.AbstractEmbeddedServletContainerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.validation.BindException; import org.springframework.util.Assert;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver;
import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver;
/** /**
* Basic global error {@link Controller}, rendering servlet container error codes and * Basic global error {@link Controller}, rendering {@link ErrorAttributes}. More specific
* messages where available. More specific errors can be handled either using Spring MVC * errors can be handled either using Spring MVC abstractions (e.g.
* abstractions (e.g. {@code @ExceptionHandler}) or by adding servlet * {@code @ExceptionHandler}) or by adding servlet
* {@link AbstractEmbeddedServletContainerFactory#setErrorPages(java.util.Set) container * {@link AbstractEmbeddedServletContainerFactory#setErrorPages container error pages}.
* error pages}.
* *
* @author Dave Syer * @author Dave Syer
* @author Phillip Webb
* @see ErrorAttributes
*/ */
@Controller @Controller
@ControllerAdvice
@Order(0)
public class BasicErrorController implements ErrorController { public class BasicErrorController implements ErrorController {
private static final String ERROR_KEY = "error";
private final Log logger = LogFactory.getLog(BasicErrorController.class);
private DefaultHandlerExceptionResolver resolver = new DefaultHandlerExceptionResolver();
private ResponseStatusExceptionResolver statuses = new ResponseStatusExceptionResolver();
@Value("${error.path:/error}") @Value("${error.path:/error}")
private String errorPath; private String errorPath;
private final ErrorAttributes errorAttributes;
public BasicErrorController(ErrorAttributes errorAttributes) {
Assert.notNull(errorAttributes, "ErrorAttributes must not be null");
this.errorAttributes = errorAttributes;
}
@Override @Override
public String getErrorPath() { public String getErrorPath() {
return this.errorPath; return this.errorPath;
} }
@ExceptionHandler(Exception.class)
public void handle(HttpServletRequest request, HttpServletResponse response,
Exception e) throws Exception {
if (this.statuses.resolveException(request, response, null, e) == null) {
this.resolver.resolveException(request, response, null, e);
}
if (response.getStatus() == HttpServletResponse.SC_OK) {
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
// There's only one exception so it's easier for the error controller to identify
// it this way...
request.setAttribute(ErrorController.class.getName(), e);
if (e instanceof BindException) {
// ... but other error pages might be looking for it here as well
request.setAttribute(
BindingResult.MODEL_KEY_PREFIX + ((BindException) e).getObjectName(),
e);
}
}
@RequestMapping(value = "${error.path:/error}", produces = "text/html") @RequestMapping(value = "${error.path:/error}", produces = "text/html")
public ModelAndView errorHtml(HttpServletRequest request) { public ModelAndView errorHtml(HttpServletRequest request) {
Map<String, Object> map = extract(new ServletRequestAttributes(request), false, return new ModelAndView("error", getErrorAttributes(request, false));
false);
return new ModelAndView(ERROR_KEY, map);
} }
@RequestMapping(value = "${error.path:/error}") @RequestMapping(value = "${error.path:/error}")
@ResponseBody @ResponseBody
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) { public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
ServletRequestAttributes attributes = new ServletRequestAttributes(request); Map<String, Object> body = getErrorAttributes(request, getTraceParameter(request));
String trace = request.getParameter("trace"); HttpStatus status = getStatus(request);
Map<String, Object> extracted = extract(attributes, return new ResponseEntity<Map<String, Object>>(body, status);
trace != null && !"false".equals(trace.toLowerCase()), true);
HttpStatus statusCode = getStatus((Integer) extracted.get("status"));
return new ResponseEntity<Map<String, Object>>(extracted, statusCode);
} }
private HttpStatus getStatus(Integer value) { private boolean getTraceParameter(HttpServletRequest request) {
String parameter = request.getParameter("trace");
if (parameter == null) {
return false;
}
return !"false".equals(parameter.toLowerCase());
}
private Map<String, Object> getErrorAttributes(HttpServletRequest request,
boolean includeStackTrace) {
RequestAttributes requestAttributes = new ServletRequestAttributes(request);
return this.errorAttributes.getErrorAttributes(requestAttributes,
includeStackTrace);
}
private HttpStatus getStatus(HttpServletRequest request) {
Integer statusCode = (Integer) request
.getAttribute("javax.servlet.error.status_code");
if (statusCode != null) {
try { try {
return HttpStatus.valueOf(value); return HttpStatus.valueOf(statusCode);
} }
catch (Exception ex) { catch (Exception ex) {
}
}
return HttpStatus.INTERNAL_SERVER_ERROR; return HttpStatus.INTERNAL_SERVER_ERROR;
} }
}
@Override
public Map<String, Object> extract(RequestAttributes attributes, boolean trace,
boolean log) {
Map<String, Object> map = new LinkedHashMap<String, Object>();
map.put("timestamp", new Date());
try {
Throwable error = (Throwable) attributes.getAttribute(
ErrorController.class.getName(), RequestAttributes.SCOPE_REQUEST);
Object obj = attributes.getAttribute("javax.servlet.error.status_code",
RequestAttributes.SCOPE_REQUEST);
int status = 999;
if (obj != null) {
status = (Integer) obj;
map.put(ERROR_KEY, HttpStatus.valueOf(status).getReasonPhrase());
}
else {
map.put(ERROR_KEY, "None");
}
map.put("status", status);
if (error == null) {
error = (Throwable) attributes.getAttribute(
"javax.servlet.error.exception", RequestAttributes.SCOPE_REQUEST);
}
if (error != null) {
while (error instanceof ServletException && error.getCause() != null) {
error = ((ServletException) error).getCause();
}
map.put("exception", error.getClass().getName());
addMessage(map, error);
if (trace) {
StringWriter stackTrace = new StringWriter();
error.printStackTrace(new PrintWriter(stackTrace));
stackTrace.flush();
map.put("trace", stackTrace.toString());
}
if (log) {
this.logger.error(error);
}
}
else {
Object message = attributes.getAttribute("javax.servlet.error.message",
RequestAttributes.SCOPE_REQUEST);
map.put("message", message == null ? "No message available" : message);
}
String path = (String) attributes.getAttribute(
"javax.servlet.error.request_uri", RequestAttributes.SCOPE_REQUEST);
map.put("path", path == null ? "No path available" : path);
return map;
}
catch (Exception ex) {
map.put(ERROR_KEY, ex.getClass().getName());
map.put("message", ex.getMessage());
if (log) {
this.logger.error(ex);
}
return map;
}
}
protected void addMessage(Map<String, Object> map, Throwable error) {
if (error instanceof BindingResult) {
BindingResult result = (BindingResult) error;
if (result.getErrorCount() > 0) {
map.put("errors", result.getAllErrors());
map.put("message",
"Validation failed for object='" + result.getObjectName()
+ "'. Error count: " + result.getErrorCount());
}
else {
map.put("message", "No errors");
}
}
else {
map.put("message", error.getMessage());
}
}
} }

View File

@ -0,0 +1,177 @@
/*
* Copyright 2012-2014 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.io.PrintWriter;
import java.io.StringWriter;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
/**
* Default implementation of {@link ErrorAttributes}. Provides the following attributes
* when possible:
* <ul>
* <li>timestamp - The time that the errors were extracted</li>
* <li>status - The status code</li>
* <li>error - The error reason</li>
* <li>exception - The class name of the root exception</li>
* <li>message - The exception message</li>
* <li>errors - Any {@link ObjectError}s from a {@link BindingResult} exception
* <li>trace - The exception stack trace</li>
* <li>path - The URL path when the exception was raised</li>
* </ul>
*
* @author Phillip Webb
* @author Dave Syer
* @since 1.1.0
* @see ErrorAttributes
*/
@Order(Ordered.HIGHEST_PRECEDENCE)
public class DefaulErrorAttributes implements ErrorAttributes, HandlerExceptionResolver,
Ordered {
private static final String ERROR_ATTRIBUTE = DefaulErrorAttributes.class.getName()
+ ".ERROR";
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
@Override
public ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex) {
storeErrorAttributes(request, ex);
return null;
}
private void storeErrorAttributes(HttpServletRequest request, Exception ex) {
request.setAttribute(ERROR_ATTRIBUTE, ex);
}
@Override
public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes,
boolean includeStackTrace) {
Map<String, Object> errorAttributes = new LinkedHashMap<String, Object>();
errorAttributes.put("timestamp", new Date());
addStatus(errorAttributes, requestAttributes);
addErrorDetails(errorAttributes, requestAttributes, includeStackTrace);
addPath(errorAttributes, requestAttributes);
return errorAttributes;
}
private void addStatus(Map<String, Object> errorAttributes,
RequestAttributes requestAttributes) {
Integer status = getAttribute(requestAttributes,
"javax.servlet.error.status_code");
if (status == null) {
errorAttributes.put("status", 999);
errorAttributes.put("error", "None");
return;
}
errorAttributes.put("status", status);
try {
errorAttributes.put("error", HttpStatus.valueOf(status).getReasonPhrase());
}
catch (Exception ex) {
// Unable to obtain a reason
errorAttributes.put("error", "Http Status " + status);
}
}
private void addErrorDetails(Map<String, Object> errorAttributes,
RequestAttributes requestAttributes, boolean includeStackTrace) {
Throwable error = getError(requestAttributes);
if (error != null) {
while (error instanceof ServletException && error.getCause() != null) {
error = ((ServletException) error).getCause();
}
errorAttributes.put("exception", error.getClass().getName());
addErrorMessage(errorAttributes, error);
if (includeStackTrace) {
addStackTrace(errorAttributes, error);
}
}
else {
Object message = getAttribute(requestAttributes,
"javax.servlet.error.message");
errorAttributes.put("message", message == null ? "No message available"
: message);
}
}
private void addErrorMessage(Map<String, Object> errorAttributes, Throwable error) {
if (!(error instanceof BindingResult)) {
errorAttributes.put("message", error.getMessage());
return;
}
BindingResult result = (BindingResult) error;
if (result.getErrorCount() > 0) {
errorAttributes.put("errors", result.getAllErrors());
errorAttributes.put("message",
"Validation failed for object='" + result.getObjectName()
+ "'. Error count: " + result.getErrorCount());
}
else {
errorAttributes.put("message", "No errors");
}
}
private void addStackTrace(Map<String, Object> errorAttributes, Throwable error) {
StringWriter stackTrace = new StringWriter();
error.printStackTrace(new PrintWriter(stackTrace));
stackTrace.flush();
errorAttributes.put("trace", stackTrace.toString());
}
private void addPath(Map<String, Object> errorAttributes,
RequestAttributes requestAttributes) {
String path = getAttribute(requestAttributes, "javax.servlet.error.request_uri");
if (path != null) {
errorAttributes.put("path", path);
}
}
@Override
public Throwable getError(RequestAttributes requestAttributes) {
Exception exception = getAttribute(requestAttributes, ERROR_ATTRIBUTE);
if (exception == null) {
exception = getAttribute(requestAttributes, "javax.servlet.error.exception");
}
return exception;
}
@SuppressWarnings("unchecked")
private <T> T getAttribute(RequestAttributes requestAttributes, String name) {
return (T) requestAttributes.getAttribute(name, RequestAttributes.SCOPE_REQUEST);
}
}

View File

@ -0,0 +1,52 @@
/*
* Copyright 2012-2014 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 org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.servlet.ModelAndView;
/**
* Provides access to error attributes which can be logged or presented to the user.
*
* @author Phillip Webb
* @since 1.1.0
* @see DefaulErrorAttributes
*/
public interface ErrorAttributes {
/**
* Returns a {@link Map} of the error attributes. The map can be used as the model of
* an error page {@link ModelAndView}, or returned as a {@link ResponseBody}.
* @param requestAttributes the source request attributes
* @param includeStackTrace if stack trace elements should be included
* @return a map of error attributes
*/
public Map<String, Object> getErrorAttributes(RequestAttributes requestAttributes,
boolean includeStackTrace);
/**
* Return the underlying cause of the error or {@code null} if the error cannot be
* extracted.
* @param requestAttributes the source request attributes
* @return the {@link Exception} that caused the error or {@code null}
*/
public Throwable getError(RequestAttributes requestAttributes);
}

View File

@ -16,14 +16,11 @@
package org.springframework.boot.autoconfigure.web; package org.springframework.boot.autoconfigure.web;
import java.util.Map;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.web.context.request.RequestAttributes;
/** /**
* Marker interface used to indicate that a {@link Controller @Controller} is used to * Marker interface used to indicate that a {@link Controller @Controller} is used to
* render errors. * render errors. Primarily used to know the error paths that will not need to be secured.
* *
* @author Phillip Webb * @author Phillip Webb
*/ */
@ -34,14 +31,4 @@ public interface ErrorController {
*/ */
public String getErrorPath(); public String getErrorPath();
/**
* Extract a useful model of the error from the request attributes.
* @param attributes the request attributes
* @param trace flag to indicate that stack trace information should be included
* @param log flag to indicate that an error should be logged
* @return a model containing error messages and codes etc.
*/
public Map<String, Object> extract(RequestAttributes attributes, boolean trace,
boolean log);
} }

View File

@ -72,10 +72,16 @@ public class ErrorMvcAutoConfiguration implements EmbeddedServletContainerCustom
@Value("${error.path:/error}") @Value("${error.path:/error}")
private String errorPath = "/error"; private String errorPath = "/error";
@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaulErrorAttributes errorAttributes() {
return new DefaulErrorAttributes();
}
@Bean @Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT) @ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController() { public BasicErrorController basicErrorController(ErrorAttributes errorAttributes) {
return new BasicErrorController(); return new BasicErrorController(errorAttributes);
} }
@Override @Override
@ -136,6 +142,9 @@ public class ErrorMvcAutoConfiguration implements EmbeddedServletContainerCustom
} }
/**
* Simple {@link View} implementation that resolves variables as SpEL expressions.
*/
private static class SpelView implements View { private static class SpelView implements View {
private final String template; private final String template;
@ -156,8 +165,13 @@ public class ErrorMvcAutoConfiguration implements EmbeddedServletContainerCustom
@Override @Override
public String resolvePlaceholder(String name) { public String resolvePlaceholder(String name) {
Expression expression = SpelView.this.parser.parseExpression(name); Expression expression = SpelView.this.parser.parseExpression(name);
try {
Object value = expression.getValue(SpelView.this.context); Object value = expression.getValue(SpelView.this.context);
return value == null ? null : value.toString(); return (value == null ? null : value.toString());
}
catch (Exception ex) {
return null;
}
} }
}; };
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2013 the original author or authors. * Copyright 2012-2014 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -26,6 +26,7 @@ import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration
import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.test.web.servlet.setup.MockMvcBuilders;
@ -37,9 +38,12 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/** /**
* Tests for {@link BasicErrorController} using {@link MockMvc} but not
* {@link SpringJUnit4ClassRunner}.
*
* @author Dave Syer * @author Dave Syer
*/ */
public class BasicErrorControllerSpecialIntegrationTests { public class BasicErrorControllerDirectMockMvcTests {
private ConfigurableWebApplicationContext wac; private ConfigurableWebApplicationContext wac;

View File

@ -0,0 +1,72 @@
/*
* Copyright 2012-2014 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 org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.IntegrationTest;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.boot.test.TestRestTemplate;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.endsWith;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link BasicErrorController} using {@link IntegrationTest} that hit a real
* HTTP server.
*
* @author Phillip Webb
* @author Dave Syer
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = BasicErrorControllerMockMvcTests.TestConfiguration.class)
@WebAppConfiguration
@IntegrationTest("server.port=0")
public class BasicErrorControllerIntegrationTest {
@Value("${local.server.port}")
private int port;
@Test
@SuppressWarnings("rawtypes")
public void testErrorForMachineClient() throws Exception {
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(
"http://localhost:" + this.port, Map.class);
assertThat(entity.getBody().toString(), endsWith("status=500, "
+ "error=Internal Server Error, "
+ "exception=java.lang.IllegalStateException, " + "message=Expected!, "
+ "path=/}"));
}
@Test
@SuppressWarnings("rawtypes")
public void testBindingExceptionForMachineClient() throws Exception {
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(
"http://localhost:" + this.port + "/bind", Map.class);
String resp = entity.getBody().toString();
assertThat(resp, containsString("Error count: 1"));
assertThat(resp, containsString("errors=[{codes="));
assertThat(resp, containsString("org.springframework.validation.BindException"));
}
}

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2013 the original author or authors. * Copyright 2012-2014 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -32,7 +32,7 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration; import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration;
import org.springframework.boot.autoconfigure.web.BasicErrorControllerIntegrationTests.TestConfiguration; import org.springframework.boot.autoconfigure.web.BasicErrorControllerMockMvcTests.TestConfiguration;
import org.springframework.boot.test.SpringApplicationConfiguration; import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@ -59,13 +59,16 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/** /**
* Tests for {@link BasicErrorController} using {@link MockMvc} and
* {@link SpringJUnit4ClassRunner}.
*
* @author Dave Syer * @author Dave Syer
*/ */
@SpringApplicationConfiguration(classes = TestConfiguration.class) @SpringApplicationConfiguration(classes = TestConfiguration.class)
@RunWith(SpringJUnit4ClassRunner.class) @RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration @WebAppConfiguration
@DirtiesContext @DirtiesContext
public class BasicErrorControllerIntegrationTests { public class BasicErrorControllerMockMvcTests {
@Autowired @Autowired
private WebApplicationContext wac; private WebApplicationContext wac;
@ -85,16 +88,6 @@ public class BasicErrorControllerIntegrationTests {
assertTrue("Wrong content: " + content, content.contains("999")); assertTrue("Wrong content: " + content, content.contains("999"));
} }
@Test
public void testErrorForMachineClient() throws Exception {
MvcResult result = this.mockMvc.perform(get("/"))
.andExpect(status().is5xxServerError()).andReturn();
MvcResult response = this.mockMvc.perform(new ErrorDispatcher(result, "/error"))
.andReturn();
String content = response.getResponse().getContentAsString();
assertTrue("Wrong content: " + content, content.contains("Expected!"));
}
@Test @Test
public void testErrorWithResponseStatus() throws Exception { public void testErrorWithResponseStatus() throws Exception {
MvcResult result = this.mockMvc.perform(get("/bang")) MvcResult result = this.mockMvc.perform(get("/bang"))

View File

@ -0,0 +1,176 @@
/*
* Copyright 2012-2014 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.Date;
import java.util.Map;
import javax.servlet.ServletException;
import org.junit.Test;
import org.springframework.http.HttpStatus;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.MapBindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.ModelAndView;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.sameInstance;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link DefaulErrorAttributes}.
*
* @author Phillip Webb
*/
public class DefaultErrorAttributesTests {
private DefaulErrorAttributes errorAttributes = new DefaulErrorAttributes();
private MockHttpServletRequest request = new MockHttpServletRequest();
private RequestAttributes requestAttributes = new ServletRequestAttributes(
this.request);
@Test
public void includeTimeStamp() throws Exception {
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(
this.requestAttributes, false);
assertThat(attributes.get("timestamp"), instanceOf(Date.class));
}
@Test
public void specificStatusCode() throws Exception {
this.request.setAttribute("javax.servlet.error.status_code", 404);
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(
this.requestAttributes, false);
assertThat(attributes.get("error"),
equalTo((Object) HttpStatus.NOT_FOUND.getReasonPhrase()));
assertThat(attributes.get("status"), equalTo((Object) 404));
}
@Test
public void missingStatusCode() throws Exception {
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(
this.requestAttributes, false);
assertThat(attributes.get("error"), equalTo((Object) "None"));
assertThat(attributes.get("status"), equalTo((Object) 999));
}
@Test
public void mvcError() throws Exception {
RuntimeException ex = new RuntimeException("Test");
ModelAndView modelAndView = this.errorAttributes.resolveException(this.request,
null, null, ex);
this.request.setAttribute("javax.servlet.error.exception", new RuntimeException(
"Ignored"));
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(
this.requestAttributes, false);
assertThat(this.errorAttributes.getError(this.requestAttributes),
sameInstance((Object) ex));
assertThat(modelAndView, nullValue());
assertThat(attributes.get("exception"),
equalTo((Object) RuntimeException.class.getName()));
assertThat(attributes.get("message"), equalTo((Object) "Test"));
}
@Test
public void servletError() throws Exception {
RuntimeException ex = new RuntimeException("Test");
this.request.setAttribute("javax.servlet.error.exception", ex);
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(
this.requestAttributes, false);
assertThat(this.errorAttributes.getError(this.requestAttributes),
sameInstance((Object) ex));
assertThat(attributes.get("exception"),
equalTo((Object) RuntimeException.class.getName()));
assertThat(attributes.get("message"), equalTo((Object) "Test"));
}
@Test
public void servletMessage() throws Exception {
this.request.setAttribute("javax.servlet.error.message", "Test");
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(
this.requestAttributes, false);
assertThat(attributes.get("exception"), nullValue());
assertThat(attributes.get("message"), equalTo((Object) "Test"));
}
@Test
public void unwrapServletException() throws Exception {
RuntimeException ex = new RuntimeException("Test");
ServletException wrapped = new ServletException(new ServletException(ex));
this.request.setAttribute("javax.servlet.error.exception", wrapped);
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(
this.requestAttributes, false);
assertThat(this.errorAttributes.getError(this.requestAttributes),
sameInstance((Object) wrapped));
assertThat(attributes.get("exception"),
equalTo((Object) RuntimeException.class.getName()));
assertThat(attributes.get("message"), equalTo((Object) "Test"));
}
@Test
public void extractBindingResultErrors() throws Exception {
BindingResult bindingResult = new MapBindingResult(Collections.singletonMap("a",
"b"), "objectName");
bindingResult.addError(new ObjectError("c", "d"));
BindException ex = new BindException(bindingResult);
this.request.setAttribute("javax.servlet.error.exception", ex);
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(
this.requestAttributes, false);
assertThat(attributes.get("message"), equalTo((Object) ("Validation failed for "
+ "object='objectName'. Error count: 1")));
assertThat(attributes.get("errors"),
equalTo((Object) bindingResult.getAllErrors()));
}
@Test
public void trace() throws Exception {
RuntimeException ex = new RuntimeException("Test");
this.request.setAttribute("javax.servlet.error.exception", ex);
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(
this.requestAttributes, true);
assertThat(attributes.get("trace").toString(), startsWith("java.lang"));
}
@Test
public void noTrace() throws Exception {
RuntimeException ex = new RuntimeException("Test");
this.request.setAttribute("javax.servlet.error.exception", ex);
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(
this.requestAttributes, false);
assertThat(attributes.get("trace"), nullValue());
}
@Test
public void path() throws Exception {
this.request.setAttribute("javax.servlet.error.request_uri", "path");
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(
this.requestAttributes, false);
assertThat(attributes.get("path"), equalTo((Object) "path"));
}
}