Create spring-boot-webmvc module

This commit is contained in:
Andy Wilkinson
2025-03-27 14:05:55 +00:00
committed by Phillip Webb
parent e83f4eed4e
commit dfd5e56956
129 changed files with 450 additions and 360 deletions

View File

@@ -0,0 +1,214 @@
/*
* Copyright 2012-2025 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
*
* https://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.webmvc.autoconfigure;
import java.util.Arrays;
import java.util.List;
import jakarta.servlet.MultipartConfigElement;
import jakarta.servlet.ServletRegistration;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureOrder;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
import org.springframework.boot.autoconfigure.condition.ConditionMessage.Style;
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.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.servlet.DispatcherServlet;
/**
* {@link EnableAutoConfiguration Auto-configuration} for the Spring
* {@link DispatcherServlet}. Should work for a standalone application where an embedded
* web server is already present and also for a deployable application using
* {@link SpringBootServletInitializer}.
*
* @author Phillip Webb
* @author Dave Syer
* @author Stephane Nicoll
* @author Brian Clozel
* @since 4.0.0
*/
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@AutoConfiguration
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass(DispatcherServlet.class)
public class DispatcherServletAutoConfiguration {
/**
* The bean name for a DispatcherServlet that will be mapped to the root URL "/".
*/
public static final String DEFAULT_DISPATCHER_SERVLET_BEAN_NAME = "dispatcherServlet";
/**
* The bean name for a ServletRegistrationBean for the DispatcherServlet "/".
*/
public static final String DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME = "dispatcherServletRegistration";
@Configuration(proxyBeanMethods = false)
@Conditional(DefaultDispatcherServletCondition.class)
@ConditionalOnClass(ServletRegistration.class)
@EnableConfigurationProperties(WebMvcProperties.class)
protected static class DispatcherServletConfiguration {
@Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) {
DispatcherServlet dispatcherServlet = new DispatcherServlet();
dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest());
dispatcherServlet.setDispatchTraceRequest(webMvcProperties.isDispatchTraceRequest());
dispatcherServlet.setPublishEvents(webMvcProperties.isPublishRequestHandledEvents());
dispatcherServlet.setEnableLoggingRequestDetails(webMvcProperties.isLogRequestDetails());
return dispatcherServlet;
}
@Bean
@ConditionalOnBean(MultipartResolver.class)
@ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
public MultipartResolver multipartResolver(MultipartResolver resolver) {
// Detect if the user has created a MultipartResolver but named it incorrectly
return resolver;
}
}
@Configuration(proxyBeanMethods = false)
@Conditional(DispatcherServletRegistrationCondition.class)
@ConditionalOnClass(ServletRegistration.class)
@EnableConfigurationProperties(WebMvcProperties.class)
@Import(DispatcherServletConfiguration.class)
protected static class DispatcherServletRegistrationConfiguration {
@Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
@ConditionalOnBean(value = DispatcherServlet.class, name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet,
WebMvcProperties webMvcProperties, ObjectProvider<MultipartConfigElement> multipartConfig) {
DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet,
webMvcProperties.getServlet().getPath());
registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
registration.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup());
multipartConfig.ifAvailable(registration::setMultipartConfig);
return registration;
}
}
@Order(Ordered.LOWEST_PRECEDENCE - 10)
private static final class DefaultDispatcherServletCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
ConditionMessage.Builder message = ConditionMessage.forCondition("Default DispatcherServlet");
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
List<String> dispatchServletBeans = Arrays
.asList(beanFactory.getBeanNamesForType(DispatcherServlet.class, false, false));
if (dispatchServletBeans.contains(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)) {
return ConditionOutcome
.noMatch(message.found("dispatcher servlet bean").items(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME));
}
if (beanFactory.containsBean(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)) {
return ConditionOutcome
.noMatch(message.found("non dispatcher servlet bean").items(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME));
}
if (dispatchServletBeans.isEmpty()) {
return ConditionOutcome.match(message.didNotFind("dispatcher servlet beans").atAll());
}
return ConditionOutcome.match(message.found("dispatcher servlet bean", "dispatcher servlet beans")
.items(Style.QUOTE, dispatchServletBeans)
.append("and none is named " + DEFAULT_DISPATCHER_SERVLET_BEAN_NAME));
}
}
@Order(Ordered.LOWEST_PRECEDENCE - 10)
private static final class DispatcherServletRegistrationCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
ConditionOutcome outcome = checkDefaultDispatcherName(beanFactory);
if (!outcome.isMatch()) {
return outcome;
}
return checkServletRegistration(beanFactory);
}
private ConditionOutcome checkDefaultDispatcherName(ConfigurableListableBeanFactory beanFactory) {
boolean containsDispatcherBean = beanFactory.containsBean(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
if (!containsDispatcherBean) {
return ConditionOutcome.match();
}
List<String> servlets = Arrays
.asList(beanFactory.getBeanNamesForType(DispatcherServlet.class, false, false));
if (!servlets.contains(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)) {
return ConditionOutcome.noMatch(
startMessage().found("non dispatcher servlet").items(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME));
}
return ConditionOutcome.match();
}
private ConditionOutcome checkServletRegistration(ConfigurableListableBeanFactory beanFactory) {
ConditionMessage.Builder message = startMessage();
List<String> registrations = Arrays
.asList(beanFactory.getBeanNamesForType(ServletRegistrationBean.class, false, false));
boolean containsDispatcherRegistrationBean = beanFactory
.containsBean(DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME);
if (registrations.isEmpty()) {
if (containsDispatcherRegistrationBean) {
return ConditionOutcome.noMatch(message.found("non servlet registration bean")
.items(DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME));
}
return ConditionOutcome.match(message.didNotFind("servlet registration bean").atAll());
}
if (registrations.contains(DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)) {
return ConditionOutcome.noMatch(message.found("servlet registration bean")
.items(DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME));
}
if (containsDispatcherRegistrationBean) {
return ConditionOutcome.noMatch(message.found("non servlet registration bean")
.items(DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME));
}
return ConditionOutcome.match(message.found("servlet registration beans")
.items(Style.QUOTE, registrations)
.append("and none is named " + DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME));
}
private ConditionMessage.Builder startMessage() {
return ConditionMessage.forCondition("DispatcherServlet Registration");
}
}
}

View File

@@ -0,0 +1,89 @@
/*
* Copyright 2012-2025 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
*
* https://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.webmvc.autoconfigure;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.web.servlet.DispatcherServlet;
/**
* Interface that can be used by auto-configurations that need path details for the
* {@link DispatcherServletAutoConfiguration#DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME
* default} {@link DispatcherServlet}.
*
* @author Madhura Bhave
* @author Stephane Nicoll
* @since 4.0.0
*/
@FunctionalInterface
public interface DispatcherServletPath {
/**
* Returns the configured path of the dispatcher servlet.
* @return the configured path
*/
String getPath();
/**
* Return a form of the given path that's relative to the dispatcher servlet path.
* @param path the path to make relative
* @return the relative path
*/
default String getRelativePath(String path) {
String prefix = getPrefix();
if (!path.startsWith("/")) {
path = "/" + path;
}
return prefix + path;
}
/**
* Return a cleaned up version of the path that can be used as a prefix for URLs. The
* resulting path will have path will not have a trailing slash.
* @return the prefix
* @see #getRelativePath(String)
*/
default String getPrefix() {
String result = getPath();
int index = result.indexOf('*');
if (index != -1) {
result = result.substring(0, index);
}
if (result.endsWith("/")) {
result = result.substring(0, result.length() - 1);
}
return result;
}
/**
* Return a URL mapping pattern that can be used with a
* {@link ServletRegistrationBean} to map the dispatcher servlet.
* @return the path as a servlet URL mapping
*/
default String getServletUrlMapping() {
if (getPath().isEmpty() || getPath().equals("/")) {
return "/";
}
if (getPath().contains("*")) {
return getPath();
}
if (getPath().endsWith("/")) {
return getPath() + "*";
}
return getPath() + "/*";
}
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright 2012-2025 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
*
* https://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.webmvc.autoconfigure;
import java.util.Collection;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.util.Assert;
import org.springframework.web.servlet.DispatcherServlet;
/**
* {@link ServletRegistrationBean} for the auto-configured {@link DispatcherServlet}. Both
* registers the servlet and exposes {@link DispatcherServletPath} information.
*
* @author Phillip Webb
* @since 4.0.0
*/
public class DispatcherServletRegistrationBean extends ServletRegistrationBean<DispatcherServlet>
implements DispatcherServletPath {
private final String path;
/**
* Create a new {@link DispatcherServletRegistrationBean} instance for the given
* servlet and path.
* @param servlet the dispatcher servlet
* @param path the dispatcher servlet path
*/
public DispatcherServletRegistrationBean(DispatcherServlet servlet, String path) {
super(servlet);
Assert.notNull(path, "'path' must not be null");
this.path = path;
super.addUrlMappings(getServletUrlMapping());
}
@Override
public String getPath() {
return this.path;
}
@Override
public void setUrlMappings(Collection<String> urlMappings) {
throw new UnsupportedOperationException("URL Mapping cannot be changed on a DispatcherServlet registration");
}
@Override
public void addUrlMappings(String... urlMappings) {
throw new UnsupportedOperationException("URL Mapping cannot be changed on a DispatcherServlet registration");
}
}

View File

@@ -0,0 +1,56 @@
/*
* Copyright 2012-2025 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
*
* https://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.webmvc.autoconfigure;
import java.io.File;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.ClassUtils;
/**
* {@link TemplateAvailabilityProvider} that provides availability information for JSP
* view templates.
*
* @author Andy Wilkinson
* @author Stephane Nicoll
* @author Madhura Bhave
* @since 4.0.0
*/
public class JspTemplateAvailabilityProvider implements TemplateAvailabilityProvider {
@Override
public boolean isTemplateAvailable(String view, Environment environment, ClassLoader classLoader,
ResourceLoader resourceLoader) {
if (ClassUtils.isPresent("org.apache.jasper.compiler.JspConfig", classLoader)) {
String resourceName = getResourceName(view, environment);
if (resourceLoader.getResource(resourceName).exists()) {
return true;
}
return new File("src/main/webapp", resourceName).exists();
}
return false;
}
private String getResourceName(String view, Environment environment) {
String prefix = environment.getProperty("spring.mvc.view.prefix", WebMvcAutoConfiguration.DEFAULT_PREFIX);
String suffix = environment.getProperty("spring.mvc.view.suffix", WebMvcAutoConfiguration.DEFAULT_SUFFIX);
return prefix + view + suffix;
}
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2012-2025 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
*
* https://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.webmvc.autoconfigure;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
/**
* {@code @ControllerAdvice} annotated {@link ResponseEntityExceptionHandler} that is
* auto-configured for problem details support.
*
* @author Brian Clozel
*/
@ControllerAdvice
class ProblemDetailsExceptionHandler extends ResponseEntityExceptionHandler {
}

View File

@@ -0,0 +1,659 @@
/*
* Copyright 2012-2025 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
*
* https://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.webmvc.autoconfigure;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import jakarta.servlet.Servlet;
import jakarta.servlet.ServletContext;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureOrder;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingFilterBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders;
import org.springframework.boot.autoconfigure.web.ConditionalOnEnabledResourceChain;
import org.springframework.boot.autoconfigure.web.WebProperties;
import org.springframework.boot.autoconfigure.web.WebProperties.Resources;
import org.springframework.boot.autoconfigure.web.WebProperties.Resources.Chain.Strategy;
import org.springframework.boot.autoconfigure.web.WebResourcesRuntimeHints;
import org.springframework.boot.autoconfigure.web.format.DateTimeFormatters;
import org.springframework.boot.autoconfigure.web.format.WebConversionService;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.convert.ApplicationConversionService;
import org.springframework.boot.http.autoconfigure.HttpMessageConverters;
import org.springframework.boot.validation.autoconfigure.ValidatorAdapter;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.boot.web.servlet.filter.OrderedFormContentFilter;
import org.springframework.boot.web.servlet.filter.OrderedHiddenHttpMethodFilter;
import org.springframework.boot.web.servlet.filter.OrderedRequestContextFilter;
import org.springframework.boot.webmvc.autoconfigure.WebMvcProperties.Format;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportRuntimeHints;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.format.FormatterRegistry;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.validation.DefaultMessageCodesResolver;
import org.springframework.validation.MessageCodesResolver;
import org.springframework.validation.Validator;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer;
import org.springframework.web.context.ServletContextAware;
import org.springframework.web.context.request.RequestContextListener;
import org.springframework.web.context.support.ServletContextResource;
import org.springframework.web.filter.FormContentFilter;
import org.springframework.web.filter.HiddenHttpMethodFilter;
import org.springframework.web.filter.RequestContextFilter;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.FlashMapManager;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.RequestToViewNameTranslator;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer;
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
import org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.ResourceChainRegistration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver;
import org.springframework.web.servlet.handler.AbstractUrlHandlerMapping;
import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;
import org.springframework.web.servlet.i18n.FixedLocaleResolver;
import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import org.springframework.web.servlet.resource.EncodedResourceResolver;
import org.springframework.web.servlet.resource.ResourceResolver;
import org.springframework.web.servlet.resource.ResourceUrlProvider;
import org.springframework.web.servlet.resource.VersionResourceResolver;
import org.springframework.web.servlet.view.BeanNameViewResolver;
import org.springframework.web.servlet.view.ContentNegotiatingViewResolver;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.util.UrlPathHelper;
/**
* {@link EnableAutoConfiguration Auto-configuration} for {@link EnableWebMvc Web MVC}.
*
* @author Phillip Webb
* @author Dave Syer
* @author Andy Wilkinson
* @author Sébastien Deleuze
* @author Eddú Meléndez
* @author Stephane Nicoll
* @author Kristine Jetzke
* @author Bruce Brouwer
* @author Artsiom Yudovin
* @author Scott Frederick
* @since 4.0.0
*/
@AutoConfiguration(after = { DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class },
afterName = "org.springframework.boot.validation.autoconfigure.ValidationAutoConfiguration")
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@ImportRuntimeHints(WebResourcesRuntimeHints.class)
public class WebMvcAutoConfiguration {
/**
* The default Spring MVC view prefix.
*/
public static final String DEFAULT_PREFIX = "";
/**
* The default Spring MVC view suffix.
*/
public static final String DEFAULT_SUFFIX = "";
private static final String SERVLET_LOCATION = "/";
@Bean
@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
@ConditionalOnBooleanProperty("spring.mvc.hiddenmethod.filter.enabled")
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new OrderedHiddenHttpMethodFilter();
}
@Bean
@ConditionalOnMissingBean(FormContentFilter.class)
@ConditionalOnBooleanProperty(name = "spring.mvc.formcontent.filter.enabled", matchIfMissing = true)
public OrderedFormContentFilter formContentFilter() {
return new OrderedFormContentFilter();
}
// Defined as a nested config to ensure WebMvcConfigurer is not read when not
// on the classpath
@Configuration(proxyBeanMethods = false)
@Import(EnableWebMvcConfiguration.class)
@EnableConfigurationProperties({ WebMvcProperties.class, WebProperties.class })
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware {
private static final Log logger = LogFactory.getLog(WebMvcConfigurer.class);
private final Resources resourceProperties;
private final WebMvcProperties mvcProperties;
private final ListableBeanFactory beanFactory;
private final ObjectProvider<HttpMessageConverters> messageConvertersProvider;
private final ObjectProvider<DispatcherServletPath> dispatcherServletPath;
private final ObjectProvider<ServletRegistrationBean<?>> servletRegistrations;
private final ResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer;
private ServletContext servletContext;
public WebMvcAutoConfigurationAdapter(WebProperties webProperties, WebMvcProperties mvcProperties,
ListableBeanFactory beanFactory, ObjectProvider<HttpMessageConverters> messageConvertersProvider,
ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider,
ObjectProvider<DispatcherServletPath> dispatcherServletPath,
ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) {
this.resourceProperties = webProperties.getResources();
this.mvcProperties = mvcProperties;
this.beanFactory = beanFactory;
this.messageConvertersProvider = messageConvertersProvider;
this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
this.dispatcherServletPath = dispatcherServletPath;
this.servletRegistrations = servletRegistrations;
}
@Override
public void setServletContext(ServletContext servletContext) {
this.servletContext = servletContext;
}
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
this.messageConvertersProvider
.ifAvailable((customConverters) -> converters.addAll(customConverters.getConverters()));
}
@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
if (this.beanFactory.containsBean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)) {
Object taskExecutor = this.beanFactory
.getBean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME);
if (taskExecutor instanceof AsyncTaskExecutor asyncTaskExecutor) {
configurer.setTaskExecutor(asyncTaskExecutor);
}
}
Duration timeout = this.mvcProperties.getAsync().getRequestTimeout();
if (timeout != null) {
configurer.setDefaultTimeout(timeout.toMillis());
}
}
@Override
@SuppressWarnings("removal")
public void configurePathMatch(PathMatchConfigurer configurer) {
if (this.mvcProperties.getPathmatch()
.getMatchingStrategy() == WebMvcProperties.MatchingStrategy.ANT_PATH_MATCHER) {
configurer.setPathMatcher(new AntPathMatcher());
this.dispatcherServletPath.ifAvailable((dispatcherPath) -> {
String servletUrlMapping = dispatcherPath.getServletUrlMapping();
if (servletUrlMapping.equals("/") && singleDispatcherServlet()) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
urlPathHelper.setAlwaysUseFullPath(true);
configurer.setUrlPathHelper(urlPathHelper);
}
});
}
}
private boolean singleDispatcherServlet() {
return this.servletRegistrations.stream()
.map(ServletRegistrationBean::getServlet)
.filter(DispatcherServlet.class::isInstance)
.count() == 1;
}
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
WebMvcProperties.Contentnegotiation contentnegotiation = this.mvcProperties.getContentnegotiation();
configurer.favorParameter(contentnegotiation.isFavorParameter());
if (contentnegotiation.getParameterName() != null) {
configurer.parameterName(contentnegotiation.getParameterName());
}
Map<String, MediaType> mediaTypes = contentnegotiation.getMediaTypes();
mediaTypes.forEach(configurer::mediaType);
List<MediaType> defaultContentTypes = contentnegotiation.getDefaultContentTypes();
if (!CollectionUtils.isEmpty(defaultContentTypes)) {
configurer.defaultContentType(defaultContentTypes.toArray(new MediaType[0]));
}
}
@Bean
@ConditionalOnMissingBean
public InternalResourceViewResolver defaultViewResolver() {
InternalResourceViewResolver resolver = new InternalResourceViewResolver();
resolver.setPrefix(this.mvcProperties.getView().getPrefix());
resolver.setSuffix(this.mvcProperties.getView().getSuffix());
return resolver;
}
@Bean
@ConditionalOnBean(View.class)
@ConditionalOnMissingBean
public BeanNameViewResolver beanNameViewResolver() {
BeanNameViewResolver resolver = new BeanNameViewResolver();
resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
return resolver;
}
@Bean
@ConditionalOnBean(ViewResolver.class)
@ConditionalOnMissingBean(name = "viewResolver", value = ContentNegotiatingViewResolver.class)
public ContentNegotiatingViewResolver viewResolver(BeanFactory beanFactory) {
ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver();
resolver.setContentNegotiationManager(beanFactory.getBean(ContentNegotiationManager.class));
// ContentNegotiatingViewResolver uses all the other view resolvers to locate
// a view so it should have a high precedence
resolver.setOrder(Ordered.HIGHEST_PRECEDENCE);
return resolver;
}
@Override
public MessageCodesResolver getMessageCodesResolver() {
if (this.mvcProperties.getMessageCodesResolverFormat() != null) {
DefaultMessageCodesResolver resolver = new DefaultMessageCodesResolver();
resolver.setMessageCodeFormatter(this.mvcProperties.getMessageCodesResolverFormat());
return resolver;
}
return null;
}
@Override
public void addFormatters(FormatterRegistry registry) {
ApplicationConversionService.addBeans(registry, this.beanFactory);
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
return;
}
addResourceHandler(registry, this.mvcProperties.getWebjarsPathPattern(),
"classpath:/META-INF/resources/webjars/");
addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
registration.addResourceLocations(this.resourceProperties.getStaticLocations());
if (this.servletContext != null) {
ServletContextResource resource = new ServletContextResource(this.servletContext, SERVLET_LOCATION);
registration.addResourceLocations(resource);
}
});
}
private void addResourceHandler(ResourceHandlerRegistry registry, String pattern, String... locations) {
addResourceHandler(registry, pattern, (registration) -> registration.addResourceLocations(locations));
}
private void addResourceHandler(ResourceHandlerRegistry registry, String pattern,
Consumer<ResourceHandlerRegistration> customizer) {
if (registry.hasMappingForPattern(pattern)) {
return;
}
ResourceHandlerRegistration registration = registry.addResourceHandler(pattern);
customizer.accept(registration);
registration.setCachePeriod(getSeconds(this.resourceProperties.getCache().getPeriod()));
registration.setCacheControl(this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl());
registration.setUseLastModified(this.resourceProperties.getCache().isUseLastModified());
customizeResourceHandlerRegistration(registration);
}
private Integer getSeconds(Duration cachePeriod) {
return (cachePeriod != null) ? (int) cachePeriod.getSeconds() : null;
}
private void customizeResourceHandlerRegistration(ResourceHandlerRegistration registration) {
if (this.resourceHandlerRegistrationCustomizer != null) {
this.resourceHandlerRegistrationCustomizer.customize(registration);
}
}
@Bean
@ConditionalOnMissingBean({ RequestContextListener.class, RequestContextFilter.class })
@ConditionalOnMissingFilterBean
public static RequestContextFilter requestContextFilter() {
return new OrderedRequestContextFilter();
}
}
/**
* Configuration equivalent to {@code @EnableWebMvc}.
*/
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(WebProperties.class)
public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware {
private final Resources resourceProperties;
private final WebMvcProperties mvcProperties;
private final WebProperties webProperties;
private final ListableBeanFactory beanFactory;
private final WebMvcRegistrations mvcRegistrations;
private ResourceLoader resourceLoader;
public EnableWebMvcConfiguration(WebMvcProperties mvcProperties, WebProperties webProperties,
ObjectProvider<WebMvcRegistrations> mvcRegistrationsProvider,
ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider,
ListableBeanFactory beanFactory) {
this.resourceProperties = webProperties.getResources();
this.mvcProperties = mvcProperties;
this.webProperties = webProperties;
this.mvcRegistrations = mvcRegistrationsProvider.getIfUnique();
this.beanFactory = beanFactory;
}
@Override
protected RequestMappingHandlerAdapter createRequestMappingHandlerAdapter() {
if (this.mvcRegistrations != null) {
RequestMappingHandlerAdapter adapter = this.mvcRegistrations.getRequestMappingHandlerAdapter();
if (adapter != null) {
return adapter;
}
}
return super.createRequestMappingHandlerAdapter();
}
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext,
FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
return createWelcomePageHandlerMapping(applicationContext, mvcConversionService, mvcResourceUrlProvider,
WelcomePageHandlerMapping::new);
}
@Bean
public WelcomePageNotAcceptableHandlerMapping welcomePageNotAcceptableHandlerMapping(
ApplicationContext applicationContext, FormattingConversionService mvcConversionService,
ResourceUrlProvider mvcResourceUrlProvider) {
return createWelcomePageHandlerMapping(applicationContext, mvcConversionService, mvcResourceUrlProvider,
WelcomePageNotAcceptableHandlerMapping::new);
}
private <T extends AbstractUrlHandlerMapping> T createWelcomePageHandlerMapping(
ApplicationContext applicationContext, FormattingConversionService mvcConversionService,
ResourceUrlProvider mvcResourceUrlProvider, WelcomePageHandlerMappingFactory<T> factory) {
TemplateAvailabilityProviders templateAvailabilityProviders = new TemplateAvailabilityProviders(
applicationContext);
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
T handlerMapping = factory.create(templateAvailabilityProviders, applicationContext, getIndexHtmlResource(),
staticPathPattern);
handlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider));
handlerMapping.setCorsConfigurations(getCorsConfigurations());
return handlerMapping;
}
@Override
@Bean
@ConditionalOnMissingBean(name = DispatcherServlet.LOCALE_RESOLVER_BEAN_NAME)
public LocaleResolver localeResolver() {
if (this.webProperties.getLocaleResolver() == WebProperties.LocaleResolver.FIXED) {
return new FixedLocaleResolver(this.webProperties.getLocale());
}
AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
localeResolver.setDefaultLocale(this.webProperties.getLocale());
return localeResolver;
}
@Override
@Bean
@ConditionalOnMissingBean(name = DispatcherServlet.FLASH_MAP_MANAGER_BEAN_NAME)
public FlashMapManager flashMapManager() {
return super.flashMapManager();
}
@Override
@Bean
@ConditionalOnMissingBean(name = DispatcherServlet.REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME)
public RequestToViewNameTranslator viewNameTranslator() {
return super.viewNameTranslator();
}
private Resource getIndexHtmlResource() {
for (String location : this.resourceProperties.getStaticLocations()) {
Resource indexHtml = getIndexHtmlResource(location);
if (indexHtml != null) {
return indexHtml;
}
}
ServletContext servletContext = getServletContext();
if (servletContext != null) {
return getIndexHtmlResource(new ServletContextResource(servletContext, SERVLET_LOCATION));
}
return null;
}
private Resource getIndexHtmlResource(String location) {
return getIndexHtmlResource(this.resourceLoader.getResource(location));
}
private Resource getIndexHtmlResource(Resource location) {
try {
Resource resource = location.createRelative("index.html");
if (resource.exists() && (resource.getURL() != null)) {
return resource;
}
}
catch (Exception ex) {
// Ignore
}
return null;
}
@Bean
@Override
public FormattingConversionService mvcConversionService() {
Format format = this.mvcProperties.getFormat();
WebConversionService conversionService = new WebConversionService(
new DateTimeFormatters().dateFormat(format.getDate())
.timeFormat(format.getTime())
.dateTimeFormat(format.getDateTime()));
addFormatters(conversionService);
return conversionService;
}
@Bean
@Override
public Validator mvcValidator() {
if (!ClassUtils.isPresent("jakarta.validation.Validator", getClass().getClassLoader())
|| !ClassUtils.isPresent("org.springframework.boot.validation.autoconfigure.ValidatorAdapter",
getClass().getClassLoader())) {
return super.mvcValidator();
}
return ValidatorAdapter.get(getApplicationContext(), getValidator());
}
@Override
protected RequestMappingHandlerMapping createRequestMappingHandlerMapping() {
if (this.mvcRegistrations != null) {
RequestMappingHandlerMapping mapping = this.mvcRegistrations.getRequestMappingHandlerMapping();
if (mapping != null) {
return mapping;
}
}
return super.createRequestMappingHandlerMapping();
}
@Override
protected ConfigurableWebBindingInitializer getConfigurableWebBindingInitializer(
FormattingConversionService mvcConversionService, Validator mvcValidator) {
try {
return this.beanFactory.getBean(ConfigurableWebBindingInitializer.class);
}
catch (NoSuchBeanDefinitionException ex) {
return super.getConfigurableWebBindingInitializer(mvcConversionService, mvcValidator);
}
}
@Override
protected ExceptionHandlerExceptionResolver createExceptionHandlerExceptionResolver() {
if (this.mvcRegistrations != null) {
ExceptionHandlerExceptionResolver resolver = this.mvcRegistrations
.getExceptionHandlerExceptionResolver();
if (resolver != null) {
return resolver;
}
}
return super.createExceptionHandlerExceptionResolver();
}
@Override
protected void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {
super.extendHandlerExceptionResolvers(exceptionResolvers);
if (this.mvcProperties.isLogResolvedException()) {
for (HandlerExceptionResolver resolver : exceptionResolvers) {
if (resolver instanceof AbstractHandlerExceptionResolver abstractResolver) {
abstractResolver.setWarnLogCategory(resolver.getClass().getName());
}
}
}
}
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnEnabledResourceChain
static class ResourceChainCustomizerConfiguration {
@Bean
ResourceChainResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer(
WebProperties webProperties) {
return new ResourceChainResourceHandlerRegistrationCustomizer(webProperties.getResources());
}
}
@FunctionalInterface
interface WelcomePageHandlerMappingFactory<T extends AbstractUrlHandlerMapping> {
T create(TemplateAvailabilityProviders templateAvailabilityProviders, ApplicationContext applicationContext,
Resource indexHtmlResource, String staticPathPattern);
}
@FunctionalInterface
interface ResourceHandlerRegistrationCustomizer {
void customize(ResourceHandlerRegistration registration);
}
static class ResourceChainResourceHandlerRegistrationCustomizer implements ResourceHandlerRegistrationCustomizer {
private final Resources resourceProperties;
ResourceChainResourceHandlerRegistrationCustomizer(Resources resourceProperties) {
this.resourceProperties = resourceProperties;
}
@Override
public void customize(ResourceHandlerRegistration registration) {
Resources.Chain properties = this.resourceProperties.getChain();
configureResourceChain(properties, registration.resourceChain(properties.isCache()));
}
private void configureResourceChain(Resources.Chain properties, ResourceChainRegistration chain) {
Strategy strategy = properties.getStrategy();
if (properties.isCompressed()) {
chain.addResolver(new EncodedResourceResolver());
}
if (strategy.getFixed().isEnabled() || strategy.getContent().isEnabled()) {
chain.addResolver(getVersionResourceResolver(strategy));
}
}
private ResourceResolver getVersionResourceResolver(Strategy properties) {
VersionResourceResolver resolver = new VersionResourceResolver();
if (properties.getFixed().isEnabled()) {
String version = properties.getFixed().getVersion();
String[] paths = properties.getFixed().getPaths();
resolver.addFixedVersionStrategy(version, paths);
}
if (properties.getContent().isEnabled()) {
String[] paths = properties.getContent().getPaths();
resolver.addContentVersionStrategy(paths);
}
return resolver;
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnBooleanProperty("spring.mvc.problemdetails.enabled")
static class ProblemDetailsErrorHandlingConfiguration {
@Bean
@ConditionalOnMissingBean(ResponseEntityExceptionHandler.class)
@Order(0)
ProblemDetailsExceptionHandler problemDetailsExceptionHandler() {
return new ProblemDetailsExceptionHandler();
}
}
}

View File

@@ -0,0 +1,464 @@
/*
* Copyright 2012-2025 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
*
* https://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.webmvc.autoconfigure;
import java.time.Duration;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.http.MediaType;
import org.springframework.util.Assert;
import org.springframework.validation.DefaultMessageCodesResolver;
/**
* {@link ConfigurationProperties Properties} for Spring MVC.
*
* @author Phillip Webb
* @author Sébastien Deleuze
* @author Stephane Nicoll
* @author Eddú Meléndez
* @author Brian Clozel
* @author Vedran Pavic
* @since 4.0.0
*/
@ConfigurationProperties("spring.mvc")
public class WebMvcProperties {
/**
* Formatting strategy for message codes. For instance, 'PREFIX_ERROR_CODE'.
*/
private DefaultMessageCodesResolver.Format messageCodesResolverFormat;
private final Format format = new Format();
/**
* Whether to dispatch TRACE requests to the FrameworkServlet doService method.
*/
private boolean dispatchTraceRequest = false;
/**
* Whether to dispatch OPTIONS requests to the FrameworkServlet doService method.
*/
private boolean dispatchOptionsRequest = true;
/**
* Whether to publish a ServletRequestHandledEvent at the end of each request.
*/
private boolean publishRequestHandledEvents = true;
/**
* Whether logging of (potentially sensitive) request details at DEBUG and TRACE level
* is allowed.
*/
private boolean logRequestDetails;
/**
* Whether to enable warn logging of exceptions resolved by a
* "HandlerExceptionResolver", except for "DefaultHandlerExceptionResolver".
*/
private boolean logResolvedException = false;
/**
* Path pattern used for static resources.
*/
private String staticPathPattern = "/**";
/**
* Path pattern used for WebJar assets.
*/
private String webjarsPathPattern = "/webjars/**";
private final Async async = new Async();
private final Servlet servlet = new Servlet();
private final View view = new View();
private final Contentnegotiation contentnegotiation = new Contentnegotiation();
private final Pathmatch pathmatch = new Pathmatch();
private final Problemdetails problemdetails = new Problemdetails();
public DefaultMessageCodesResolver.Format getMessageCodesResolverFormat() {
return this.messageCodesResolverFormat;
}
public void setMessageCodesResolverFormat(DefaultMessageCodesResolver.Format messageCodesResolverFormat) {
this.messageCodesResolverFormat = messageCodesResolverFormat;
}
public Format getFormat() {
return this.format;
}
public boolean isPublishRequestHandledEvents() {
return this.publishRequestHandledEvents;
}
public void setPublishRequestHandledEvents(boolean publishRequestHandledEvents) {
this.publishRequestHandledEvents = publishRequestHandledEvents;
}
public boolean isLogRequestDetails() {
return this.logRequestDetails;
}
public void setLogRequestDetails(boolean logRequestDetails) {
this.logRequestDetails = logRequestDetails;
}
public boolean isLogResolvedException() {
return this.logResolvedException;
}
public void setLogResolvedException(boolean logResolvedException) {
this.logResolvedException = logResolvedException;
}
public boolean isDispatchOptionsRequest() {
return this.dispatchOptionsRequest;
}
public void setDispatchOptionsRequest(boolean dispatchOptionsRequest) {
this.dispatchOptionsRequest = dispatchOptionsRequest;
}
public boolean isDispatchTraceRequest() {
return this.dispatchTraceRequest;
}
public void setDispatchTraceRequest(boolean dispatchTraceRequest) {
this.dispatchTraceRequest = dispatchTraceRequest;
}
public String getStaticPathPattern() {
return this.staticPathPattern;
}
public void setStaticPathPattern(String staticPathPattern) {
this.staticPathPattern = staticPathPattern;
}
public String getWebjarsPathPattern() {
return this.webjarsPathPattern;
}
public void setWebjarsPathPattern(String webjarsPathPattern) {
this.webjarsPathPattern = webjarsPathPattern;
}
public Async getAsync() {
return this.async;
}
public Servlet getServlet() {
return this.servlet;
}
public View getView() {
return this.view;
}
public Contentnegotiation getContentnegotiation() {
return this.contentnegotiation;
}
public Pathmatch getPathmatch() {
return this.pathmatch;
}
public Problemdetails getProblemdetails() {
return this.problemdetails;
}
public static class Async {
/**
* Amount of time before asynchronous request handling times out. If this value is
* not set, the default timeout of the underlying implementation is used.
*/
private Duration requestTimeout;
public Duration getRequestTimeout() {
return this.requestTimeout;
}
public void setRequestTimeout(Duration requestTimeout) {
this.requestTimeout = requestTimeout;
}
}
public static class Servlet {
/**
* Path of the dispatcher servlet. Setting a custom value for this property is not
* compatible with the PathPatternParser matching strategy.
*/
private String path = "/";
/**
* Load on startup priority of the dispatcher servlet.
*/
private int loadOnStartup = -1;
public String getPath() {
return this.path;
}
public void setPath(String path) {
Assert.notNull(path, "'path' must not be null");
Assert.isTrue(!path.contains("*"), "'path' must not contain wildcards");
this.path = path;
}
public int getLoadOnStartup() {
return this.loadOnStartup;
}
public void setLoadOnStartup(int loadOnStartup) {
this.loadOnStartup = loadOnStartup;
}
public String getServletMapping() {
if (this.path.isEmpty() || this.path.equals("/")) {
return "/";
}
if (this.path.endsWith("/")) {
return this.path + "*";
}
return this.path + "/*";
}
public String getPath(String path) {
String prefix = getServletPrefix();
if (!path.startsWith("/")) {
path = "/" + path;
}
return prefix + path;
}
public String getServletPrefix() {
String result = this.path;
int index = result.indexOf('*');
if (index != -1) {
result = result.substring(0, index);
}
if (result.endsWith("/")) {
result = result.substring(0, result.length() - 1);
}
return result;
}
}
public static class View {
/**
* Spring MVC view prefix.
*/
private String prefix;
/**
* Spring MVC view suffix.
*/
private String suffix;
public String getPrefix() {
return this.prefix;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
public String getSuffix() {
return this.suffix;
}
public void setSuffix(String suffix) {
this.suffix = suffix;
}
}
public static class Contentnegotiation {
/**
* Whether a request parameter ("format" by default) should be used to determine
* the requested media type.
*/
private boolean favorParameter = false;
/**
* Query parameter name to use when "favor-parameter" is enabled.
*/
private String parameterName;
/**
* Map file extensions to media types for content negotiation. For instance, yml
* to text/yaml.
*/
private Map<String, MediaType> mediaTypes = new LinkedHashMap<>();
/**
* List of default content types to be used when no specific content type is
* requested.
*/
private List<MediaType> defaultContentTypes = new ArrayList<>();
public boolean isFavorParameter() {
return this.favorParameter;
}
public void setFavorParameter(boolean favorParameter) {
this.favorParameter = favorParameter;
}
public String getParameterName() {
return this.parameterName;
}
public void setParameterName(String parameterName) {
this.parameterName = parameterName;
}
public Map<String, MediaType> getMediaTypes() {
return this.mediaTypes;
}
public void setMediaTypes(Map<String, MediaType> mediaTypes) {
this.mediaTypes = mediaTypes;
}
public List<MediaType> getDefaultContentTypes() {
return this.defaultContentTypes;
}
public void setDefaultContentTypes(List<MediaType> defaultContentTypes) {
this.defaultContentTypes = defaultContentTypes;
}
}
public static class Pathmatch {
/**
* Choice of strategy for matching request paths against registered mappings.
*/
private MatchingStrategy matchingStrategy = MatchingStrategy.PATH_PATTERN_PARSER;
public MatchingStrategy getMatchingStrategy() {
return this.matchingStrategy;
}
public void setMatchingStrategy(MatchingStrategy matchingStrategy) {
this.matchingStrategy = matchingStrategy;
}
}
public static class Format {
/**
* Date format to use, for example 'dd/MM/yyyy'. Used for formatting of
* java.util.Date and java.time.LocalDate.
*/
private String date;
/**
* Time format to use, for example 'HH:mm:ss'. Used for formatting of java.time's
* LocalTime and OffsetTime.
*/
private String time;
/**
* Date-time format to use, for example 'yyyy-MM-dd HH:mm:ss'. Used for formatting
* of java.time's LocalDateTime, OffsetDateTime, and ZonedDateTime.
*/
private String dateTime;
public String getDate() {
return this.date;
}
public void setDate(String date) {
this.date = date;
}
public String getTime() {
return this.time;
}
public void setTime(String time) {
this.time = time;
}
public String getDateTime() {
return this.dateTime;
}
public void setDateTime(String dateTime) {
this.dateTime = dateTime;
}
}
/**
* Matching strategy options.
*
* @since 2.4.0
*/
public enum MatchingStrategy {
/**
* Use the {@code AntPathMatcher} implementation.
* @deprecated since 4.0.0 for removal in 4.2.0 in favor of
* {@link #PATH_PATTERN_PARSER}
*/
@Deprecated(since = "4.0.0", forRemoval = true)
ANT_PATH_MATCHER,
/**
* Use the {@code PathPatternParser} implementation.
*/
PATH_PATTERN_PARSER
}
public static class Problemdetails {
/**
* Whether RFC 9457 Problem Details support should be enabled.
*/
private boolean enabled = false;
public boolean isEnabled() {
return this.enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
}
}

View File

@@ -0,0 +1,69 @@
/*
* Copyright 2012-2025 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
*
* https://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.webmvc.autoconfigure;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
/**
* Interface to register key components of the {@link WebMvcConfigurationSupport} in place
* of the default ones provided by Spring MVC.
* <p>
* All custom instances are later processed by Boot and Spring MVC configurations. To
* participate in, and if desired, override that subsequent processing,
* {@link WebMvcConfigurer} should be used.
* <p>
* A single instance of this component should be registered, otherwise making it
* impossible to choose from redundant MVC components.
*
* @author Brian Clozel
* @since 4.0.0
* @see org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration.EnableWebMvcConfiguration
*/
public interface WebMvcRegistrations {
/**
* Return the custom {@link RequestMappingHandlerMapping} that should be used and
* processed by the MVC configuration.
* @return the custom {@link RequestMappingHandlerMapping} instance
*/
default RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
return null;
}
/**
* Return the custom {@link RequestMappingHandlerAdapter} that should be used and
* processed by the MVC configuration.
* @return the custom {@link RequestMappingHandlerAdapter} instance
*/
default RequestMappingHandlerAdapter getRequestMappingHandlerAdapter() {
return null;
}
/**
* Return the custom {@link ExceptionHandlerExceptionResolver} that should be used and
* processed by the MVC configuration.
* @return the custom {@link ExceptionHandlerExceptionResolver} instance
*/
default ExceptionHandlerExceptionResolver getExceptionHandlerExceptionResolver() {
return null;
}
}

View File

@@ -0,0 +1,79 @@
/*
* Copyright 2012-2025 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
*
* https://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.webmvc.autoconfigure;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders;
import org.springframework.context.ApplicationContext;
import org.springframework.core.io.Resource;
/**
* Details for a welcome page resolved from a resource or a template.
*
* @author Phillip Webb
*/
final class WelcomePage {
/**
* Value used for an unresolved welcome page.
*/
static final WelcomePage UNRESOLVED = new WelcomePage(null, false);
private final String viewName;
private final boolean templated;
private WelcomePage(String viewName, boolean templated) {
this.viewName = viewName;
this.templated = templated;
}
/**
* Return the view name of the welcome page.
* @return the view name
*/
String getViewName() {
return this.viewName;
}
/**
* Return if the welcome page is from a template.
* @return if the welcome page is templated
*/
boolean isTemplated() {
return this.templated;
}
/**
* Resolve the {@link WelcomePage} to use.
* @param templateAvailabilityProviders the template availability providers
* @param applicationContext the application context
* @param indexHtmlResource the index HTML resource to use or {@code null}
* @param staticPathPattern the static path pattern being used
* @return a resolved {@link WelcomePage} instance or {@link #UNRESOLVED}
*/
static WelcomePage resolve(TemplateAvailabilityProviders templateAvailabilityProviders,
ApplicationContext applicationContext, Resource indexHtmlResource, String staticPathPattern) {
if (indexHtmlResource != null && "/**".equals(staticPathPattern)) {
return new WelcomePage("forward:index.html", false);
}
if (templateAvailabilityProviders.getProvider("index", applicationContext) != null) {
return new WelcomePage("index", true);
}
return UNRESOLVED;
}
}

View File

@@ -0,0 +1,95 @@
/*
* Copyright 2012-2025 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
*
* https://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.webmvc.autoconfigure;
import java.util.Collections;
import java.util.List;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders;
import org.springframework.context.ApplicationContext;
import org.springframework.core.io.Resource;
import org.springframework.core.log.LogMessage;
import org.springframework.http.HttpHeaders;
import org.springframework.http.InvalidMediaTypeException;
import org.springframework.http.MediaType;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.handler.AbstractUrlHandlerMapping;
import org.springframework.web.servlet.mvc.ParameterizableViewController;
/**
* An {@link AbstractUrlHandlerMapping} for an application's HTML welcome page. Supports
* both static and templated files. If both a static and templated index page are
* available, the static page is preferred.
*
* @author Andy Wilkinson
* @author Bruce Brouwer
* @author Moritz Halbritter
* @see WelcomePageNotAcceptableHandlerMapping
*/
final class WelcomePageHandlerMapping extends AbstractUrlHandlerMapping {
private static final Log logger = LogFactory.getLog(WelcomePageHandlerMapping.class);
private static final List<MediaType> MEDIA_TYPES_ALL = Collections.singletonList(MediaType.ALL);
WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders,
ApplicationContext applicationContext, Resource indexHtmlResource, String staticPathPattern) {
setOrder(2);
WelcomePage welcomePage = WelcomePage.resolve(templateAvailabilityProviders, applicationContext,
indexHtmlResource, staticPathPattern);
if (welcomePage != WelcomePage.UNRESOLVED) {
logger.info(LogMessage.of(() -> (!welcomePage.isTemplated()) ? "Adding welcome page: " + indexHtmlResource
: "Adding welcome page template: index"));
ParameterizableViewController controller = new ParameterizableViewController();
controller.setViewName(welcomePage.getViewName());
setRootHandler(controller);
}
}
@Override
public Object getHandlerInternal(HttpServletRequest request) throws Exception {
return (!isHtmlTextAccepted(request)) ? null : super.getHandlerInternal(request);
}
private boolean isHtmlTextAccepted(HttpServletRequest request) {
for (MediaType mediaType : getAcceptedMediaTypes(request)) {
if (mediaType.includes(MediaType.TEXT_HTML)) {
return true;
}
}
return false;
}
private List<MediaType> getAcceptedMediaTypes(HttpServletRequest request) {
String acceptHeader = request.getHeader(HttpHeaders.ACCEPT);
if (StringUtils.hasText(acceptHeader)) {
try {
return MediaType.parseMediaTypes(acceptHeader);
}
catch (InvalidMediaTypeException ex) {
logger.warn("Received invalid Accept header. Assuming all media types are accepted",
logger.isDebugEnabled() ? ex : null);
}
}
return MEDIA_TYPES_ALL;
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2012-2025 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
*
* https://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.webmvc.autoconfigure;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders;
import org.springframework.context.ApplicationContext;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpStatus;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.AbstractUrlHandlerMapping;
import org.springframework.web.servlet.mvc.Controller;
/**
* An {@link AbstractUrlHandlerMapping} for an application's welcome page that was
* ultimately not accepted.
*
* @author Phillip Webb
*/
class WelcomePageNotAcceptableHandlerMapping extends AbstractUrlHandlerMapping {
WelcomePageNotAcceptableHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders,
ApplicationContext applicationContext, Resource indexHtmlResource, String staticPathPattern) {
setOrder(LOWEST_PRECEDENCE - 10); // Before ResourceHandlerRegistry
WelcomePage welcomePage = WelcomePage.resolve(templateAvailabilityProviders, applicationContext,
indexHtmlResource, staticPathPattern);
if (welcomePage != WelcomePage.UNRESOLVED) {
setRootHandler((Controller) this::handleRequest);
}
}
private ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) {
response.setStatus(HttpStatus.NOT_ACCEPTABLE.value());
return null;
}
@Override
protected Object getHandlerInternal(HttpServletRequest request) throws Exception {
return super.getHandlerInternal(request);
}
}

View File

@@ -0,0 +1,156 @@
/*
* Copyright 2012-2025 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
*
* https://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.webmvc.autoconfigure.error;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.webmvc.error.ErrorAttributes;
import org.springframework.boot.webmvc.error.ErrorController;
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.ServletWebRequest;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.ModelAndView;
/**
* Abstract base class for error {@link Controller @Controller} implementations.
*
* @author Dave Syer
* @author Phillip Webb
* @author Scott Frederick
* @author Moritz Halbritter
* @since 4.0.0
* @see ErrorAttributes
*/
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<>();
if (resolvers != null) {
sorted.addAll(resolvers);
AnnotationAwareOrderComparator.sortIfNecessary(sorted);
}
return sorted;
}
protected Map<String, Object> getErrorAttributes(HttpServletRequest request, ErrorAttributeOptions options) {
WebRequest webRequest = new ServletWebRequest(request);
return this.errorAttributes.getErrorAttributes(webRequest, options);
}
/**
* Returns whether the trace parameter is set.
* @param request the request
* @return whether the trace parameter is set
*/
protected boolean getTraceParameter(HttpServletRequest request) {
return getBooleanParameter(request, "trace");
}
/**
* Returns whether the message parameter is set.
* @param request the request
* @return whether the message parameter is set
*/
protected boolean getMessageParameter(HttpServletRequest request) {
return getBooleanParameter(request, "message");
}
/**
* Returns whether the errors parameter is set.
* @param request the request
* @return whether the errors parameter is set
*/
protected boolean getErrorsParameter(HttpServletRequest request) {
return getBooleanParameter(request, "errors");
}
/**
* Returns whether the path parameter is set.
* @param request the request
* @return whether the path parameter is set
*/
protected boolean getPathParameter(HttpServletRequest request) {
return getBooleanParameter(request, "path");
}
protected boolean getBooleanParameter(HttpServletRequest request, String parameterName) {
String parameter = request.getParameter(parameterName);
if (parameter == null) {
return false;
}
return !"false".equalsIgnoreCase(parameter);
}
protected HttpStatus getStatus(HttpServletRequest request) {
Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
if (statusCode == null) {
return HttpStatus.INTERNAL_SERVER_ERROR;
}
try {
return HttpStatus.valueOf(statusCode);
}
catch (Exception ex) {
return HttpStatus.INTERNAL_SERVER_ERROR;
}
}
/**
* 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
*/
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;
}
}

View File

@@ -0,0 +1,193 @@
/*
* Copyright 2012-2025 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
*
* https://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.webmvc.autoconfigure.error;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.boot.autoconfigure.web.ErrorProperties;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.error.ErrorAttributeOptions.Include;
import org.springframework.boot.web.server.servlet.ConfigurableServletWebServerFactory;
import org.springframework.boot.webmvc.error.ErrorAttributes;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.util.Assert;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
/**
* Basic global error {@link Controller @Controller}, rendering {@link ErrorAttributes}.
* More specific errors can be handled either using Spring MVC abstractions (e.g.
* {@code @ExceptionHandler}) or by adding servlet
* {@link ConfigurableServletWebServerFactory#setErrorPages server error pages}.
*
* @author Dave Syer
* @author Phillip Webb
* @author Michael Stummvoll
* @author Stephane Nicoll
* @author Scott Frederick
* @author Moritz Halbritter
* @since 4.0.0
* @see ErrorAttributes
* @see ErrorProperties
*/
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
private final ErrorProperties errorProperties;
/**
* Create a new {@link BasicErrorController} instance.
* @param errorAttributes the error attributes
* @param errorProperties configuration properties
*/
public BasicErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties) {
this(errorAttributes, errorProperties, Collections.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;
}
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}
@ExceptionHandler(HttpMediaTypeNotAcceptableException.class)
public ResponseEntity<String> mediaTypeNotAcceptable(HttpServletRequest request) {
HttpStatus status = getStatus(request);
return ResponseEntity.status(status).build();
}
protected ErrorAttributeOptions getErrorAttributeOptions(HttpServletRequest request, MediaType mediaType) {
ErrorAttributeOptions options = ErrorAttributeOptions.defaults();
if (this.errorProperties.isIncludeException()) {
options = options.including(Include.EXCEPTION);
}
if (isIncludeStackTrace(request, mediaType)) {
options = options.including(Include.STACK_TRACE);
}
if (isIncludeMessage(request, mediaType)) {
options = options.including(Include.MESSAGE);
}
if (isIncludeBindingErrors(request, mediaType)) {
options = options.including(Include.BINDING_ERRORS);
}
options = isIncludePath(request, mediaType) ? options.including(Include.PATH) : options.excluding(Include.PATH);
return options;
}
/**
* Determine if the stacktrace attribute should be included.
* @param request the source request
* @param produces the media type produced (or {@code MediaType.ALL})
* @return if the stacktrace attribute should be included
*/
protected boolean isIncludeStackTrace(HttpServletRequest request, MediaType produces) {
return switch (getErrorProperties().getIncludeStacktrace()) {
case ALWAYS -> true;
case ON_PARAM -> getTraceParameter(request);
case NEVER -> false;
};
}
/**
* Determine if the message attribute should be included.
* @param request the source request
* @param produces the media type produced (or {@code MediaType.ALL})
* @return if the message attribute should be included
*/
protected boolean isIncludeMessage(HttpServletRequest request, MediaType produces) {
return switch (getErrorProperties().getIncludeMessage()) {
case ALWAYS -> true;
case ON_PARAM -> getMessageParameter(request);
case NEVER -> false;
};
}
/**
* Determine if the errors attribute should be included.
* @param request the source request
* @param produces the media type produced (or {@code MediaType.ALL})
* @return if the errors attribute should be included
*/
protected boolean isIncludeBindingErrors(HttpServletRequest request, MediaType produces) {
return switch (getErrorProperties().getIncludeBindingErrors()) {
case ALWAYS -> true;
case ON_PARAM -> getErrorsParameter(request);
case NEVER -> false;
};
}
/**
* Determine if the path attribute should be included.
* @param request the source request
* @param produces the media type produced (or {@code MediaType.ALL})
* @return if the path attribute should be included
*/
protected boolean isIncludePath(HttpServletRequest request, MediaType produces) {
return switch (getErrorProperties().getIncludePath()) {
case ALWAYS -> true;
case ON_PARAM -> getPathParameter(request);
case NEVER -> false;
};
}
/**
* Provide access to the error properties.
* @return the error properties
*/
protected ErrorProperties getErrorProperties() {
return this.errorProperties;
}
}

View File

@@ -0,0 +1,168 @@
/*
* Copyright 2012-2025 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
*
* https://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.webmvc.autoconfigure.error;
import java.util.Collections;
import java.util.EnumMap;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders;
import org.springframework.boot.autoconfigure.web.WebProperties.Resources;
import org.springframework.context.ApplicationContext;
import org.springframework.core.Ordered;
import org.springframework.core.io.Resource;
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
* @author Andy Wilkinson
* @since 4.0.0
*/
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
private static final Map<Series, String> SERIES_VIEWS;
static {
Map<Series, String> views = new EnumMap<>(Series.class);
views.put(Series.CLIENT_ERROR, "4xx");
views.put(Series.SERVER_ERROR, "5xx");
SERIES_VIEWS = Collections.unmodifiableMap(views);
}
private final ApplicationContext applicationContext;
private final Resources resources;
private final TemplateAvailabilityProviders templateAvailabilityProviders;
private int order = Ordered.LOWEST_PRECEDENCE;
/**
* Create a new {@link DefaultErrorViewResolver} instance.
* @param applicationContext the source application context
* @param resources resource properties
*/
public DefaultErrorViewResolver(ApplicationContext applicationContext, Resources resources) {
Assert.notNull(applicationContext, "'applicationContext' must not be null");
Assert.notNull(resources, "'resources' must not be null");
this.applicationContext = applicationContext;
this.resources = resources;
this.templateAvailabilityProviders = new TemplateAvailabilityProviders(applicationContext);
}
DefaultErrorViewResolver(ApplicationContext applicationContext, Resources resourceProperties,
TemplateAvailabilityProviders templateAvailabilityProviders) {
Assert.notNull(applicationContext, "ApplicationContext must not be null");
Assert.notNull(resourceProperties, "Resources must not be null");
this.applicationContext = applicationContext;
this.resources = resourceProperties;
this.templateAvailabilityProviders = templateAvailabilityProviders;
}
@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
ModelAndView modelAndView = resolve(String.valueOf(status.value()), 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) {
String errorViewName = "error/" + viewName;
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
this.applicationContext);
if (provider != null) {
return new ModelAndView(errorViewName, model);
}
return resolveResource(errorViewName, model);
}
private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
for (String location : this.resources.getStaticLocations()) {
try {
Resource resource = this.applicationContext.getResource(location);
resource = resource.createRelative(viewName + ".html");
if (resource.exists()) {
return new ModelAndView(new HtmlResourceView(resource), model);
}
}
catch (Exception ex) {
// Ignore
}
}
return null;
}
@Override
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
/**
* {@link View} backed by an HTML resource.
*/
private static class HtmlResourceView implements View {
private final 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 {
response.setContentType(getContentType());
FileCopyUtils.copy(this.resource.getInputStream(), response.getOutputStream());
}
}
}

View File

@@ -0,0 +1,305 @@
/*
* Copyright 2012-2025 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
*
* https://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.webmvc.autoconfigure.error;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import jakarta.servlet.Servlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.framework.autoproxy.AutoProxyUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
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.autoconfigure.template.TemplateAvailabilityProviders;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.autoconfigure.web.WebProperties;
import org.springframework.boot.autoconfigure.web.WebProperties.Resources;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.server.ErrorPage;
import org.springframework.boot.web.server.ErrorPageRegistrar;
import org.springframework.boot.web.server.ErrorPageRegistry;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.webmvc.autoconfigure.DispatcherServletPath;
import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration;
import org.springframework.boot.webmvc.autoconfigure.WebMvcProperties;
import org.springframework.boot.webmvc.error.DefaultErrorAttributes;
import org.springframework.boot.webmvc.error.ErrorAttributes;
import org.springframework.boot.webmvc.error.ErrorController;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.view.BeanNameViewResolver;
import org.springframework.web.util.HtmlUtils;
/**
* {@link EnableAutoConfiguration Auto-configuration} to render errors through an MVC
* error controller.
*
* @author Dave Syer
* @author Andy Wilkinson
* @author Stephane Nicoll
* @author Brian Clozel
* @author Scott Frederick
* @since 4.0.0
*/
// Load before the main WebMvcAutoConfiguration so that the error View is available
@AutoConfiguration(before = WebMvcAutoConfiguration.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class })
@EnableConfigurationProperties({ ServerProperties.class, WebMvcProperties.class })
public class ErrorMvcAutoConfiguration {
private final ServerProperties serverProperties;
public ErrorMvcAutoConfiguration(ServerProperties serverProperties) {
this.serverProperties = serverProperties;
}
@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes();
}
@Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,
ObjectProvider<ErrorViewResolver> errorViewResolvers) {
return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
errorViewResolvers.orderedStream().toList());
}
@Bean
public ErrorPageCustomizer errorPageCustomizer(DispatcherServletPath dispatcherServletPath) {
return new ErrorPageCustomizer(this.serverProperties, dispatcherServletPath);
}
@Bean
public static PreserveErrorControllerTargetClassPostProcessor preserveErrorControllerTargetClassPostProcessor() {
return new PreserveErrorControllerTargetClassPostProcessor();
}
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties({ WebProperties.class, WebMvcProperties.class })
static class DefaultErrorViewResolverConfiguration {
private final ApplicationContext applicationContext;
private final Resources resources;
DefaultErrorViewResolverConfiguration(ApplicationContext applicationContext, WebProperties webProperties) {
this.applicationContext = applicationContext;
this.resources = webProperties.getResources();
}
@Bean
@ConditionalOnBean(DispatcherServlet.class)
@ConditionalOnMissingBean(ErrorViewResolver.class)
DefaultErrorViewResolver conventionErrorViewResolver() {
return new DefaultErrorViewResolver(this.applicationContext, this.resources);
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnBooleanProperty(name = "server.error.whitelabel.enabled", matchIfMissing = true)
@Conditional(ErrorTemplateMissingCondition.class)
protected static class WhitelabelErrorViewConfiguration {
private final StaticView defaultErrorView = new StaticView();
@Bean(name = "error")
@ConditionalOnMissingBean(name = "error")
public View defaultErrorView() {
return this.defaultErrorView;
}
// If the user adds @EnableWebMvc then the bean name view resolver from
// WebMvcAutoConfiguration disappears, so add it back in to avoid disappointment.
@Bean
@ConditionalOnMissingBean
public BeanNameViewResolver beanNameViewResolver() {
BeanNameViewResolver resolver = new BeanNameViewResolver();
resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
return resolver;
}
}
/**
* {@link SpringBootCondition} that matches when no error template view is detected.
*/
private static final class ErrorTemplateMissingCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
ConditionMessage.Builder message = ConditionMessage.forCondition("ErrorTemplate Missing");
TemplateAvailabilityProviders providers = new TemplateAvailabilityProviders(context.getClassLoader());
TemplateAvailabilityProvider provider = providers.getProvider("error", context.getEnvironment(),
context.getClassLoader(), context.getResourceLoader());
if (provider != null) {
return ConditionOutcome.noMatch(message.foundExactly("template from " + provider));
}
return ConditionOutcome.match(message.didNotFind("error template view").atAll());
}
}
/**
* Simple {@link View} implementation that writes a default HTML error page.
*/
private static final class StaticView implements View {
private static final MediaType TEXT_HTML_UTF8 = new MediaType("text", "html", StandardCharsets.UTF_8);
private static final Log logger = LogFactory.getLog(StaticView.class);
@Override
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
throws Exception {
if (response.isCommitted()) {
String message = getMessage(model);
logger.error(message);
return;
}
response.setContentType(TEXT_HTML_UTF8.toString());
StringBuilder builder = new StringBuilder();
Object timestamp = model.get("timestamp");
Object message = model.get("message");
Object trace = model.get("trace");
if (response.getContentType() == null) {
response.setContentType(getContentType());
}
builder.append("<html><body><h1>Whitelabel Error Page</h1>")
.append("<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>")
.append("<div id='created'>")
.append(timestamp)
.append("</div>")
.append("<div>There was an unexpected error (type=")
.append(htmlEscape(model.get("error")))
.append(", status=")
.append(htmlEscape(model.get("status")))
.append(").</div>");
if (message != null) {
builder.append("<div>").append(htmlEscape(message)).append("</div>");
}
if (trace != null) {
builder.append("<div style='white-space:pre-wrap;'>").append(htmlEscape(trace)).append("</div>");
}
builder.append("</body></html>");
response.getWriter().append(builder.toString());
}
private String htmlEscape(Object input) {
return (input != null) ? HtmlUtils.htmlEscape(input.toString()) : null;
}
private String getMessage(Map<String, ?> model) {
Object path = model.get("path");
String message = "Cannot render error page for request [" + path + "]";
if (model.get("message") != null) {
message += " and exception [" + model.get("message") + "]";
}
message += " as the response has already been committed.";
message += " As a result, the response may have the wrong status code.";
return message;
}
@Override
public String getContentType() {
return "text/html";
}
}
/**
* {@link WebServerFactoryCustomizer} that configures the server's error pages.
*/
static class ErrorPageCustomizer implements ErrorPageRegistrar, Ordered {
private final ServerProperties properties;
private final DispatcherServletPath dispatcherServletPath;
protected ErrorPageCustomizer(ServerProperties properties, DispatcherServletPath dispatcherServletPath) {
this.properties = properties;
this.dispatcherServletPath = dispatcherServletPath;
}
@Override
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
ErrorPage errorPage = new ErrorPage(
this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
errorPageRegistry.addErrorPages(errorPage);
}
@Override
public int getOrder() {
return 0;
}
}
/**
* {@link BeanFactoryPostProcessor} to ensure that the target class of ErrorController
* MVC beans are preserved when using AOP.
*/
static class PreserveErrorControllerTargetClassPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
String[] errorControllerBeans = beanFactory.getBeanNamesForType(ErrorController.class, false, false);
for (String errorControllerBean : errorControllerBeans) {
try {
beanFactory.getBeanDefinition(errorControllerBean)
.setAttribute(AutoProxyUtils.PRESERVE_TARGET_CLASS_ATTRIBUTE, Boolean.TRUE);
}
catch (Throwable ex) {
// Ignore
}
}
}
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2012-2025 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
*
* https://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.webmvc.autoconfigure.error;
import java.util.Map;
import jakarta.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 4.0.0
*/
@FunctionalInterface
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);
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2025 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
*
* https://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.
*/
/**
* Auto-configuration for Spring MVC error handling.
*/
package org.springframework.boot.webmvc.autoconfigure.error;

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2025 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
*
* https://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.
*/
/**
* Auto-configuration for Spring MVC.
*/
package org.springframework.boot.webmvc.autoconfigure;

View File

@@ -0,0 +1,237 @@
/*
* Copyright 2012-2025 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
*
* https://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.webmvc.error;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.boot.web.error.Error;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.error.ErrorAttributeOptions.Include;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindingResult;
import org.springframework.validation.method.MethodValidationResult;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.WebRequest;
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 (if configured)</li>
* <li>message - The exception message (if configured)</li>
* <li>errors - Any validation errors wrapped in {@link Error}, derived from a
* {@link BindingResult} or {@link MethodValidationResult} exception (if configured)</li>
* <li>trace - The exception stack trace (if configured)</li>
* <li>path - The URL path when the exception was raised</li>
* </ul>
*
* @author Phillip Webb
* @author Dave Syer
* @author Stephane Nicoll
* @author Vedran Pavic
* @author Scott Frederick
* @author Moritz Halbritter
* @author Yanming Zhou
* @author Yongjun Hong
* @since 4.0.0
* @see ErrorAttributes
*/
@Order(Ordered.HIGHEST_PRECEDENCE)
public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered {
private static final String ERROR_INTERNAL_ATTRIBUTE = DefaultErrorAttributes.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_INTERNAL_ATTRIBUTE, ex);
}
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
Map<String, Object> errorAttributes = getErrorAttributes(webRequest, options.isIncluded(Include.STACK_TRACE));
options.retainIncluded(errorAttributes);
return errorAttributes;
}
private Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> errorAttributes = new LinkedHashMap<>();
errorAttributes.put("timestamp", new Date());
addStatus(errorAttributes, webRequest);
addErrorDetails(errorAttributes, webRequest, includeStackTrace);
addPath(errorAttributes, webRequest);
return errorAttributes;
}
private void addStatus(Map<String, Object> errorAttributes, RequestAttributes requestAttributes) {
Integer status = getAttribute(requestAttributes, RequestDispatcher.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, WebRequest webRequest,
boolean includeStackTrace) {
Throwable error = getError(webRequest);
if (error != null) {
while (error instanceof ServletException && error.getCause() != null) {
error = error.getCause();
}
errorAttributes.put("exception", error.getClass().getName());
if (includeStackTrace) {
addStackTrace(errorAttributes, error);
}
}
addErrorMessage(errorAttributes, webRequest, error);
}
private void addErrorMessage(Map<String, Object> errorAttributes, WebRequest webRequest, Throwable error) {
BindingResult bindingResult = extractBindingResult(error);
if (bindingResult != null) {
addMessageAndErrorsFromBindingResult(errorAttributes, bindingResult);
return;
}
MethodValidationResult methodValidationResult = extractMethodValidationResult(error);
if (methodValidationResult != null) {
addMessageAndErrorsFromMethodValidationResult(errorAttributes, methodValidationResult);
return;
}
addExceptionErrorMessage(errorAttributes, webRequest, error);
}
private void addMessageAndErrorsFromBindingResult(Map<String, Object> errorAttributes, BindingResult result) {
errorAttributes.put("message", "Validation failed for object='%s'. Error count: %s"
.formatted(result.getObjectName(), result.getAllErrors().size()));
errorAttributes.put("errors", Error.wrap(result.getAllErrors()));
}
private void addMessageAndErrorsFromMethodValidationResult(Map<String, Object> errorAttributes,
MethodValidationResult result) {
errorAttributes.put("message", "Validation failed for method='%s'. Error count: %s"
.formatted(result.getMethod(), result.getAllErrors().size()));
errorAttributes.put("errors", Error.wrap(result.getAllErrors()));
}
private void addExceptionErrorMessage(Map<String, Object> errorAttributes, WebRequest webRequest, Throwable error) {
errorAttributes.put("message", getMessage(webRequest, error));
}
/**
* Returns the message to be included as the value of the {@code message} error
* attribute. By default the returned message is the first of the following that is
* not empty:
* <ol>
* <li>Value of the {@link RequestDispatcher#ERROR_MESSAGE} request attribute.
* <li>Message of the given {@code error}.
* <li>{@code No message available}.
* </ol>
* @param webRequest current request
* @param error current error, if any
* @return message to include in the error attributes
*/
protected String getMessage(WebRequest webRequest, Throwable error) {
Object message = getAttribute(webRequest, RequestDispatcher.ERROR_MESSAGE);
if (!ObjectUtils.isEmpty(message)) {
return message.toString();
}
if (error != null && StringUtils.hasLength(error.getMessage())) {
return error.getMessage();
}
return "No message available";
}
private BindingResult extractBindingResult(Throwable error) {
if (error instanceof BindingResult bindingResult) {
return bindingResult;
}
return null;
}
private MethodValidationResult extractMethodValidationResult(Throwable error) {
if (error instanceof MethodValidationResult methodValidationResult) {
return methodValidationResult;
}
return null;
}
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, RequestDispatcher.ERROR_REQUEST_URI);
if (path != null) {
errorAttributes.put("path", path);
}
}
@Override
public Throwable getError(WebRequest webRequest) {
Throwable exception = getAttribute(webRequest, ERROR_INTERNAL_ATTRIBUTE);
if (exception == null) {
exception = getAttribute(webRequest, RequestDispatcher.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,57 @@
/*
* Copyright 2012-2025 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
*
* https://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.webmvc.error;
import java.util.Collections;
import java.util.Map;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.ModelAndView;
/**
* Provides access to error attributes which can be logged or presented to the user.
*
* @author Phillip Webb
* @author Scott Frederick
* @since 4.0.0
* @see DefaultErrorAttributes
*/
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 @ResponseBody}.
* @param webRequest the source request
* @param options options for error attribute contents
* @return a map of error attributes
*/
default Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
return Collections.emptyMap();
}
/**
* Return the underlying cause of the error or {@code null} if the error cannot be
* extracted.
* @param webRequest the source request
* @return the {@link Exception} that caused the error or {@code null}
*/
Throwable getError(WebRequest webRequest);
}

View File

@@ -0,0 +1,31 @@
/*
* Copyright 2012-2025 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
*
* https://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.webmvc.error;
import org.springframework.stereotype.Controller;
/**
* Marker interface used to identify a {@link Controller @Controller} that should be used
* to render errors.
*
* @author Phillip Webb
* @author Scott Frederick
* @since 4.0.0
*/
public interface ErrorController {
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2025 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
*
* https://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.
*/
/**
* Spring MVC error handling infrastructure.
*/
package org.springframework.boot.webmvc.error;

View File

@@ -0,0 +1,156 @@
{
"groups": [],
"properties": [
{
"name": "spring.mvc.date-format",
"type": "java.lang.String",
"description": "Date format to use, for example 'dd/MM/yyyy'.",
"deprecation": {
"level": "error"
}
},
{
"name": "spring.mvc.favicon.enabled",
"type": "java.lang.Boolean",
"description": "Whether to enable resolution of favicon.ico.",
"deprecation": {
"level": "error"
}
},
{
"name": "spring.mvc.formcontent.filter.enabled",
"type": "java.lang.Boolean",
"description": "Whether to enable Spring's FormContentFilter.",
"defaultValue": true
},
{
"name": "spring.mvc.formcontent.putfilter.enabled",
"type": "java.lang.Boolean",
"description": "Whether to enable Spring's HttpPutFormContentFilter.",
"defaultValue": true,
"deprecation": {
"replacement": "spring.mvc.formcontent.filter.enabled",
"level": "error"
}
},
{
"name": "spring.mvc.hiddenmethod.filter.enabled",
"type": "java.lang.Boolean",
"description": "Whether to enable Spring's HiddenHttpMethodFilter.",
"defaultValue": false
},
{
"name": "spring.mvc.ignore-default-model-on-redirect",
"deprecation": {
"reason": "Deprecated for removal in Spring MVC.",
"level": "error"
}
},
{
"name": "spring.mvc.locale",
"type": "java.util.Locale",
"deprecation": {
"replacement": "spring.web.locale",
"level": "error"
}
},
{
"name": "spring.mvc.locale-resolver",
"type": "org.springframework.boot.autoconfigure.web.WebProperties$LocaleResolver",
"deprecation": {
"replacement": "spring.web.locale-resolver",
"level": "error"
}
},
{
"name": "spring.mvc.throw-exception-if-no-handler-found",
"deprecation": {
"reason": "DispatcherServlet property is deprecated for removal and should no longer need to be configured.",
"level": "error"
}
}
],
"hints": [
{
"name": "spring.mvc.converters.preferred-json-mapper",
"values": [
{
"value": "gson"
},
{
"value": "jackson"
},
{
"value": "jsonb"
}
],
"providers": [
{
"name": "any"
}
]
},
{
"name": "spring.mvc.format.date",
"values": [
{
"value": "dd/MM/yyyy",
"description": "Example date format. Any format supported by DateTimeFormatter.parse can be used."
},
{
"value": "iso",
"description": "ISO-8601 extended local date format."
}
],
"providers": [
{
"name": "any"
}
]
},
{
"name": "spring.mvc.format.date-time",
"values": [
{
"value": "yyyy-MM-dd HH:mm:ss",
"description": "Example date-time format. Any format supported by DateTimeFormatter.parse can be used."
},
{
"value": "iso",
"description": "ISO-8601 extended local date-time format."
},
{
"value": "iso-offset",
"description": "ISO offset date-time format."
}
],
"providers": [
{
"name": "any"
}
]
},
{
"name": "spring.mvc.format.time",
"values": [
{
"value": "HH:mm:ss",
"description": "Example time format. Any format supported by DateTimeFormatter.parse can be used."
},
{
"value": "iso",
"description": "ISO-8601 extended local time format."
},
{
"value": "iso-offset",
"description": "ISO offset time format."
}
],
"providers": [
{
"name": "any"
}
]
}
]
}

View File

@@ -0,0 +1,3 @@
# Template Availability Providers
org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider=\
org.springframework.boot.webmvc.autoconfigure.JspTemplateAvailabilityProvider

View File

@@ -0,0 +1,3 @@
org.springframework.boot.webmvc.autoconfigure.DispatcherServletAutoConfiguration
org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration
org.springframework.boot.webmvc.autoconfigure.error.ErrorMvcAutoConfiguration

View File

@@ -0,0 +1,288 @@
/*
* Copyright 2012-2025 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
*
* https://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.webmvc.autoconfigure;
import jakarta.servlet.MultipartConfigElement;
import jakarta.servlet.http.HttpServletRequest;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.boot.web.servlet.MultipartConfigFactory;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.util.unit.DataSize;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.servlet.DispatcherServlet;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link DispatcherServletAutoConfiguration}.
*
* @author Dave Syer
* @author Andy Wilkinson
* @author Brian Clozel
*/
class DispatcherServletAutoConfigurationTests {
private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(DispatcherServletAutoConfiguration.class));
@Test
void registrationProperties() {
this.contextRunner.run((context) -> {
assertThat(context.getBean(DispatcherServlet.class)).isNotNull();
ServletRegistrationBean<?> registration = context.getBean(ServletRegistrationBean.class);
assertThat(registration.getUrlMappings()).containsExactly("/");
});
}
@Test
void registrationNonServletBean() {
this.contextRunner.withUserConfiguration(NonServletConfiguration.class).run((context) -> {
assertThat(context).doesNotHaveBean(ServletRegistrationBean.class);
assertThat(context).doesNotHaveBean(DispatcherServlet.class);
assertThat(context).doesNotHaveBean(DispatcherServletPath.class);
});
}
@Test
void registrationOverrideWithDefaultDispatcherServletNameResultsInSingleDispatcherServlet() {
this.contextRunner.withUserConfiguration(CustomDispatcherServletSameName.class).run((context) -> {
ServletRegistrationBean<?> registration = context.getBean(ServletRegistrationBean.class);
assertThat(registration.getUrlMappings()).containsExactly("/");
assertThat(registration.getServletName()).isEqualTo("dispatcherServlet");
assertThat(context).getBeanNames(DispatcherServlet.class).hasSize(1);
});
}
@Test
void registrationOverrideWithNonDefaultDispatcherServletNameResultsInTwoDispatcherServlets() {
this.contextRunner
.withUserConfiguration(CustomDispatcherServletDifferentName.class, CustomDispatcherServletPath.class)
.run((context) -> {
ServletRegistrationBean<?> registration = context.getBean(ServletRegistrationBean.class);
assertThat(registration.getUrlMappings()).containsExactly("/");
assertThat(registration.getServletName()).isEqualTo("dispatcherServlet");
assertThat(context).getBeanNames(DispatcherServlet.class).hasSize(2);
});
}
@Test
void registrationOverrideWithAutowiredServlet() {
this.contextRunner.withUserConfiguration(CustomAutowiredRegistration.class).run((context) -> {
ServletRegistrationBean<?> registration = context.getBean(ServletRegistrationBean.class);
assertThat(registration.getUrlMappings()).containsExactly("/foo");
assertThat(registration.getServletName()).isEqualTo("customDispatcher");
assertThat(context).hasSingleBean(DispatcherServlet.class);
});
}
@Test
void servletPath() {
this.contextRunner.withPropertyValues("spring.mvc.servlet.path:/spring").run((context) -> {
assertThat(context.getBean(DispatcherServlet.class)).isNotNull();
ServletRegistrationBean<?> registration = context.getBean(ServletRegistrationBean.class);
assertThat(registration.getUrlMappings()).containsExactly("/spring/*");
assertThat(registration.getMultipartConfig()).isNull();
assertThat(context.getBean(DispatcherServletPath.class).getPath()).isEqualTo("/spring");
});
}
@Test
void dispatcherServletPathWhenCustomDispatcherServletSameNameShouldReturnConfiguredServletPath() {
this.contextRunner.withUserConfiguration(CustomDispatcherServletSameName.class)
.withPropertyValues("spring.mvc.servlet.path:/spring")
.run((context) -> assertThat(context.getBean(DispatcherServletPath.class).getPath()).isEqualTo("/spring"));
}
@Test
void dispatcherServletPathNotCreatedWhenDefaultDispatcherServletNotAvailable() {
this.contextRunner
.withUserConfiguration(CustomDispatcherServletDifferentName.class, NonServletConfiguration.class)
.run((context) -> assertThat(context).doesNotHaveBean(DispatcherServletPath.class));
}
@Test
void dispatcherServletPathNotCreatedWhenCustomRegistrationBeanPresent() {
this.contextRunner.withUserConfiguration(CustomDispatcherServletRegistration.class)
.run((context) -> assertThat(context).doesNotHaveBean(DispatcherServletPath.class));
}
@Test
void multipartConfig() {
this.contextRunner.withUserConfiguration(MultipartConfiguration.class).run((context) -> {
ServletRegistrationBean<?> registration = context.getBean(ServletRegistrationBean.class);
assertThat(registration.getMultipartConfig()).isNotNull();
});
}
@Test
void renamesMultipartResolver() {
this.contextRunner.withUserConfiguration(MultipartResolverConfiguration.class).run((context) -> {
DispatcherServlet dispatcherServlet = context.getBean(DispatcherServlet.class);
dispatcherServlet.onApplicationEvent(new ContextRefreshedEvent(context));
assertThat(dispatcherServlet.getMultipartResolver()).isInstanceOf(MockMultipartResolver.class);
});
}
@Test
void dispatcherServletDefaultConfig() {
this.contextRunner.run((context) -> {
DispatcherServlet dispatcherServlet = context.getBean(DispatcherServlet.class);
assertThat(dispatcherServlet).extracting("dispatchOptionsRequest").isEqualTo(true);
assertThat(dispatcherServlet).extracting("dispatchTraceRequest").isEqualTo(false);
assertThat(dispatcherServlet).extracting("enableLoggingRequestDetails").isEqualTo(false);
assertThat(dispatcherServlet).extracting("publishEvents").isEqualTo(true);
assertThat(context.getBean("dispatcherServletRegistration")).hasFieldOrPropertyWithValue("loadOnStartup",
-1);
});
}
@Test
void dispatcherServletCustomConfig() {
this.contextRunner
.withPropertyValues("spring.mvc.dispatch-options-request:false", "spring.mvc.dispatch-trace-request:true",
"spring.mvc.publish-request-handled-events:false", "spring.mvc.servlet.load-on-startup=5")
.run((context) -> {
DispatcherServlet dispatcherServlet = context.getBean(DispatcherServlet.class);
assertThat(dispatcherServlet).extracting("dispatchOptionsRequest").isEqualTo(false);
assertThat(dispatcherServlet).extracting("dispatchTraceRequest").isEqualTo(true);
assertThat(dispatcherServlet).extracting("publishEvents").isEqualTo(false);
assertThat(context.getBean("dispatcherServletRegistration"))
.hasFieldOrPropertyWithValue("loadOnStartup", 5);
});
}
@Configuration(proxyBeanMethods = false)
static class MultipartConfiguration {
@Bean
MultipartConfigElement multipartConfig() {
MultipartConfigFactory factory = new MultipartConfigFactory();
factory.setMaxFileSize(DataSize.ofKilobytes(128));
factory.setMaxRequestSize(DataSize.ofKilobytes(128));
return factory.createMultipartConfig();
}
}
@Configuration(proxyBeanMethods = false)
static class CustomDispatcherServletDifferentName {
@Bean
DispatcherServlet customDispatcherServlet() {
return new DispatcherServlet();
}
}
@Configuration(proxyBeanMethods = false)
static class CustomDispatcherServletPath {
@Bean
DispatcherServletPath dispatcherServletPath() {
return mock(DispatcherServletPath.class);
}
}
@Configuration(proxyBeanMethods = false)
static class CustomAutowiredRegistration {
@Bean
ServletRegistrationBean<?> dispatcherServletRegistration(DispatcherServlet dispatcherServlet) {
ServletRegistrationBean<DispatcherServlet> registration = new ServletRegistrationBean<>(dispatcherServlet,
"/foo");
registration.setName("customDispatcher");
return registration;
}
@Bean
DispatcherServletPath dispatcherServletPath() {
return mock(DispatcherServletPath.class);
}
}
@Configuration(proxyBeanMethods = false)
static class NonServletConfiguration {
@Bean
String dispatcherServlet() {
return "spring";
}
}
@Configuration(proxyBeanMethods = false)
static class MultipartResolverConfiguration {
@Bean
MultipartResolver getMultipartResolver() {
return new MockMultipartResolver();
}
}
@Configuration(proxyBeanMethods = false)
static class CustomDispatcherServletSameName {
@Bean(name = DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
DispatcherServlet dispatcherServlet() {
return new DispatcherServlet();
}
}
@Configuration(proxyBeanMethods = false)
static class CustomDispatcherServletRegistration {
@Bean(name = DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
ServletRegistrationBean<DispatcherServlet> dispatcherServletRegistration(DispatcherServlet dispatcherServlet) {
ServletRegistrationBean<DispatcherServlet> registration = new ServletRegistrationBean<>(dispatcherServlet,
"/foo");
registration.setName("customDispatcher");
return registration;
}
}
static class MockMultipartResolver implements MultipartResolver {
@Override
public boolean isMultipart(HttpServletRequest request) {
return false;
}
@Override
public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) {
return null;
}
@Override
public void cleanupMultipart(MultipartHttpServletRequest request) {
}
}
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright 2012-2025 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
*
* https://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.webmvc.autoconfigure;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link DispatcherServletPath}.
*
* @author Phillip Webb
*/
class DispatcherServletPathTests {
@Test
void getRelativePathReturnsRelativePath() {
assertThat(((DispatcherServletPath) () -> "spring").getRelativePath("boot")).isEqualTo("spring/boot");
assertThat(((DispatcherServletPath) () -> "spring/").getRelativePath("boot")).isEqualTo("spring/boot");
assertThat(((DispatcherServletPath) () -> "spring").getRelativePath("/boot")).isEqualTo("spring/boot");
}
@Test
void getPrefixWhenHasSimplePathReturnPath() {
assertThat(((DispatcherServletPath) () -> "spring").getPrefix()).isEqualTo("spring");
}
@Test
void getPrefixWhenHasPatternRemovesPattern() {
assertThat(((DispatcherServletPath) () -> "spring/*.do").getPrefix()).isEqualTo("spring");
}
@Test
void getPathWhenPathEndsWithSlashRemovesSlash() {
assertThat(((DispatcherServletPath) () -> "spring/").getPrefix()).isEqualTo("spring");
}
@Test
void getServletUrlMappingWhenPathIsEmptyReturnsSlash() {
assertThat(((DispatcherServletPath) () -> "").getServletUrlMapping()).isEqualTo("/");
}
@Test
void getServletUrlMappingWhenPathIsSlashReturnsSlash() {
assertThat(((DispatcherServletPath) () -> "/").getServletUrlMapping()).isEqualTo("/");
}
@Test
void getServletUrlMappingWhenPathContainsStarReturnsPath() {
assertThat(((DispatcherServletPath) () -> "spring/*.do").getServletUrlMapping()).isEqualTo("spring/*.do");
}
@Test
void getServletUrlMappingWhenHasPathNotEndingSlashReturnsSlashStarPattern() {
assertThat(((DispatcherServletPath) () -> "spring/boot").getServletUrlMapping()).isEqualTo("spring/boot/*");
}
@Test
void getServletUrlMappingWhenHasPathEndingWithSlashReturnsSlashStarPattern() {
assertThat(((DispatcherServletPath) () -> "spring/boot/").getServletUrlMapping()).isEqualTo("spring/boot/*");
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2012-2025 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
*
* https://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.webmvc.autoconfigure;
import java.util.Collections;
import org.junit.jupiter.api.Test;
import org.springframework.web.servlet.DispatcherServlet;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link DispatcherServletRegistrationBean}.
*
* @author Phillip Webb
*/
class DispatcherServletRegistrationBeanTests {
@Test
void createWhenPathIsNullThrowsException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new DispatcherServletRegistrationBean(new DispatcherServlet(), null))
.withMessageContaining("'path' must not be null");
}
@Test
void getPathReturnsPath() {
DispatcherServletRegistrationBean bean = new DispatcherServletRegistrationBean(new DispatcherServlet(),
"/test");
assertThat(bean.getPath()).isEqualTo("/test");
}
@Test
void getUrlMappingsReturnsSinglePathMappedPattern() {
DispatcherServletRegistrationBean bean = new DispatcherServletRegistrationBean(new DispatcherServlet(),
"/test");
assertThat(bean.getUrlMappings()).containsOnly("/test/*");
}
@Test
void setUrlMappingsCannotBeCalled() {
DispatcherServletRegistrationBean bean = new DispatcherServletRegistrationBean(new DispatcherServlet(),
"/test");
assertThatExceptionOfType(UnsupportedOperationException.class)
.isThrownBy(() -> bean.setUrlMappings(Collections.emptyList()));
}
@Test
void addUrlMappingsCannotBeCalled() {
DispatcherServletRegistrationBean bean = new DispatcherServletRegistrationBean(new DispatcherServlet(),
"/test");
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> bean.addUrlMappings("/test"));
}
}

View File

@@ -0,0 +1,65 @@
/*
* Copyright 2012-2025 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
*
* https://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.webmvc.autoconfigure;
import org.junit.jupiter.api.Test;
import org.springframework.boot.testsupport.classpath.resources.WithResource;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.ResourceLoader;
import org.springframework.mock.env.MockEnvironment;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link JspTemplateAvailabilityProvider}.
*
* @author Yunkun Huang
*/
class JspTemplateAvailabilityProviderTests {
private final JspTemplateAvailabilityProvider provider = new JspTemplateAvailabilityProvider();
private final ResourceLoader resourceLoader = new DefaultResourceLoader();
private final MockEnvironment environment = new MockEnvironment();
@Test
void availabilityOfTemplateThatDoesNotExist() {
assertThat(isTemplateAvailable("whatever")).isFalse();
}
@Test
@WithResource(name = "custom-templates/custom.jsp")
void availabilityOfTemplateWithCustomPrefix() {
this.environment.setProperty("spring.mvc.view.prefix", "classpath:/custom-templates/");
assertThat(isTemplateAvailable("custom.jsp")).isTrue();
}
@Test
@WithResource(name = "suffixed.java-server-pages")
void availabilityOfTemplateWithCustomSuffix() {
this.environment.setProperty("spring.mvc.view.suffix", ".java-server-pages");
assertThat(isTemplateAvailable("suffixed")).isTrue();
}
private boolean isTemplateAvailable(String view) {
return this.provider.isTemplateAvailable(view, this.environment, getClass().getClassLoader(),
this.resourceLoader);
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright 2012-2025 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
*
* https://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.webmvc.autoconfigure;
import java.util.Collections;
import java.util.Map;
import org.assertj.core.util.Throwables;
import org.junit.jupiter.api.Test;
import org.springframework.boot.context.properties.bind.BindException;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Tests for {@link WebMvcProperties}.
*
* @author Stephane Nicoll
*/
class WebMvcPropertiesTests {
private final WebMvcProperties properties = new WebMvcProperties();
@Test
void servletPathWhenEndsWithSlashHasValidMappingAndPrefix() {
bind("spring.mvc.servlet.path", "/foo/");
assertThat(this.properties.getServlet().getServletMapping()).isEqualTo("/foo/*");
assertThat(this.properties.getServlet().getServletPrefix()).isEqualTo("/foo");
}
@Test
void servletPathWhenDoesNotEndWithSlashHasValidMappingAndPrefix() {
bind("spring.mvc.servlet.path", "/foo");
assertThat(this.properties.getServlet().getServletMapping()).isEqualTo("/foo/*");
assertThat(this.properties.getServlet().getServletPrefix()).isEqualTo("/foo");
}
@Test
void servletPathWhenHasWildcardThrowsException() {
assertThatExceptionOfType(BindException.class).isThrownBy(() -> bind("spring.mvc.servlet.path", "/*"))
.withRootCauseInstanceOf(IllegalArgumentException.class)
.satisfies((ex) -> assertThat(Throwables.getRootCause(ex)).hasMessage("'path' must not contain wildcards"));
}
private void bind(String name, String value) {
bind(Collections.singletonMap(name, value));
}
private void bind(Map<String, String> map) {
ConfigurationPropertySource source = new MapConfigurationPropertySource(map);
new Binder(source).bind("spring.mvc", Bindable.ofInstance(this.properties));
}
}

View File

@@ -0,0 +1,219 @@
/*
* Copyright 2012-2025 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
*
* https://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.webmvc.autoconfigure;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.assertj.core.api.ThrowingConsumer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders;
import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext;
import org.springframework.boot.test.context.runner.ContextConsumer;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.view.AbstractView;
import org.springframework.web.servlet.view.InternalResourceView;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link WelcomePageHandlerMapping}.
*
* @author Andy Wilkinson
* @author Moritz Halbritter
*/
@ExtendWith(OutputCaptureExtension.class)
class WelcomePageHandlerMappingTests {
private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
.withUserConfiguration(HandlerMappingConfiguration.class)
.withConfiguration(AutoConfigurations.of(PropertyPlaceholderAutoConfiguration.class));
@Test
void isOrderedAtLowPriority() {
this.contextRunner.withUserConfiguration(StaticResourceConfiguration.class).run((context) -> {
WelcomePageHandlerMapping handler = context.getBean(WelcomePageHandlerMapping.class);
assertThat(handler.getOrder()).isEqualTo(2);
});
}
@Test
void handlesRequestForStaticPageThatAcceptsTextHtml() {
this.contextRunner.withUserConfiguration(StaticResourceConfiguration.class)
.run(testWith((mvc) -> assertThat(mvc.get().uri("/").accept(MediaType.TEXT_HTML)).hasStatusOk()
.hasForwardedUrl("index.html")));
}
@Test
void handlesRequestForStaticPageThatAcceptsAll() {
this.contextRunner.withUserConfiguration(StaticResourceConfiguration.class)
.run(testWith((mvc) -> assertThat(mvc.get().uri("/").accept(MediaType.ALL)).hasStatusOk()
.hasForwardedUrl("index.html")));
}
@Test
void doesNotHandleRequestThatDoesNotAcceptTextHtml() {
this.contextRunner.withUserConfiguration(StaticResourceConfiguration.class)
.run(testWith((mvc) -> assertThat(mvc.get().uri("/").accept(MediaType.APPLICATION_JSON))
.hasStatus(HttpStatus.NOT_FOUND)));
}
@Test
void handlesRequestWithNoAcceptHeader() {
this.contextRunner.withUserConfiguration(StaticResourceConfiguration.class)
.run(testWith((mvc) -> assertThat(mvc.get().uri("/")).hasStatusOk().hasForwardedUrl("index.html")));
}
@Test
void handlesRequestWithEmptyAcceptHeader() {
this.contextRunner.withUserConfiguration(StaticResourceConfiguration.class)
.run(testWith((mvc) -> assertThat(mvc.get().uri("/").header(HttpHeaders.ACCEPT, "")).hasStatusOk()
.hasForwardedUrl("index.html")));
}
@Test
void rootHandlerIsNotRegisteredWhenStaticPathPatternIsNotSlashStarStar() {
this.contextRunner.withUserConfiguration(StaticResourceConfiguration.class)
.withPropertyValues("static-path-pattern=/foo/**")
.run((context) -> assertThat(context.getBean(WelcomePageHandlerMapping.class).getRootHandler()).isNull());
}
@Test
void producesNotFoundResponseWhenThereIsNoWelcomePage() {
this.contextRunner.run(testWith(
(mvc) -> assertThat(mvc.get().uri("/").accept(MediaType.TEXT_HTML)).hasStatus(HttpStatus.NOT_FOUND)));
}
@Test
void handlesRequestForTemplateThatAcceptsTextHtml() {
this.contextRunner.withUserConfiguration(TemplateConfiguration.class)
.run(testWith((mvc) -> assertThat(mvc.get().uri("/").accept(MediaType.TEXT_HTML)).hasStatusOk()
.hasBodyTextEqualTo("index template")));
}
@Test
void handlesRequestForTemplateThatAcceptsAll() {
this.contextRunner.withUserConfiguration(TemplateConfiguration.class)
.run(testWith((mvc) -> assertThat(mvc.get().uri("/").accept(MediaType.ALL)).hasStatusOk()
.hasBodyTextEqualTo("index template")));
}
@Test
void prefersAStaticResourceToATemplate() {
this.contextRunner.withUserConfiguration(StaticResourceConfiguration.class, TemplateConfiguration.class)
.run(testWith((mvc) -> assertThat(mvc.get().uri("/").accept(MediaType.ALL)).hasStatusOk()
.hasForwardedUrl("index.html")));
}
@Test
void logsInvalidAcceptHeader(CapturedOutput output) {
this.contextRunner.withUserConfiguration(TemplateConfiguration.class)
.run(testWith((mvc) -> assertThat(mvc.get().uri("/").accept("*/*q=0.8")).hasStatusOk()
.hasBodyTextEqualTo("index template")));
assertThat(output).contains("Received invalid Accept header. Assuming all media types are accepted");
}
private ContextConsumer<AssertableWebApplicationContext> testWith(ThrowingConsumer<MockMvcTester> mvc) {
return (context) -> mvc.accept(MockMvcTester.from(context));
}
@Configuration(proxyBeanMethods = false)
static class HandlerMappingConfiguration {
@Bean
WelcomePageHandlerMapping handlerMapping(ApplicationContext applicationContext,
ObjectProvider<TemplateAvailabilityProviders> templateAvailabilityProviders,
ObjectProvider<Resource> staticIndexPage,
@Value("${static-path-pattern:/**}") String staticPathPattern) {
return new WelcomePageHandlerMapping(
templateAvailabilityProviders
.getIfAvailable(() -> new TemplateAvailabilityProviders(applicationContext)),
applicationContext, staticIndexPage.getIfAvailable(), staticPathPattern);
}
}
@Configuration(proxyBeanMethods = false)
static class StaticResourceConfiguration {
@Bean
Resource staticIndexPage() {
return new ByteArrayResource("welcome-page-static".getBytes(StandardCharsets.UTF_8));
}
}
@Configuration(proxyBeanMethods = false)
static class TemplateConfiguration {
@Bean
TemplateAvailabilityProviders templateAvailabilityProviders() {
return new TestTemplateAvailabilityProviders(
(view, environment, classLoader, resourceLoader) -> view.equals("index"));
}
@Bean
ViewResolver viewResolver() {
return (name, locale) -> {
if (name.startsWith("forward:")) {
return new InternalResourceView(name.substring("forward:".length()));
}
return new AbstractView() {
@Override
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
response.getWriter().print(name + " template");
}
};
};
}
}
static class TestTemplateAvailabilityProviders extends TemplateAvailabilityProviders {
TestTemplateAvailabilityProviders(TemplateAvailabilityProvider provider) {
super(Collections.singletonList(provider));
}
}
}

View File

@@ -0,0 +1,87 @@
/*
* Copyright 2012-2025 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
*
* https://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.webmvc.autoconfigure;
import java.net.URI;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.http.autoconfigure.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.boot.thymeleaf.autoconfigure.ThymeleafAutoConfiguration;
import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration;
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;
import org.springframework.http.ResponseEntity;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for the welcome page.
*
* @author Madhura Bhave
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, properties = {
"spring.web.resources.chain.strategy.content.enabled=true",
"spring.thymeleaf.prefix=classpath:/org/springframework/boot/webmvc/autoconfigure/",
"spring.web.resources.static-locations=classpath:/org/springframework/boot/webmvc/autoconfigure/static" })
class WelcomePageIntegrationTests {
@LocalServerPort
private int port;
private final TestRestTemplate template = new TestRestTemplate();
@Test
void contentStrategyWithWelcomePage() throws Exception {
RequestEntity<?> entity = RequestEntity.get(new URI("http://localhost:" + this.port + "/"))
.header("Accept", MediaType.ALL.toString())
.build();
ResponseEntity<String> content = this.template.exchange(entity, String.class);
assertThat(content.getBody()).contains("/custom-");
assertThat(content.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@Test
void notAcceptableWelcomePage() throws Exception {
RequestEntity<?> entity = RequestEntity.get(new URI("http://localhost:" + this.port + "/"))
.header("Accept", "spring/boot")
.build();
ResponseEntity<String> content = this.template.exchange(entity, String.class);
assertThat(content.getStatusCode()).isEqualTo(HttpStatus.NOT_ACCEPTABLE);
}
@Configuration
@Import({ PropertyPlaceholderAutoConfiguration.class, WebMvcAutoConfiguration.class,
HttpMessageConvertersAutoConfiguration.class, TomcatServletWebServerAutoConfiguration.class,
DispatcherServletAutoConfiguration.class, ThymeleafAutoConfiguration.class })
static class TestConfiguration {
static void main(String[] args) {
new SpringApplicationBuilder(TestConfiguration.class).run(args);
}
}
}

View File

@@ -0,0 +1,139 @@
/*
* Copyright 2012-2025 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
*
* https://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.webmvc.autoconfigure;
import org.assertj.core.api.ThrowingConsumer;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders;
import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext;
import org.springframework.boot.test.context.runner.ContextConsumer;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link WelcomePageNotAcceptableHandlerMapping}.
*
* @author Phillip Webb
*/
class WelcomePageNotAcceptableHandlerMappingTests {
private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
.withUserConfiguration(HandlerMappingConfiguration.class)
.withConfiguration(AutoConfigurations.of(PropertyPlaceholderAutoConfiguration.class));
@Test
void isOrderedAtLowPriorityButAboveResourceHandlerRegistry() {
this.contextRunner.withUserConfiguration(StaticResourceConfiguration.class).run((context) -> {
WelcomePageNotAcceptableHandlerMapping handler = context
.getBean(WelcomePageNotAcceptableHandlerMapping.class);
ResourceHandlerRegistry registry = new ResourceHandlerRegistry(context, null);
Integer resourceOrder = (Integer) ReflectionTestUtils.getField(registry, "order");
assertThat(handler.getOrder()).isEqualTo(Ordered.LOWEST_PRECEDENCE - 10);
assertThat(handler.getOrder()).isLessThan(resourceOrder);
});
}
@Test
void handlesRequestForStaticPageThatAcceptsTextHtml() {
this.contextRunner.withUserConfiguration(StaticResourceConfiguration.class)
.run(testWith((mvc) -> assertThat(mvc.get().uri("/").accept(MediaType.TEXT_HTML))
.hasStatus(HttpStatus.NOT_ACCEPTABLE)));
}
@Test
void handlesRequestForStaticPageThatDoesNotAcceptTextHtml() {
this.contextRunner.withUserConfiguration(StaticResourceConfiguration.class)
.run(testWith((mvc) -> assertThat(mvc.get().uri("/").accept(MediaType.APPLICATION_JSON))
.hasStatus(HttpStatus.NOT_ACCEPTABLE)));
}
@Test
void handlesRequestWithNoAcceptHeader() {
this.contextRunner.withUserConfiguration(StaticResourceConfiguration.class)
.run(testWith((mvc) -> assertThat(mvc.get().uri("/")).hasStatus(HttpStatus.NOT_ACCEPTABLE)));
}
@Test
void handlesRequestWithEmptyAcceptHeader() {
this.contextRunner.withUserConfiguration(StaticResourceConfiguration.class)
.run(testWith((mvc) -> assertThat(mvc.get().uri("/").header(HttpHeaders.ACCEPT, ""))
.hasStatus(HttpStatus.NOT_ACCEPTABLE)));
}
@Test
void rootHandlerIsNotRegisteredWhenStaticPathPatternIsNotSlashStarStar() {
this.contextRunner.withUserConfiguration(StaticResourceConfiguration.class)
.withPropertyValues("static-path-pattern=/foo/**")
.run((context) -> assertThat(context.getBean(WelcomePageNotAcceptableHandlerMapping.class).getRootHandler())
.isNull());
}
@Test
void producesNotFoundResponseWhenThereIsNoWelcomePage() {
this.contextRunner.run(testWith(
(mvc) -> assertThat(mvc.get().uri("/").accept(MediaType.TEXT_HTML)).hasStatus(HttpStatus.NOT_FOUND)));
}
private ContextConsumer<AssertableWebApplicationContext> testWith(ThrowingConsumer<MockMvcTester> mvc) {
return (context) -> mvc.accept(MockMvcTester.from(context));
}
@Configuration(proxyBeanMethods = false)
static class HandlerMappingConfiguration {
@Bean
WelcomePageNotAcceptableHandlerMapping handlerMapping(ApplicationContext applicationContext,
ObjectProvider<TemplateAvailabilityProviders> templateAvailabilityProviders,
ObjectProvider<Resource> staticIndexPage,
@Value("${static-path-pattern:/**}") String staticPathPattern) {
return new WelcomePageNotAcceptableHandlerMapping(
templateAvailabilityProviders
.getIfAvailable(() -> new TemplateAvailabilityProviders(applicationContext)),
applicationContext, staticIndexPage.getIfAvailable(), staticPathPattern);
}
}
@Configuration(proxyBeanMethods = false)
static class StaticResourceConfiguration {
@Bean
Resource staticIndexPage() {
return new FileSystemResource("src/test/resources/welcome-page/index.html");
}
}
}

View File

@@ -0,0 +1,178 @@
/*
* Copyright 2012-2025 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
*
* https://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.webmvc.autoconfigure.error;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import jakarta.servlet.ServletException;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.http.autoconfigure.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.test.util.ApplicationContextTestUtils;
import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration;
import org.springframework.boot.webmvc.autoconfigure.DispatcherServletAutoConfiguration;
import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
import org.springframework.web.context.ConfigurableWebApplicationContext;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link BasicErrorController} using {@link MockMvcTester} but not
* {@link org.springframework.test.context.junit.jupiter.SpringExtension}.
*
* @author Dave Syer
* @author Sebastien Deleuze
*/
class BasicErrorControllerDirectMockMvcTests {
private ConfigurableWebApplicationContext wac;
private MockMvcTester mvc;
@AfterEach
void close() {
ApplicationContextTestUtils.closeAll(this.wac);
}
void setup(ConfigurableWebApplicationContext context) {
this.wac = context;
this.mvc = MockMvcTester.from(this.wac);
}
@Test
void errorPageAvailableWithParentContext() {
setup((ConfigurableWebApplicationContext) new SpringApplicationBuilder(ParentConfiguration.class)
.child(ChildConfiguration.class)
.run("--server.port=0"));
assertThat(this.mvc.get().uri("/error").accept(MediaType.TEXT_HTML)).hasStatus5xxServerError()
.bodyText()
.contains("status=999");
}
@Test
void errorPageAvailableWithMvcIncluded() {
setup((ConfigurableWebApplicationContext) new SpringApplication(WebMvcIncludedConfiguration.class)
.run("--server.port=0"));
assertThat(this.mvc.get().uri("/error").accept(MediaType.TEXT_HTML)).hasStatus5xxServerError()
.bodyText()
.contains("status=999");
}
@Test
void errorPageNotAvailableWithWhitelabelDisabled() {
setup((ConfigurableWebApplicationContext) new SpringApplication(WebMvcIncludedConfiguration.class)
.run("--server.port=0", "--server.error.whitelabel.enabled=false"));
assertThat(this.mvc.get().uri("/error").accept(MediaType.TEXT_HTML)).hasFailed()
.failure()
.isInstanceOf(ServletException.class);
}
@Test
void errorControllerWithAop() {
setup((ConfigurableWebApplicationContext) new SpringApplication(WithAopConfiguration.class)
.run("--server.port=0"));
assertThat(this.mvc.get().uri("/error").accept(MediaType.TEXT_HTML)).hasStatus5xxServerError()
.bodyText()
.contains("status=999");
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({ TomcatServletWebServerAutoConfiguration.class, DispatcherServletAutoConfiguration.class,
WebMvcAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class,
ErrorMvcAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class })
protected @interface MinimalWebConfiguration {
}
@Configuration(proxyBeanMethods = false)
@MinimalWebConfiguration
static class ParentConfiguration {
}
@Configuration(proxyBeanMethods = false)
@MinimalWebConfiguration
@EnableWebMvc
static class WebMvcIncludedConfiguration {
// For manual testing
static void main(String[] args) {
SpringApplication.run(WebMvcIncludedConfiguration.class, args);
}
}
@Configuration(proxyBeanMethods = false)
@MinimalWebConfiguration
static class VanillaConfiguration {
// For manual testing
static void main(String[] args) {
SpringApplication.run(VanillaConfiguration.class, args);
}
}
@Configuration(proxyBeanMethods = false)
@MinimalWebConfiguration
static class ChildConfiguration {
// For manual testing
static void main(String[] args) {
new SpringApplicationBuilder(ParentConfiguration.class).child(ChildConfiguration.class).run(args);
}
}
@Configuration(proxyBeanMethods = false)
@EnableAspectJAutoProxy(proxyTargetClass = false)
@MinimalWebConfiguration
@Aspect
static class WithAopConfiguration {
@Pointcut("within(@org.springframework.stereotype.Controller *)")
private void controllerPointCut() {
}
@Around("controllerPointCut()")
Object mvcAdvice(ProceedingJoinPoint pjp) throws Throwable {
return pjp.proceed();
}
}
}

View File

@@ -0,0 +1,545 @@
/*
* Copyright 2012-2025 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
*
* https://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.webmvc.autoconfigure.error;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Parameter;
import java.net.URI;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.freemarker.autoconfigure.FreeMarkerAutoConfiguration;
import org.springframework.boot.http.autoconfigure.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.testsupport.classpath.resources.WithResource;
import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.error.ErrorAttributeOptions.Include;
import org.springframework.boot.webmvc.autoconfigure.DispatcherServletAutoConfiguration;
import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration;
import org.springframework.boot.webmvc.error.ErrorAttributes;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestAttribute;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.view.AbstractView;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link BasicErrorController} using a real HTTP server.
*
* @author Phillip Webb
* @author Dave Syer
* @author Stephane Nicoll
* @author Scott Frederick
*/
class BasicErrorControllerIntegrationTests {
private ConfigurableApplicationContext context;
@AfterEach
void closeContext() {
if (this.context != null) {
this.context.close();
}
}
@Test
@SuppressWarnings({ "rawtypes", "unchecked" })
void testErrorForMachineClientDefault() {
load();
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl("?trace=true"), Map.class);
assertErrorAttributes(entity.getBody(), "500", "Internal Server Error", null, null, "/");
assertThat(entity.getBody()).doesNotContainKey("exception");
assertThat(entity.getBody()).doesNotContainKey("trace");
}
@Test
void testErrorForMachineClientWithParamsTrue() {
load("--server.error.include-exception=true", "--server.error.include-stacktrace=on-param",
"--server.error.include-message=on-param");
exceptionWithStackTraceAndMessage("?trace=true&message=true");
}
@Test
void testErrorForMachineClientWithParamsFalse() {
load("--server.error.include-exception=true", "--server.error.include-stacktrace=on-param",
"--server.error.include-message=on-param");
exceptionWithoutStackTraceAndMessage("?trace=false&message=false");
}
@Test
void testErrorForMachineClientWithParamsAbsent() {
load("--server.error.include-exception=true", "--server.error.include-stacktrace=on-param",
"--server.error.include-message=on-param");
exceptionWithoutStackTraceAndMessage("");
}
@Test
void testErrorForMachineClientNeverParams() {
load("--server.error.include-exception=true", "--server.error.include-stacktrace=never",
"--server.error.include-message=never");
exceptionWithoutStackTraceAndMessage("?trace=true&message=true");
}
@Test
void testErrorForMachineClientAlwaysParams() {
load("--server.error.include-exception=true", "--server.error.include-stacktrace=always",
"--server.error.include-message=always");
exceptionWithStackTraceAndMessage("?trace=false&message=false");
}
@Test
@SuppressWarnings("rawtypes")
void testErrorForMachineClientAlwaysParamsWithoutMessage() {
load("--server.error.include-exception=true", "--server.error.include-message=always");
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl("/noMessage"), Map.class);
assertErrorAttributes(entity.getBody(), "500", "Internal Server Error", IllegalStateException.class,
"No message available", "/noMessage");
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private void exceptionWithStackTraceAndMessage(String path) {
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl(path), Map.class);
assertErrorAttributes(entity.getBody(), "500", "Internal Server Error", IllegalStateException.class,
"Expected!", "/");
assertThat(entity.getBody()).containsKey("trace");
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private void exceptionWithoutStackTraceAndMessage(String path) {
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl(path), Map.class);
assertErrorAttributes(entity.getBody(), "500", "Internal Server Error", IllegalStateException.class, null, "/");
assertThat(entity.getBody()).doesNotContainKey("trace");
}
@Test
@SuppressWarnings("rawtypes")
void testErrorForAnnotatedExceptionWithoutMessage() {
load("--server.error.include-exception=true");
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl("/annotated"), Map.class);
assertErrorAttributes(entity.getBody(), "400", "Bad Request", TestConfiguration.Errors.ExpectedException.class,
null, "/annotated");
}
@Test
@SuppressWarnings("rawtypes")
void testErrorForAnnotatedExceptionWithMessage() {
load("--server.error.include-exception=true", "--server.error.include-message=always");
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl("/annotated"), Map.class);
assertErrorAttributes(entity.getBody(), "400", "Bad Request", TestConfiguration.Errors.ExpectedException.class,
"Expected!", "/annotated");
}
@Test
@SuppressWarnings("rawtypes")
void testErrorForAnnotatedNoReasonExceptionWithoutMessage() {
load("--server.error.include-exception=true");
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl("/annotatedNoReason"), Map.class);
assertErrorAttributes(entity.getBody(), "406", "Not Acceptable",
TestConfiguration.Errors.NoReasonExpectedException.class, null, "/annotatedNoReason");
}
@Test
@SuppressWarnings("rawtypes")
void testErrorForAnnotatedNoReasonExceptionWithMessage() {
load("--server.error.include-exception=true", "--server.error.include-message=always");
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl("/annotatedNoReason"), Map.class);
assertErrorAttributes(entity.getBody(), "406", "Not Acceptable",
TestConfiguration.Errors.NoReasonExpectedException.class, "Expected message", "/annotatedNoReason");
}
@Test
@SuppressWarnings("rawtypes")
void testErrorForAnnotatedNoMessageExceptionWithMessage() {
load("--server.error.include-exception=true", "--server.error.include-message=always");
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl("/annotatedNoMessage"), Map.class);
assertErrorAttributes(entity.getBody(), "406", "Not Acceptable",
TestConfiguration.Errors.NoReasonExpectedException.class, "No message available",
"/annotatedNoMessage");
}
@Test
void testBindingExceptionForMachineClientWithErrorsParamTrue() {
load("--server.error.include-exception=true", "--server.error.include-binding-errors=on-param");
bindingExceptionWithErrors("?errors=true");
}
@Test
void testBindingExceptionForMachineClientWithErrorsParamFalse() {
load("--server.error.include-exception=true", "--server.error.include-binding-errors=on-param");
bindingExceptionWithoutErrors("?errors=false");
}
@Test
void testBindingExceptionForMachineClientWithErrorsParamAbsent() {
load("--server.error.include-exception=true", "--server.error.include-binding-errors=on-param");
bindingExceptionWithoutErrors("");
}
@Test
void testBindingExceptionForMachineClientAlwaysErrors() {
load("--server.error.include-exception=true", "--server.error.include-binding-errors=always");
bindingExceptionWithErrors("?errors=false");
}
@Test
void testBindingExceptionForMachineClientNeverErrors() {
load("--server.error.include-exception=true", "--server.error.include-binding-errors=never");
bindingExceptionWithoutErrors("?errors=true");
}
@Test
void testBindingExceptionForMachineClientWithMessageParamTrue() {
load("--server.error.include-exception=true", "--server.error.include-message=on-param");
bindingExceptionWithMessage("?message=true");
}
@Test
void testBindingExceptionForMachineClientWithMessageParamFalse() {
load("--server.error.include-exception=true", "--server.error.include-message=on-param");
bindingExceptionWithoutMessage("?message=false");
}
@Test
void testBindingExceptionForMachineClientWithMessageParamAbsent() {
load("--server.error.include-exception=true", "--server.error.include-message=on-param");
bindingExceptionWithoutMessage("");
}
@Test
void testBindingExceptionForMachineClientAlwaysMessage() {
load("--server.error.include-exception=true", "--server.error.include-message=always");
bindingExceptionWithMessage("?message=false");
}
@Test
void testBindingExceptionForMachineClientNeverMessage() {
load("--server.error.include-exception=true", "--server.error.include-message=never");
bindingExceptionWithoutMessage("?message=true");
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private void bindingExceptionWithErrors(String param) {
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl("/bind" + param), Map.class);
assertErrorAttributes(entity.getBody(), "400", "Bad Request", MethodArgumentNotValidException.class, null,
"/bind");
assertThat(entity.getBody()).containsKey("errors");
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private void bindingExceptionWithoutErrors(String param) {
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl("/bind" + param), Map.class);
assertErrorAttributes(entity.getBody(), "400", "Bad Request", MethodArgumentNotValidException.class, null,
"/bind");
assertThat(entity.getBody()).doesNotContainKey("errors");
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private void bindingExceptionWithMessage(String param) {
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl("/bind" + param), Map.class);
assertErrorAttributes(entity.getBody(), "400", "Bad Request", MethodArgumentNotValidException.class,
"Validation failed for object='test'. Error count: 1", "/bind");
assertThat(entity.getBody()).doesNotContainKey("errors");
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private void bindingExceptionWithoutMessage(String param) {
ResponseEntity<Map> entity = new TestRestTemplate().getForEntity(createUrl("/bind" + param), Map.class);
assertErrorAttributes(entity.getBody(), "400", "Bad Request", MethodArgumentNotValidException.class, null,
"/bind");
assertThat(entity.getBody()).doesNotContainKey("errors");
}
@Test
@SuppressWarnings({ "rawtypes", "unchecked" })
void testRequestBodyValidationForMachineClient() {
load("--server.error.include-exception=true");
RequestEntity request = RequestEntity.post(URI.create(createUrl("/bodyValidation")))
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.body("{}");
ResponseEntity<Map> entity = new TestRestTemplate().exchange(request, Map.class);
assertErrorAttributes(entity.getBody(), "400", "Bad Request", MethodArgumentNotValidException.class, null,
"/bodyValidation");
assertThat(entity.getBody()).doesNotContainKey("errors");
}
@Test
@SuppressWarnings({ "rawtypes", "unchecked" })
void testBindingExceptionForMachineClientDefault() {
load();
RequestEntity request = RequestEntity.get(URI.create(createUrl("/bind?trace=true,message=true")))
.accept(MediaType.APPLICATION_JSON)
.build();
ResponseEntity<Map> entity = new TestRestTemplate().exchange(request, Map.class);
assertThat(entity.getBody()).doesNotContainKey("exception");
assertThat(entity.getBody()).doesNotContainKey("trace");
assertThat(entity.getBody()).doesNotContainKey("errors");
}
@Test
@WithResource(name = "templates/error/507.ftlh", content = "We are out of storage")
void testConventionTemplateMapping() {
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();
assertThat(resp).contains("We are out of storage");
}
@Test
void testIncompatibleMediaType() {
load();
RequestEntity<?> request = RequestEntity.get(URI.create(createUrl("/incompatibleType")))
.accept(MediaType.TEXT_PLAIN)
.build();
ResponseEntity<String> entity = new TestRestTemplate().exchange(request, String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
assertThat(entity.getHeaders().getContentType()).isNull();
assertThat(entity.getBody()).isNull();
}
@Test
@SuppressWarnings({ "rawtypes", "unchecked" })
void customErrorControllerWithoutStatusConfiguration() {
load(CustomErrorControllerWithoutStatusConfiguration.class);
RequestEntity request = RequestEntity.post(URI.create(createUrl("/bodyValidation")))
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.body("{}");
ResponseEntity<Map> entity = new TestRestTemplate().exchange(request, Map.class);
assertThat(entity.getBody()).doesNotContainKey("status");
}
private void assertErrorAttributes(Map<?, ?> content, String status, String error, Class<?> exception,
String message, String path) {
assertThat(content.get("status")).as("Wrong status").hasToString(status);
assertThat(content.get("error")).as("Wrong error").isEqualTo(error);
if (exception != null) {
assertThat(content.get("exception")).as("Wrong exception").isEqualTo(exception.getName());
}
else {
assertThat(content.containsKey("exception")).as("Exception attribute should not be set").isFalse();
}
assertThat(content.get("message")).as("Wrong message").isEqualTo(message);
assertThat(content.get("path")).as("Wrong path").isEqualTo(path);
}
private String createUrl(String path) {
int port = this.context.getEnvironment().getProperty("local.server.port", int.class);
return "http://localhost:" + port + path;
}
private void load(String... arguments) {
load(TestConfiguration.class, arguments);
}
private void load(Class<?> configuration, String... arguments) {
List<String> args = new ArrayList<>();
args.add("--server.port=0");
if (arguments != null) {
args.addAll(Arrays.asList(arguments));
}
this.context = SpringApplication.run(configuration, StringUtils.toStringArray(args));
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ImportAutoConfiguration({ TomcatServletWebServerAutoConfiguration.class, DispatcherServletAutoConfiguration.class,
WebMvcAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class,
ErrorMvcAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class })
private @interface MinimalWebConfiguration {
}
@Configuration(proxyBeanMethods = false)
@MinimalWebConfiguration
@ImportAutoConfiguration(FreeMarkerAutoConfiguration.class)
public static class TestConfiguration {
// For manual testing
static void main(String[] args) {
SpringApplication.run(TestConfiguration.class, args);
}
@Bean
View error() {
return new AbstractView() {
@Override
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
response.getWriter().write("ERROR_BEAN");
}
};
}
@RestController
public static class Errors {
public String getFoo() {
return "foo";
}
@RequestMapping("/")
String home() {
throw new IllegalStateException("Expected!");
}
@RequestMapping("/noMessage")
String noMessage() {
throw new IllegalStateException();
}
@RequestMapping("/annotated")
String annotated() {
throw new ExpectedException();
}
@RequestMapping("/annotatedNoReason")
String annotatedNoReason() {
throw new NoReasonExpectedException("Expected message");
}
@RequestMapping("/annotatedNoMessage")
String annotatedNoMessage() {
throw new NoReasonExpectedException("");
}
@RequestMapping("/bind")
String bind(@RequestAttribute(required = false) String foo) throws Exception {
BindException error = new BindException(this, "test");
error.rejectValue("foo", "bar.error");
Parameter fooParameter = ReflectionUtils.findMethod(Errors.class, "bind", String.class)
.getParameters()[0];
throw new MethodArgumentNotValidException(MethodParameter.forParameter(fooParameter), error);
}
@PostMapping(path = "/bodyValidation", produces = "application/json")
String bodyValidation(@Valid @RequestBody DummyBody body) {
return body.content;
}
@RequestMapping(path = "/noStorage")
String noStorage() {
throw new InsufficientStorageException();
}
@RequestMapping(path = "/incompatibleType", produces = "text/plain")
String incompatibleType() {
throw new ExpectedException();
}
@ResponseStatus(value = HttpStatus.BAD_REQUEST, reason = "Expected!")
@SuppressWarnings("serial")
static class ExpectedException extends RuntimeException {
}
@ResponseStatus(HttpStatus.INSUFFICIENT_STORAGE)
static class InsufficientStorageException extends RuntimeException {
}
@ResponseStatus(HttpStatus.NOT_ACCEPTABLE)
@SuppressWarnings("serial")
static class NoReasonExpectedException extends RuntimeException {
NoReasonExpectedException(String message) {
super(message);
}
}
static class DummyBody {
@NotNull
private String content;
String getContent() {
return this.content;
}
void setContent(String content) {
this.content = content;
}
}
}
}
static class CustomErrorControllerWithoutStatusConfiguration extends TestConfiguration {
@Bean
BasicErrorController basicErrorController(ServerProperties serverProperties, ErrorAttributes errorAttributes,
ObjectProvider<ErrorViewResolver> errorViewResolvers) {
return new BasicErrorController(errorAttributes, serverProperties.getError(),
errorViewResolvers.orderedStream().toList()) {
@Override
protected ErrorAttributeOptions getErrorAttributeOptions(HttpServletRequest request,
MediaType mediaType) {
return super.getErrorAttributeOptions(request, mediaType).excluding(Include.STATUS);
}
};
}
}
}

View File

@@ -0,0 +1,231 @@
/*
* Copyright 2012-2025 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
*
* https://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.webmvc.autoconfigure.error;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Parameter;
import java.util.Map;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.ServletContext;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.http.autoconfigure.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.webmvc.autoconfigure.DispatcherServletAutoConfiguration;
import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
import org.springframework.test.web.servlet.assertj.MvcTestResult;
import org.springframework.util.ReflectionUtils;
import org.springframework.validation.BindException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.RequestAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.view.AbstractView;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link BasicErrorController} using {@link MockMvcTester} and
* {@link SpringBootTest @SpringBootTest}.
*
* @author Dave Syer
* @author Scott Frederick
*/
@SpringBootTest(properties = { "server.error.include-message=always", "debug=true" })
@DirtiesContext
class BasicErrorControllerMockMvcTests {
@Autowired
private WebApplicationContext wac;
private MockMvcTester mvc;
@BeforeEach
void setup() {
this.mvc = MockMvcTester.from(this.wac);
}
@Test
void testDirectAccessForMachineClient() {
assertThat(this.mvc.get().uri("/error")).hasStatus5xxServerError().bodyText().contains("999");
}
@Test
void testErrorWithNotFoundResponseStatus() {
assertThat(this.mvc.get().uri("/bang")).hasStatus(HttpStatus.NOT_FOUND)
.satisfies((result) -> assertThat(this.mvc.perform(new ErrorDispatcher(result, "/error"))).bodyText()
.contains("Expected!"));
}
@Test
void testErrorWithNoContentResponseStatus() {
assertThat(this.mvc.get().uri("/noContent").accept("some/thing")).hasStatus(HttpStatus.NO_CONTENT)
.satisfies((result) -> assertThat(this.mvc.perform(new ErrorDispatcher(result, "/error")))
.hasStatus(HttpStatus.NO_CONTENT)
.body()
.isEmpty());
}
@Test
void testBindingExceptionForMachineClient() {
// In a real server the response is carried over into the error dispatcher, but
// in the mock a new one is created, so we have to assert the status at this
// intermediate point, and the rendered status code is always wrong (but would
// be 400 in a real system)
assertThat(this.mvc.get().uri("/bind")).hasStatus4xxClientError()
.satisfies((result) -> assertThat(this.mvc.perform(new ErrorDispatcher(result, "/error"))).bodyText()
.contains("Validation failed"));
}
@Test
void testDirectAccessForBrowserClient() {
assertThat(this.mvc.get().uri("/error").accept(MediaType.TEXT_HTML)).hasStatus5xxServerError()
.bodyText()
.contains("ERROR_BEAN");
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ImportAutoConfiguration({ DispatcherServletAutoConfiguration.class, WebMvcAutoConfiguration.class,
HttpMessageConvertersAutoConfiguration.class, ErrorMvcAutoConfiguration.class,
PropertyPlaceholderAutoConfiguration.class })
private @interface MinimalWebConfiguration {
}
@Configuration(proxyBeanMethods = false)
@MinimalWebConfiguration
static class TestConfiguration {
// For manual testing
static void main(String[] args) {
SpringApplication.run(TestConfiguration.class, args);
}
@Bean
View error() {
return new AbstractView() {
@Override
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
response.getWriter().write("ERROR_BEAN");
}
};
}
@RestController
public static class Errors {
@RequestMapping("/")
String home() {
throw new IllegalStateException("Expected!");
}
@RequestMapping("/bang")
String bang() {
throw new NotFoundException("Expected!");
}
@RequestMapping("/bind")
String bind(@RequestAttribute(required = false) String foo) throws Exception {
BindException error = new BindException(this, "test");
error.rejectValue("foo", "bar.error");
Parameter fooParameter = ReflectionUtils.findMethod(Errors.class, "bind", String.class)
.getParameters()[0];
throw new MethodArgumentNotValidException(MethodParameter.forParameter(fooParameter), error);
}
@RequestMapping("/noContent")
void noContent() {
throw new NoContentException("Expected!");
}
public String getFoo() {
return "foo";
}
}
}
@ResponseStatus(HttpStatus.NOT_FOUND)
static class NotFoundException extends RuntimeException {
NotFoundException(String string) {
super(string);
}
}
@ResponseStatus(HttpStatus.NO_CONTENT)
private static class NoContentException extends RuntimeException {
NoContentException(String string) {
super(string);
}
}
private class ErrorDispatcher implements RequestBuilder {
private final MvcResult result;
private final String path;
ErrorDispatcher(MvcTestResult mvcTestResult, String path) {
this.result = mvcTestResult.getMvcResult();
this.path = path;
}
@Override
public MockHttpServletRequest buildRequest(ServletContext servletContext) {
MockHttpServletRequest request = this.result.getRequest();
request.setDispatcherType(DispatcherType.ERROR);
request.setRequestURI(this.path);
request.setAttribute("jakarta.servlet.error.status_code", this.result.getResponse().getStatus());
return request;
}
}
}

View File

@@ -0,0 +1,116 @@
/*
* Copyright 2012-2025 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
*
* https://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.webmvc.autoconfigure.error;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.http.autoconfigure.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.webmvc.autoconfigure.DispatcherServletAutoConfiguration;
import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
import org.springframework.web.context.WebApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for the default error view.
*
* @author Dave Syer
* @author Scott Frederick
*/
@SpringBootTest(properties = { "server.error.include-message=always" })
@DirtiesContext
class DefaultErrorViewIntegrationTests {
@Autowired
private WebApplicationContext wac;
private MockMvcTester mvc;
@BeforeEach
void setup() {
this.mvc = MockMvcTester.from(this.wac);
}
@Test
void testErrorForBrowserClient() {
assertThat(this.mvc.get().uri("/error").accept(MediaType.TEXT_HTML)).hasStatus5xxServerError()
.bodyText()
.contains("<html>", "999");
}
@Test
void testErrorWithHtmlEscape() {
assertThat(this.mvc.get()
.uri("/error")
.requestAttr("jakarta.servlet.error.exception",
new RuntimeException("<script>alert('Hello World')</script>"))
.accept(MediaType.TEXT_HTML)).hasStatus5xxServerError()
.bodyText()
.contains("&lt;script&gt;", "Hello World", "999");
}
@Test
void testErrorWithSpelEscape() {
String spel = "${T(" + getClass().getName() + ").injectCall()}";
assertThat(this.mvc.get()
.uri("/error")
.requestAttr("jakarta.servlet.error.exception", new RuntimeException(spel))
.accept(MediaType.TEXT_HTML)).hasStatus5xxServerError().bodyText().doesNotContain("injection");
}
static String injectCall() {
return "injection";
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({ DispatcherServletAutoConfiguration.class, WebMvcAutoConfiguration.class,
HttpMessageConvertersAutoConfiguration.class, ErrorMvcAutoConfiguration.class,
PropertyPlaceholderAutoConfiguration.class })
protected @interface MinimalWebConfiguration {
}
@Configuration(proxyBeanMethods = false)
@MinimalWebConfiguration
static class TestConfiguration {
// For manual testing
static void main(String[] args) {
SpringApplication.run(TestConfiguration.class, args);
}
}
}

View File

@@ -0,0 +1,223 @@
/*
* Copyright 2012-2025 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
*
* https://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.webmvc.autoconfigure.error;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders;
import org.springframework.boot.autoconfigure.web.WebProperties.Resources;
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.http.MediaType;
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.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link DefaultErrorViewResolver}.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
@ExtendWith(MockitoExtension.class)
class DefaultErrorViewResolverTests {
private DefaultErrorViewResolver resolver;
@Mock
private TemplateAvailabilityProvider templateAvailabilityProvider;
private Resources resourcesProperties;
private final Map<String, Object> model = new HashMap<>();
private final HttpServletRequest request = new MockHttpServletRequest();
@BeforeEach
void setup() {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
applicationContext.refresh();
this.resourcesProperties = new Resources();
TemplateAvailabilityProviders templateAvailabilityProviders = new TestTemplateAvailabilityProviders(
this.templateAvailabilityProvider);
this.resolver = new DefaultErrorViewResolver(applicationContext, this.resourcesProperties,
templateAvailabilityProviders);
}
@Test
void createWhenApplicationContextIsNullShouldThrowException() {
assertThatIllegalArgumentException().isThrownBy(() -> new DefaultErrorViewResolver(null, new Resources()))
.withMessageContaining("'applicationContext' must not be null");
}
@Test
void createWhenResourcePropertiesIsNullShouldThrowException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new DefaultErrorViewResolver(mock(ApplicationContext.class), (Resources) null))
.withMessageContaining("'resources' must not be null");
}
@Test
void resolveWhenNoMatchShouldReturnNull() {
ModelAndView resolved = this.resolver.resolveErrorView(this.request, HttpStatus.NOT_FOUND, this.model);
assertThat(resolved).isNull();
}
@Test
void resolveWhenExactTemplateMatchShouldReturnTemplate() {
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");
then(this.templateAvailabilityProvider).should()
.isTemplateAvailable(eq("error/404"), any(Environment.class), any(ClassLoader.class),
any(ResourceLoader.class));
then(this.templateAvailabilityProvider).shouldHaveNoMoreInteractions();
}
@Test
void resolveWhenSeries5xxTemplateMatchShouldReturnTemplate() {
given(this.templateAvailabilityProvider.isTemplateAvailable(eq("error/503"), any(Environment.class),
any(ClassLoader.class), any(ResourceLoader.class)))
.willReturn(false);
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
void resolveWhenSeries4xxTemplateMatchShouldReturnTemplate() {
given(this.templateAvailabilityProvider.isTemplateAvailable(eq("error/404"), any(Environment.class),
any(ClassLoader.class), any(ResourceLoader.class)))
.willReturn(false);
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
void resolveWhenExactResourceMatchShouldReturnResource() throws Exception {
setResourceLocation("/exact");
ModelAndView resolved = this.resolver.resolveErrorView(this.request, HttpStatus.NOT_FOUND, this.model);
MockHttpServletResponse response = render(resolved);
assertThat(response.getContentAsString().trim()).isEqualTo("exact/404");
assertThat(response.getContentType()).isEqualTo(MediaType.TEXT_HTML_VALUE);
}
@Test
void resolveWhenSeries4xxResourceMatchShouldReturnResource() throws Exception {
setResourceLocation("/4xx");
ModelAndView resolved = this.resolver.resolveErrorView(this.request, HttpStatus.NOT_FOUND, this.model);
MockHttpServletResponse response = render(resolved);
assertThat(response.getContentAsString().trim()).isEqualTo("4xx/4xx");
assertThat(response.getContentType()).isEqualTo(MediaType.TEXT_HTML_VALUE);
}
@Test
void resolveWhenSeries5xxResourceMatchShouldReturnResource() throws Exception {
setResourceLocation("/5xx");
ModelAndView resolved = this.resolver.resolveErrorView(this.request, HttpStatus.INTERNAL_SERVER_ERROR,
this.model);
MockHttpServletResponse response = render(resolved);
assertThat(response.getContentAsString().trim()).isEqualTo("5xx/5xx");
assertThat(response.getContentType()).isEqualTo(MediaType.TEXT_HTML_VALUE);
}
@Test
void resolveWhenTemplateAndResourceMatchShouldFavorTemplate() {
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
void resolveWhenExactResourceMatchAndSeriesTemplateMatchShouldFavorResource() throws Exception {
setResourceLocation("/exact");
given(this.templateAvailabilityProvider.isTemplateAvailable(eq("error/404"), any(Environment.class),
any(ClassLoader.class), any(ResourceLoader.class)))
.willReturn(false);
ModelAndView resolved = this.resolver.resolveErrorView(this.request, HttpStatus.NOT_FOUND, this.model);
then(this.templateAvailabilityProvider).shouldHaveNoMoreInteractions();
MockHttpServletResponse response = render(resolved);
assertThat(response.getContentAsString().trim()).isEqualTo("exact/404");
assertThat(response.getContentType()).isEqualTo(MediaType.TEXT_HTML_VALUE);
}
@Test
void orderShouldBeLowest() {
assertThat(this.resolver.getOrder()).isEqualTo(Ordered.LOWEST_PRECEDENCE);
}
@Test
void setOrderShouldChangeOrder() {
this.resolver.setOrder(123);
assertThat(this.resolver.getOrder()).isEqualTo(123);
}
private void setResourceLocation(String path) {
String packageName = getClass().getPackage().getName();
this.resourcesProperties
.setStaticLocations(new String[] { "classpath:" + packageName.replace('.', '/') + path + "/" });
}
private MockHttpServletResponse render(ModelAndView modelAndView) throws Exception {
MockHttpServletResponse response = new MockHttpServletResponse();
modelAndView.getView().render(this.model, this.request, response);
return response;
}
static class TestTemplateAvailabilityProviders extends TemplateAvailabilityProviders {
TestTemplateAvailabilityProviders(TemplateAvailabilityProvider provider) {
super(Collections.singletonList(provider));
}
}
}

View File

@@ -0,0 +1,118 @@
/*
* Copyright 2012-2025 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
*
* https://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.webmvc.autoconfigure.error;
import java.time.Clock;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.error.ErrorAttributeOptions.Include;
import org.springframework.boot.webmvc.autoconfigure.DispatcherServletAutoConfiguration;
import org.springframework.boot.webmvc.error.ErrorAttributes;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.handler.DispatcherServletWebRequest;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ErrorMvcAutoConfiguration}.
*
* @author Brian Clozel
* @author Scott Frederick
*/
@ExtendWith(OutputCaptureExtension.class)
class ErrorMvcAutoConfigurationTests {
private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner().withConfiguration(
AutoConfigurations.of(DispatcherServletAutoConfiguration.class, ErrorMvcAutoConfiguration.class));
@Test
void renderContainsViewWithExceptionDetails() {
this.contextRunner.run((context) -> {
View errorView = context.getBean("error", View.class);
ErrorAttributes errorAttributes = context.getBean(ErrorAttributes.class);
DispatcherServletWebRequest webRequest = createWebRequest(new IllegalStateException("Exception message"),
false);
errorView.render(errorAttributes.getErrorAttributes(webRequest, withAllOptions()), webRequest.getRequest(),
webRequest.getResponse());
assertThat(webRequest.getResponse().getContentType()).isEqualTo("text/html;charset=UTF-8");
String responseString = ((MockHttpServletResponse) webRequest.getResponse()).getContentAsString();
assertThat(responseString).contains(
"<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>")
.contains("<div>Exception message</div>")
.contains("<div style='white-space:pre-wrap;'>java.lang.IllegalStateException");
});
}
@Test
void renderCanUseJavaTimeTypeAsTimestamp() { // gh-23256
this.contextRunner.run((context) -> {
View errorView = context.getBean("error", View.class);
ErrorAttributes errorAttributes = context.getBean(ErrorAttributes.class);
DispatcherServletWebRequest webRequest = createWebRequest(new IllegalStateException("Exception message"),
false);
Map<String, Object> attributes = errorAttributes.getErrorAttributes(webRequest, withAllOptions());
attributes.put("timestamp", Clock.systemUTC().instant());
errorView.render(attributes, webRequest.getRequest(), webRequest.getResponse());
assertThat(webRequest.getResponse().getContentType()).isEqualTo("text/html;charset=UTF-8");
String responseString = ((MockHttpServletResponse) webRequest.getResponse()).getContentAsString();
assertThat(responseString).contains("This application has no explicit mapping for /error");
});
}
@Test
void renderWhenAlreadyCommittedLogsMessage(CapturedOutput output) {
this.contextRunner.run((context) -> {
View errorView = context.getBean("error", View.class);
ErrorAttributes errorAttributes = context.getBean(ErrorAttributes.class);
DispatcherServletWebRequest webRequest = createWebRequest(new IllegalStateException("Exception message"),
true);
errorView.render(errorAttributes.getErrorAttributes(webRequest, withAllOptions()), webRequest.getRequest(),
webRequest.getResponse());
assertThat(output).contains("Cannot render error page for request [/path] "
+ "and exception [Exception message] as the response has "
+ "already been committed. As a result, the response may have the wrong status code.");
});
}
private DispatcherServletWebRequest createWebRequest(Exception ex, boolean committed) {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/path");
MockHttpServletResponse response = new MockHttpServletResponse();
DispatcherServletWebRequest webRequest = new DispatcherServletWebRequest(request, response);
webRequest.setAttribute("jakarta.servlet.error.exception", ex, RequestAttributes.SCOPE_REQUEST);
webRequest.setAttribute("jakarta.servlet.error.request_uri", "/path", RequestAttributes.SCOPE_REQUEST);
response.setCommitted(committed);
response.setOutputStreamAccessAllowed(!committed);
response.setWriterAccessAllowed(!committed);
return webRequest;
}
private ErrorAttributeOptions withAllOptions() {
return ErrorAttributeOptions.of(Include.values());
}
}

View File

@@ -0,0 +1,95 @@
/*
* Copyright 2012-2025 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
*
* https://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.webmvc.autoconfigure.error;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.http.autoconfigure.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration;
import org.springframework.boot.web.server.ErrorPage;
import org.springframework.boot.web.server.ErrorPageRegistrar;
import org.springframework.boot.web.server.ErrorPageRegistry;
import org.springframework.boot.webmvc.autoconfigure.DispatcherServletAutoConfiguration;
import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.stereotype.Controller;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.web.bind.annotation.RequestMapping;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for remapped error pages.
*
* @author Dave Syer
*/
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = "spring.mvc.servlet.path:/spring/")
@DirtiesContext
class RemappedErrorViewIntegrationTests {
@LocalServerPort
private int port;
private final TestRestTemplate template = new TestRestTemplate();
@Test
void directAccessToErrorPage() {
String content = this.template.getForObject("http://localhost:" + this.port + "/spring/error", String.class);
assertThat(content).contains("error");
assertThat(content).contains("999");
}
@Test
void forwardToErrorPage() {
String content = this.template.getForObject("http://localhost:" + this.port + "/spring/", String.class);
assertThat(content).contains("error");
assertThat(content).contains("500");
}
@Configuration(proxyBeanMethods = false)
@Import({ PropertyPlaceholderAutoConfiguration.class, WebMvcAutoConfiguration.class,
HttpMessageConvertersAutoConfiguration.class, TomcatServletWebServerAutoConfiguration.class,
DispatcherServletAutoConfiguration.class, ErrorMvcAutoConfiguration.class })
@Controller
static class TestConfiguration implements ErrorPageRegistrar {
@RequestMapping("/")
String home() {
throw new RuntimeException("Planned!");
}
@Override
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
errorPageRegistry.addErrorPages(new ErrorPage("/spring/error"));
}
// For manual testing
static void main(String[] args) {
new SpringApplicationBuilder(TestConfiguration.class).properties("spring.mvc.servlet.path:spring/*")
.run(args);
}
}
}

View File

@@ -0,0 +1,333 @@
/*
* Copyright 2012-2025 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
*
* https://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.webmvc.error;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import jakarta.servlet.ServletException;
import org.junit.jupiter.api.Test;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.error.ErrorAttributeOptions.Include;
import org.springframework.context.MessageSourceResolvable;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpStatus;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.util.ReflectionUtils;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.MapBindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.validation.method.MethodValidationResult;
import org.springframework.validation.method.ParameterValidationResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.method.annotation.HandlerMethodValidationException;
import org.springframework.web.servlet.ModelAndView;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link DefaultErrorAttributes}.
*
* @author Phillip Webb
* @author Vedran Pavic
* @author Scott Frederick
* @author Moritz Halbritter
* @author Yanming Zhou
*/
class DefaultErrorAttributesTests {
private final DefaultErrorAttributes errorAttributes = new DefaultErrorAttributes();
private final MockHttpServletRequest request = new MockHttpServletRequest();
private final WebRequest webRequest = new ServletWebRequest(this.request);
@Test
void includeTimeStamp() {
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
ErrorAttributeOptions.defaults());
assertThat(attributes.get("timestamp")).isInstanceOf(Date.class);
}
@Test
void specificStatusCode() {
this.request.setAttribute("jakarta.servlet.error.status_code", 404);
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
ErrorAttributeOptions.defaults());
assertThat(attributes).containsEntry("error", HttpStatus.NOT_FOUND.getReasonPhrase());
assertThat(attributes).containsEntry("status", 404);
}
@Test
void missingStatusCode() {
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
ErrorAttributeOptions.defaults());
assertThat(attributes).containsEntry("error", "None");
assertThat(attributes).containsEntry("status", 999);
}
@Test
void mvcError() {
RuntimeException ex = new RuntimeException("Test");
ModelAndView modelAndView = this.errorAttributes.resolveException(this.request, null, null, ex);
this.request.setAttribute("jakarta.servlet.error.exception", new RuntimeException("Ignored"));
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
ErrorAttributeOptions.of(Include.MESSAGE));
assertThat(this.errorAttributes.getError(this.webRequest)).isSameAs(ex);
assertThat(modelAndView).isNull();
assertThat(attributes).doesNotContainKey("exception");
assertThat(attributes).containsEntry("message", "Test");
}
@Test
void servletErrorWithMessage() {
RuntimeException ex = new RuntimeException("Test");
this.request.setAttribute("jakarta.servlet.error.exception", ex);
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
ErrorAttributeOptions.of(Include.MESSAGE));
assertThat(this.errorAttributes.getError(this.webRequest)).isSameAs(ex);
assertThat(attributes).doesNotContainKey("exception");
assertThat(attributes).containsEntry("message", "Test");
}
@Test
void servletErrorWithoutMessage() {
RuntimeException ex = new RuntimeException("Test");
this.request.setAttribute("jakarta.servlet.error.exception", ex);
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
ErrorAttributeOptions.defaults());
assertThat(this.errorAttributes.getError(this.webRequest)).isSameAs(ex);
assertThat(attributes).doesNotContainKey("exception");
assertThat(attributes).doesNotContainKey("message");
}
@Test
void servletMessageWithMessage() {
this.request.setAttribute("jakarta.servlet.error.message", "Test");
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
ErrorAttributeOptions.of(Include.MESSAGE));
assertThat(attributes).doesNotContainKey("exception");
assertThat(attributes).containsEntry("message", "Test");
}
@Test
void servletMessageWithoutMessage() {
this.request.setAttribute("jakarta.servlet.error.message", "Test");
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
ErrorAttributeOptions.defaults());
assertThat(attributes).doesNotContainKey("exception");
assertThat(attributes).doesNotContainKey("message");
}
@Test
void nullExceptionMessage() {
this.request.setAttribute("jakarta.servlet.error.exception", new RuntimeException());
this.request.setAttribute("jakarta.servlet.error.message", "Test");
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
ErrorAttributeOptions.of(Include.MESSAGE));
assertThat(attributes).doesNotContainKey("exception");
assertThat(attributes).containsEntry("message", "Test");
}
@Test
void nullExceptionMessageAndServletMessage() {
this.request.setAttribute("jakarta.servlet.error.exception", new RuntimeException());
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
ErrorAttributeOptions.of(Include.MESSAGE));
assertThat(attributes).doesNotContainKey("exception");
assertThat(attributes).containsEntry("message", "No message available");
}
@Test
void unwrapServletException() {
RuntimeException ex = new RuntimeException("Test");
ServletException wrapped = new ServletException(new ServletException(ex));
this.request.setAttribute("jakarta.servlet.error.exception", wrapped);
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
ErrorAttributeOptions.of(Include.MESSAGE));
assertThat(this.errorAttributes.getError(this.webRequest)).isSameAs(wrapped);
assertThat(attributes).doesNotContainKey("exception");
assertThat(attributes).containsEntry("message", "Test");
}
@Test
void getError() {
Error error = new OutOfMemoryError("Test error");
this.request.setAttribute("jakarta.servlet.error.exception", error);
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
ErrorAttributeOptions.of(Include.MESSAGE));
assertThat(this.errorAttributes.getError(this.webRequest)).isSameAs(error);
assertThat(attributes).doesNotContainKey("exception");
assertThat(attributes).containsEntry("message", "Test error");
}
@Test
void withBindingErrors() {
BindingResult bindingResult = new MapBindingResult(Collections.singletonMap("a", "b"), "objectName");
bindingResult.addError(new ObjectError("c", "d"));
Exception ex = new BindException(bindingResult);
testBindingResult(bindingResult, ex, ErrorAttributeOptions.of(Include.MESSAGE, Include.BINDING_ERRORS));
}
@Test
void withoutBindingErrors() {
BindingResult bindingResult = new MapBindingResult(Collections.singletonMap("a", "b"), "objectName");
bindingResult.addError(new ObjectError("c", "d"));
Exception ex = new BindException(bindingResult);
testBindingResult(bindingResult, ex, ErrorAttributeOptions.defaults());
}
@Test
void withMethodArgumentNotValidExceptionBindingErrors() {
Method method = ReflectionUtils.findMethod(String.class, "substring", int.class);
MethodParameter parameter = new MethodParameter(method, 0);
BindingResult bindingResult = new MapBindingResult(Collections.singletonMap("a", "b"), "objectName");
bindingResult.addError(new ObjectError("c", "d"));
Exception ex = new MethodArgumentNotValidException(parameter, bindingResult);
testBindingResult(bindingResult, ex, ErrorAttributeOptions.of(Include.MESSAGE, Include.BINDING_ERRORS));
}
@Test
void withHandlerMethodValidationExceptionBindingErrors() {
Object target = "test";
Method method = ReflectionUtils.findMethod(String.class, "substring", int.class);
MethodParameter parameter = new MethodParameter(method, 0);
MethodValidationResult methodValidationResult = MethodValidationResult.create(target, method,
List.of(new ParameterValidationResult(parameter, -1,
List.of(new ObjectError("beginIndex", "beginIndex is negative")), null, null, null,
(error, sourceType) -> {
throw new IllegalArgumentException("No source object of the given type");
})));
HandlerMethodValidationException ex = new HandlerMethodValidationException(methodValidationResult);
testErrors(methodValidationResult.getAllErrors(),
"Validation failed for method='public java.lang.String java.lang.String.substring(int)'. Error count: 1",
ex, ErrorAttributeOptions.of(Include.MESSAGE, Include.BINDING_ERRORS));
}
private void testBindingResult(BindingResult bindingResult, Exception ex, ErrorAttributeOptions options) {
testErrors(bindingResult.getAllErrors(), "Validation failed for object='objectName'. Error count: 1", ex,
options);
}
private void testErrors(List<? extends MessageSourceResolvable> errors, String expectedMessage, Exception ex,
ErrorAttributeOptions options) {
this.request.setAttribute("jakarta.servlet.error.exception", ex);
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest, options);
if (options.isIncluded(Include.MESSAGE)) {
assertThat(attributes).containsEntry("message", expectedMessage);
}
else {
assertThat(attributes).doesNotContainKey("message");
}
if (options.isIncluded(Include.BINDING_ERRORS)) {
assertThat(attributes).containsEntry("errors", org.springframework.boot.web.error.Error.wrap(errors));
}
else {
assertThat(attributes).doesNotContainKey("errors");
}
}
@Test
void withExceptionAttribute() {
DefaultErrorAttributes errorAttributes = new DefaultErrorAttributes();
RuntimeException ex = new RuntimeException("Test");
this.request.setAttribute("jakarta.servlet.error.exception", ex);
Map<String, Object> attributes = errorAttributes.getErrorAttributes(this.webRequest,
ErrorAttributeOptions.of(Include.EXCEPTION, Include.MESSAGE));
assertThat(attributes).containsEntry("exception", RuntimeException.class.getName());
assertThat(attributes).containsEntry("message", "Test");
}
@Test
void withStackTraceAttribute() {
RuntimeException ex = new RuntimeException("Test");
this.request.setAttribute("jakarta.servlet.error.exception", ex);
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
ErrorAttributeOptions.of(Include.STACK_TRACE));
assertThat(attributes.get("trace").toString()).startsWith("java.lang");
}
@Test
void withoutStackTraceAttribute() {
RuntimeException ex = new RuntimeException("Test");
this.request.setAttribute("jakarta.servlet.error.exception", ex);
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
ErrorAttributeOptions.defaults());
assertThat(attributes).doesNotContainKey("trace");
}
@Test
void shouldIncludePathByDefault() {
this.request.setAttribute("jakarta.servlet.error.request_uri", "path");
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
ErrorAttributeOptions.defaults());
assertThat(attributes).containsEntry("path", "path");
}
@Test
void shouldIncludePath() {
this.request.setAttribute("jakarta.servlet.error.request_uri", "path");
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
ErrorAttributeOptions.of(Include.PATH));
assertThat(attributes).containsEntry("path", "path");
}
@Test
void shouldExcludePath() {
this.request.setAttribute("jakarta.servlet.error.request_uri", "path");
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
ErrorAttributeOptions.of());
assertThat(attributes).doesNotContainEntry("path", "path");
}
@Test
void whenGetMessageIsOverriddenThenMessageAttributeContainsValueReturnedFromIt() {
Map<String, Object> attributes = new DefaultErrorAttributes() {
@Override
protected String getMessage(WebRequest webRequest, Throwable error) {
return "custom message";
}
}.getErrorAttributes(this.webRequest, ErrorAttributeOptions.of(Include.MESSAGE));
assertThat(attributes).containsEntry("message", "custom message");
}
@Test
void excludeStatus() {
this.request.setAttribute("jakarta.servlet.error.status_code", 404);
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
ErrorAttributeOptions.defaults().excluding(Include.STATUS));
assertThat(attributes).doesNotContainKey("status");
}
@Test
void excludeError() {
this.request.setAttribute("jakarta.servlet.error.status_code", 404);
Map<String, Object> attributes = this.errorAttributes.getErrorAttributes(this.webRequest,
ErrorAttributeOptions.defaults().excluding(Include.ERROR));
assertThat(attributes).doesNotContainKey("error");
}
}

View File

@@ -0,0 +1,10 @@
<!doctype html>
<html xmlns:th="https://www.thymeleaf.org">
<head>
<title>Test Thymeleaf</title>
<link th:href="@{/custom.css}" rel="stylesheet" />
</head>
<body>
</body>
</html>