Create spring-boot-webmvc module
This commit is contained in:
committed by
Phillip Webb
parent
e83f4eed4e
commit
dfd5e56956
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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() + "/*";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# Template Availability Providers
|
||||
org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider=\
|
||||
org.springframework.boot.webmvc.autoconfigure.JspTemplateAvailabilityProvider
|
||||
@@ -0,0 +1,3 @@
|
||||
org.springframework.boot.webmvc.autoconfigure.DispatcherServletAutoConfiguration
|
||||
org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration
|
||||
org.springframework.boot.webmvc.autoconfigure.error.ErrorMvcAutoConfiguration
|
||||
@@ -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) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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/*");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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("<script>", "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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
4xx/402
|
||||
@@ -0,0 +1 @@
|
||||
4xx/4xx
|
||||
@@ -0,0 +1 @@
|
||||
5xx/4xx
|
||||
@@ -0,0 +1 @@
|
||||
5xx/5xx
|
||||
@@ -0,0 +1 @@
|
||||
exact/404
|
||||
@@ -0,0 +1 @@
|
||||
exact/4xx
|
||||
@@ -0,0 +1,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>
|
||||
Reference in New Issue
Block a user