/*
- Copyright 2012-2019 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.autoconfigure.web;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Map.Entry;
import javax.servlet.Servlet;
import javax.servlet.http.HttpServletRequest;
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.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
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.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration;
import org.springframework.boot.autoconfigure.web.ResourceProperties.Strategy;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.filter.OrderedHiddenHttpMethodFilter;
import org.springframework.boot.web.filter.OrderedHttpPutFormContentFilter;
import org.springframework.boot.web.filter.OrderedRequestContextFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Primary;
import org.springframework.core.Ordered;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.core.io.Resource;
import org.springframework.format.Formatter;
import org.springframework.format.FormatterRegistry;
import org.springframework.format.datetime.DateFormatter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import org.springframework.validation.DefaultMessageCodesResolver;
import org.springframework.validation.MessageCodesResolver;
import org.springframework.validation.Validator;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.accept.ContentNegotiationStrategy;
import org.springframework.web.accept.PathExtensionContentNegotiationStrategy;
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextListener;
import org.springframework.web.filter.HiddenHttpMethodFilter;
import org.springframework.web.filter.HttpPutFormContentFilter;
import org.springframework.web.filter.RequestContextFilter;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.LocaleResolver;
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.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.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver;
import org.springframework.web.servlet.handler.AbstractUrlHandlerMapping;
import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;
import org.springframework.web.servlet.i18n.FixedLocaleResolver;
import org.springframework.web.servlet.mvc.ParameterizableViewController;
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.resource.AppCacheManifestTransformer;
import org.springframework.web.servlet.resource.GzipResourceResolver;
import org.springframework.web.servlet.resource.ResourceHttpRequestHandler;
import org.springframework.web.servlet.resource.ResourceResolver;
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;
/**
-
{@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
*/
@Configuration
@ConditionalOnWebApplication
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class,
WebMvcConfigurerAdapter.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class,
ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {public static final String DEFAULT_PREFIX = “”;
public static final String DEFAULT_SUFFIX = “”;
@Bean
@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new OrderedHiddenHttpMethodFilter();
}@Bean
@ConditionalOnMissingBean(HttpPutFormContentFilter.class)
@ConditionalOnProperty(prefix = “spring.mvc.formcontent.putfilter”, name = “enabled”,
matchIfMissing = true)
public OrderedHttpPutFormContentFilter httpPutFormContentFilter() {
return new OrderedHttpPutFormContentFilter();
}// Defined as a nested config to ensure WebMvcConfigurerAdapter is not read when not
// on the classpath
@Configuration
@Import(EnableWebMvcConfiguration.class)
@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
public static class WebMvcAutoConfigurationAdapter extends WebMvcConfigurerAdapter {private static final Log logger = LogFactory .getLog(WebMvcConfigurerAdapter.class); private final ResourceProperties resourceProperties; private final WebMvcProperties mvcProperties; private final ListableBeanFactory beanFactory; private final HttpMessageConverters messageConverters; final ResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer; public WebMvcAutoConfigurationAdapter(ResourceProperties resourceProperties, WebMvcProperties mvcProperties, ListableBeanFactory beanFactory, @Lazy HttpMessageConverters messageConverters, ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider) { this.resourceProperties = resourceProperties; this.mvcProperties = mvcProperties; this.beanFactory = beanFactory; this.messageConverters = messageConverters; this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider .getIfAvailable(); } @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { converters.addAll(this.messageConverters.getConverters()); } @Override public void configureAsyncSupport(AsyncSupportConfigurer configurer) { Long timeout = this.mvcProperties.getAsync().getRequestTimeout(); if (timeout != null) { configurer.setDefaultTimeout(timeout); } } @Override public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { Map<String, MediaType> mediaTypes = this.mvcProperties.getMediaTypes(); for (Entry<String, MediaType> mediaType : mediaTypes.entrySet()) { configurer.mediaType(mediaType.getKey(), mediaType.getValue()); } } @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; } @Bean @ConditionalOnMissingBean @ConditionalOnProperty(prefix = "spring.mvc", name = "locale") public LocaleResolver localeResolver() { if (this.mvcProperties .getLocaleResolver() == WebMvcProperties.LocaleResolver.FIXED) { return new FixedLocaleResolver(this.mvcProperties.getLocale()); } AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver(); localeResolver.setDefaultLocale(this.mvcProperties.getLocale()); return localeResolver; } @Bean @ConditionalOnProperty(prefix = "spring.mvc", name = "date-format") public Formatter<Date> dateFormatter() { return new DateFormatter(this.mvcProperties.getDateFormat()); } @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) { for (Converter<?, ?> converter : getBeansOfType(Converter.class)) { registry.addConverter(converter); } for (GenericConverter converter : getBeansOfType(GenericConverter.class)) { registry.addConverter(converter); } for (Formatter<?> formatter : getBeansOfType(Formatter.class)) { registry.addFormatter(formatter); } } private <T> Collection<T> getBeansOfType(Class<T> type) { return this.beanFactory.getBeansOfType(type).values(); } @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { if (!this.resourceProperties.isAddMappings()) { logger.debug("Default resource handling disabled"); return; } Integer cachePeriod = this.resourceProperties.getCachePeriod(); if (!registry.hasMappingForPattern("/webjars/**")) { customizeResourceHandlerRegistration(registry .addResourceHandler("/webjars/**") .addResourceLocations("classpath:/META-INF/resources/webjars/") .setCachePeriod(cachePeriod)); } String staticPathPattern = this.mvcProperties.getStaticPathPattern(); if (!registry.hasMappingForPattern(staticPathPattern)) { customizeResourceHandlerRegistration( registry.addResourceHandler(staticPathPattern) .addResourceLocations( this.resourceProperties.getStaticLocations()) .setCachePeriod(cachePeriod)); } } @Bean public WelcomePageHandlerMapping welcomePageHandlerMapping( ResourceProperties resourceProperties) { return new WelcomePageHandlerMapping(resourceProperties.getWelcomePage(), this.mvcProperties.getStaticPathPattern()); } private void customizeResourceHandlerRegistration( ResourceHandlerRegistration registration) { if (this.resourceHandlerRegistrationCustomizer != null) { this.resourceHandlerRegistrationCustomizer.customize(registration); } } @Bean @ConditionalOnMissingBean({ RequestContextListener.class, RequestContextFilter.class }) public static RequestContextFilter requestContextFilter() { return new OrderedRequestContextFilter(); } @Configuration @ConditionalOnProperty(value = "spring.mvc.favicon.enabled", matchIfMissing = true) public static class FaviconConfiguration { private final ResourceProperties resourceProperties; public FaviconConfiguration(ResourceProperties resourceProperties) { this.resourceProperties = resourceProperties; } @Bean public SimpleUrlHandlerMapping faviconHandlerMapping() { SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping(); mapping.setOrder(Ordered.HIGHEST_PRECEDENCE + 1); mapping.setUrlMap(Collections.singletonMap("**/favicon.ico", faviconRequestHandler())); return mapping; } @Bean public ResourceHttpRequestHandler faviconRequestHandler() { ResourceHttpRequestHandler requestHandler = new ResourceHttpRequestHandler(); requestHandler .setLocations(this.resourceProperties.getFaviconLocations()); return requestHandler; } }
}
/**
-
Configuration equivalent to {@code @EnableWebMvc}.
*/
@Configuration
public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration {private final WebMvcProperties mvcProperties;
private final ListableBeanFactory beanFactory;
private final WebMvcRegistrations mvcRegistrations;
public EnableWebMvcConfiguration(
ObjectProvider mvcPropertiesProvider,
ObjectProvider mvcRegistrationsProvider,
ListableBeanFactory beanFactory) {
this.mvcProperties = mvcPropertiesProvider.getIfAvailable();
this.mvcRegistrations = mvcRegistrationsProvider.getIfUnique();
this.beanFactory = beanFactory;
}@Bean
@Override
public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
RequestMappingHandlerAdapter adapter = super.requestMappingHandlerAdapter();
adapter.setIgnoreDefaultModelOnRedirect((this.mvcProperties != null)
? this.mvcProperties.isIgnoreDefaultModelOnRedirect() : true);
return adapter;
}@Override
protected RequestMappingHandlerAdapter createRequestMappingHandlerAdapter() {
if (this.mvcRegistrations != null
&& this.mvcRegistrations.getRequestMappingHandlerAdapter() != null) {
return this.mvcRegistrations.getRequestMappingHandlerAdapter();
}
return super.createRequestMappingHandlerAdapter();
}@Bean
@Primary
@Override
public RequestMappingHandlerMapping requestMappingHandlerMapping() {
// Must be @Primary for MvcUriComponentsBuilder to work
return super.requestMappingHandlerMapping();
}@Bean
@Override
public Validator mvcValidator() {
if (!ClassUtils.isPresent(“javax.validation.Validator”,
getClass().getClassLoader())) {
return super.mvcValidator();
}
return WebMvcValidator.get(getApplicationContext(), getValidator());
}@Override
protected RequestMappingHandlerMapping createRequestMappingHandlerMapping() {
if (this.mvcRegistrations != null
&& this.mvcRegistrations.getRequestMappingHandlerMapping() != null) {
return this.mvcRegistrations.getRequestMappingHandlerMapping();
}
return super.createRequestMappingHandlerMapping();
}@Override
protected ConfigurableWebBindingInitializer getConfigurableWebBindingInitializer() {
try {
return this.beanFactory.getBean(ConfigurableWebBindingInitializer.class);
}
catch (NoSuchBeanDefinitionException ex) {
return super.getConfigurableWebBindingInitializer();
}
}@Override
protected ExceptionHandlerExceptionResolver createExceptionHandlerExceptionResolver() {
if (this.mvcRegistrations != null && this.mvcRegistrations
.getExceptionHandlerExceptionResolver() != null) {
return this.mvcRegistrations.getExceptionHandlerExceptionResolver();
}
return super.createExceptionHandlerExceptionResolver();
}@Override
protected void configureHandlerExceptionResolvers(
List exceptionResolvers) {
super.configureHandlerExceptionResolvers(exceptionResolvers);
if (exceptionResolvers.isEmpty()) {
addDefaultHandlerExceptionResolvers(exceptionResolvers);
}
if (this.mvcProperties.isLogResolvedException()) {
for (HandlerExceptionResolver resolver : exceptionResolvers) {
if (resolver instanceof AbstractHandlerExceptionResolver) {
((AbstractHandlerExceptionResolver) resolver)
.setWarnLogCategory(resolver.getClass().getName());
}
}
}
}@Bean
@Override
public ContentNegotiationManager mvcContentNegotiationManager() {
ContentNegotiationManager manager = super.mvcContentNegotiationManager();
List strategies = manager.getStrategies();
ListIterator iterator = strategies.listIterator();
while (iterator.hasNext()) {
ContentNegotiationStrategy strategy = iterator.next();
if (strategy instanceof PathExtensionContentNegotiationStrategy) {
iterator.set(new OptionalPathExtensionContentNegotiationStrategy(
strategy));
}
}
return manager;
}
}
@Configuration
@ConditionalOnEnabledResourceChain
static class ResourceChainCustomizerConfiguration {@Bean public ResourceChainResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer() { return new ResourceChainResourceHandlerRegistrationCustomizer(); }
}
interface ResourceHandlerRegistrationCustomizer {
void customize(ResourceHandlerRegistration registration);
}
private static class ResourceChainResourceHandlerRegistrationCustomizer
implements ResourceHandlerRegistrationCustomizer {@Autowired private ResourceProperties resourceProperties = new ResourceProperties(); @Override public void customize(ResourceHandlerRegistration registration) { ResourceProperties.Chain properties = this.resourceProperties.getChain(); configureResourceChain(properties, registration.resourceChain(properties.isCache())); } private void configureResourceChain(ResourceProperties.Chain properties, ResourceChainRegistration chain) { Strategy strategy = properties.getStrategy(); if (strategy.getFixed().isEnabled() || strategy.getContent().isEnabled()) { chain.addResolver(getVersionResourceResolver(strategy)); } if (properties.isGzipped()) { chain.addResolver(new GzipResourceResolver()); } if (properties.isHtmlApplicationCache()) { chain.addTransformer(new AppCacheManifestTransformer()); } } private ResourceResolver getVersionResourceResolver( ResourceProperties.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; }
}
static final class WelcomePageHandlerMapping extends AbstractUrlHandlerMapping {
private static final Log logger = LogFactory .getLog(WelcomePageHandlerMapping.class); private WelcomePageHandlerMapping(Resource welcomePage, String staticPathPattern) { if (welcomePage != null && "/**".equals(staticPathPattern)) { logger.info("Adding welcome page: " + welcomePage); ParameterizableViewController controller = new ParameterizableViewController(); controller.setViewName("forward:index.html"); setRootHandler(controller); setOrder(0); } } @Override public Object getHandlerInternal(HttpServletRequest request) throws Exception { for (MediaType mediaType : getAcceptedMediaTypes(request)) { if (mediaType.includes(MediaType.TEXT_HTML)) { return super.getHandlerInternal(request); } } return null; } private List<MediaType> getAcceptedMediaTypes(HttpServletRequest request) { String acceptHeader = request.getHeader(HttpHeaders.ACCEPT); return MediaType.parseMediaTypes( StringUtils.hasText(acceptHeader) ? acceptHeader : "*/*"); }
}
/**
-
Decorator to make {@link PathExtensionContentNegotiationStrategy} optional
-
depending on a request attribute.
*/
static class OptionalPathExtensionContentNegotiationStrategy
implements ContentNegotiationStrategy {private static final String SKIP_ATTRIBUTE = PathExtensionContentNegotiationStrategy.class
.getName() + “.SKIP”;private final ContentNegotiationStrategy delegate;
OptionalPathExtensionContentNegotiationStrategy(
ContentNegotiationStrategy delegate) {
this.delegate = delegate;
}@Override
public List resolveMediaTypes(NativeWebRequest webRequest)
throws HttpMediaTypeNotAcceptableException {
Object skip = webRequest.getAttribute(SKIP_ATTRIBUTE,
RequestAttributes.SCOPE_REQUEST);
if (skip != null && Boolean.parseBoolean(skip.toString())) {
return Collections.emptyList();
}
return this.delegate.resolveMediaTypes(webRequest);
}
}
-
}