diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ErrorMvcAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ErrorMvcAutoConfiguration.java
index b0c408c6c7..9aea22c81d 100644
--- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ErrorMvcAutoConfiguration.java
+++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/ErrorMvcAutoConfiguration.java
@@ -16,19 +16,33 @@
package org.springframework.boot.actuate.autoconfigure;
+import java.util.HashMap;
+import java.util.Map;
+
import javax.servlet.Servlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.actuate.web.BasicErrorController;
import org.springframework.boot.actuate.web.ErrorController;
+import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration;
import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
import org.springframework.boot.context.embedded.ErrorPage;
import org.springframework.context.annotation.Bean;
+import org.springframework.context.expression.MapAccessor;
+import org.springframework.expression.Expression;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.expression.spel.support.StandardEvaluationContext;
+import org.springframework.util.PropertyPlaceholderHelper;
+import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver;
import org.springframework.web.servlet.DispatcherServlet;
+import org.springframework.web.servlet.View;
/**
* {@link EnableAutoConfiguration Auto-configuration} to render errors via a MVC error
@@ -37,6 +51,9 @@ import org.springframework.web.servlet.DispatcherServlet;
* @author Dave Syer
*/
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
+// Ensure this loads before the main WebMvcAutoConfiguration so that the error View is
+// available
+@AutoConfigureBefore(WebMvcAutoConfiguration.class)
public class ErrorMvcAutoConfiguration implements EmbeddedServletContainerCustomizer {
@Value("${error.path:/error}")
@@ -53,4 +70,60 @@ public class ErrorMvcAutoConfiguration implements EmbeddedServletContainerCustom
factory.addErrorPages(new ErrorPage(this.errorPath));
}
+ private SpelView defaultErrorView = new SpelView(
+ "
Whitelabel Error Page
"
+ + "This application has no explicit mapping for /error, so you are seeing this as a fallback.
"
+ + "${timestamp}
"
+ + "There was an unexpected error (type=${error}, status=${status}).
"
+ + "${message}
" + "");
+
+ @Bean(name = "error")
+ @ConditionalOnMissingBean(name = "error")
+ public View defaultErrorView() {
+ return this.defaultErrorView;
+ }
+
+ private static class SpelView implements View {
+
+ private final String template;
+
+ private final SpelExpressionParser parser = new SpelExpressionParser();
+
+ private final StandardEvaluationContext context = new StandardEvaluationContext();
+
+ private PropertyPlaceholderHelper helper;
+
+ private PlaceholderResolver resolver;
+
+ public SpelView(String template) {
+ this.template = template;
+ this.context.addPropertyAccessor(new MapAccessor());
+ this.helper = new PropertyPlaceholderHelper("${", "}");
+ this.resolver = new PlaceholderResolver() {
+ public String resolvePlaceholder(String name) {
+ Expression expression = SpelView.this.parser.parseExpression(name);
+ Object value = expression.getValue(SpelView.this.context);
+ return value == null ? null : value.toString();
+ }
+ };
+ }
+
+ public String getContentType() {
+ return "text/html";
+ }
+
+ public void render(Map model, HttpServletRequest request,
+ HttpServletResponse response) throws Exception {
+ if (response.getContentType() == null) {
+ response.setContentType(getContentType());
+ }
+ Map map = new HashMap(model);
+ map.put("path", request.getContextPath());
+ this.context.setRootObject(map);
+ String result = this.helper.replacePlaceholders(this.template, this.resolver);
+ response.getWriter().append(result);
+ }
+
+ }
+
}
diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/BasicErrorControllerIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/BasicErrorControllerIntegrationTests.java
new file mode 100644
index 0000000000..94dd58af7e
--- /dev/null
+++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/BasicErrorControllerIntegrationTests.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2012-2013 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.actuate.web;
+
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.actuate.web.BasicErrorControllerIntegrationTests.TestConfiguration;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.context.initializer.LoggingApplicationContextInitializer;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.MediaType;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+import org.springframework.test.context.web.WebAppConfiguration;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.MvcResult;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.context.WebApplicationContext;
+import org.springframework.web.servlet.View;
+import org.springframework.web.servlet.view.AbstractView;
+
+import static org.junit.Assert.assertTrue;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * @author Dave Syer
+ */
+@ContextConfiguration(classes = TestConfiguration.class, initializers = { LoggingApplicationContextInitializer.class })
+@RunWith(SpringJUnit4ClassRunner.class)
+@WebAppConfiguration
+public class BasicErrorControllerIntegrationTests {
+
+ @Autowired
+ private WebApplicationContext wac;
+
+ private MockMvc mockMvc;
+
+ @Before
+ public void setup() {
+ this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
+ }
+
+ @Test
+ public void testErrorForMachineClient() throws Exception {
+ MvcResult response = this.mockMvc.perform(get("/error"))
+ .andExpect(status().isOk()).andReturn();
+ String content = response.getResponse().getContentAsString();
+ assertTrue("Wrong content: " + content, content.contains("999"));
+ }
+
+ @Test
+ public void testErrorForBrowserClient() throws Exception {
+ MvcResult response = this.mockMvc
+ .perform(get("/error").accept(MediaType.TEXT_HTML))
+ .andExpect(status().isOk()).andReturn();
+ String content = response.getResponse().getContentAsString();
+ assertTrue("Wrong content: " + content, content.contains("ERROR_BEAN"));
+ }
+
+ @Configuration
+ @EnableAutoConfiguration
+ public static class TestConfiguration {
+
+ // For manual testing
+ public static void main(String[] args) {
+ SpringApplication.run(TestConfiguration.class, args);
+ }
+
+ @Bean
+ public View error() {
+ return new AbstractView() {
+ @Override
+ protected void renderMergedOutputModel(Map model,
+ HttpServletRequest request, HttpServletResponse response)
+ throws Exception {
+ response.getWriter().write("ERROR_BEAN");
+ }
+ };
+ }
+
+ }
+
+}
diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/DefaultErrorViewIntegrationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/DefaultErrorViewIntegrationTests.java
new file mode 100644
index 0000000000..265526de8a
--- /dev/null
+++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/web/DefaultErrorViewIntegrationTests.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2012-2013 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.actuate.web;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.actuate.web.DefaultErrorViewIntegrationTests.TestConfiguration;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.context.initializer.LoggingApplicationContextInitializer;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.MediaType;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+import org.springframework.test.context.web.WebAppConfiguration;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.MvcResult;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+import org.springframework.web.context.WebApplicationContext;
+
+import static org.junit.Assert.assertTrue;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * @author Dave Syer
+ */
+@ContextConfiguration(classes = TestConfiguration.class, initializers = { LoggingApplicationContextInitializer.class })
+@RunWith(SpringJUnit4ClassRunner.class)
+@WebAppConfiguration
+public class DefaultErrorViewIntegrationTests {
+
+ @Autowired
+ private WebApplicationContext wac;
+
+ private MockMvc mockMvc;
+
+ @Before
+ public void setup() {
+ this.mockMvc = MockMvcBuilders.webAppContextSetup(this.wac).build();
+ }
+
+ @Test
+ public void testErrorForBrowserClient() throws Exception {
+ MvcResult response = this.mockMvc
+ .perform(get("/error").accept(MediaType.TEXT_HTML))
+ .andExpect(status().isOk()).andReturn();
+ String content = response.getResponse().getContentAsString();
+ assertTrue("Wrong content: " + content, content.contains(""));
+ assertTrue("Wrong content: " + content, content.contains("999"));
+ }
+
+ @Configuration
+ @EnableAutoConfiguration
+ public static class TestConfiguration {
+
+ // For manual testing
+ public static void main(String[] args) {
+ SpringApplication.run(TestConfiguration.class, args);
+ }
+
+ }
+
+}
diff --git a/spring-boot-samples/spring-boot-sample-actuator-ui/src/main/java/org/springframework/boot/sample/ops/ui/SampleActuatorUiApplication.java b/spring-boot-samples/spring-boot-sample-actuator-ui/src/main/java/org/springframework/boot/sample/ops/ui/SampleActuatorUiApplication.java
index 503d1e8e66..1206e05a49 100644
--- a/spring-boot-samples/spring-boot-sample-actuator-ui/src/main/java/org/springframework/boot/sample/ops/ui/SampleActuatorUiApplication.java
+++ b/spring-boot-samples/spring-boot-sample-actuator-ui/src/main/java/org/springframework/boot/sample/ops/ui/SampleActuatorUiApplication.java
@@ -40,6 +40,11 @@ public class SampleActuatorUiApplication {
return "home";
}
+ @RequestMapping("/foo")
+ public String foo() {
+ throw new RuntimeException("Expected exception in controller");
+ }
+
public static void main(String[] args) throws Exception {
SpringApplication.run(SampleActuatorUiApplication.class, args);
}