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); }