上文书道 SpringBoot 的自动加载宛若魔法般的存在,而本章将讨论 SpringBoot 中静态资源的
spring: web: resources: static-locations: classpath:/resources/,classpath:/static/,classpath:/public/ server: port: 8081
原理分析
上述配置文件为 Spring 的主配置文件,可以看到这里涉及静态资源导入地址的配置项是spring.web.resources.static-locations,利用 idea 的跳转功能让我们一探究竟,按住 command加左键进入static-locations之后看到如下代码
/*
* Copyright 2012-2020 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.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.convert.DurationUnit;
import org.springframework.http.CacheControl;
/**
* {@link ConfigurationProperties Configuration properties} for general web concerns.
*
* @author Andy Wilkinson
* @since 2.4.0
*/
@ConfigurationProperties("spring.web")
public class WebProperties {
/**
* Locale to use. By default, this locale is overridden by the "Accept-Language"
* header.
*/
private Locale locale;
/**
* Define how the locale should be resolved.
*/
private LocaleResolver localeResolver = LocaleResolver.ACCEPT_HEADER;
private final Resources resources = new Resources();
public Locale getLocale() {
return this.locale;
}
public void setLocale(Locale locale) {
this.locale = locale;
}
public LocaleResolver getLocaleResolver() {
return this.localeResolver;
}
public void setLocaleResolver(LocaleResolver localeResolver) {
this.localeResolver = localeResolver;
}
public Resources getResources() {
return this.resources;
}
public enum LocaleResolver {
/**
* Always use the configured locale.
*/
FIXED,
/**
* Use the "Accept-Language" header or the configured locale if the header is not
* set.
*/
ACCEPT_HEADER
}
public static class Resources {
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/",
"classpath:/resources/", "classpath:/static/", "classpath:/public/" };
/**
* Locations of static resources. Defaults to classpath:[/META-INF/resources/,
* /resources/, /static/, /public/].
*/
private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
/**
* Whether to enable default resource handling.
*/
private boolean addMappings = true;
private boolean customized = false;
private final Chain chain = new Chain();
private final Cache cache = new Cache();
public String[] getStaticLocations() {
return this.staticLocations;
}
public void setStaticLocations(String[] staticLocations) {
this.staticLocations = appendSlashIfNecessary(staticLocations);
this.customized = true;
}
private String[] appendSlashIfNecessary(String[] staticLocations) {
String[] normalized = new String[staticLocations.length];
for (int i = 0; i < staticLocations.length; i++) {
String location = staticLocations[i];
normalized[i] = location.endsWith("/") ? location : location + "/";
}
return normalized;
}
public boolean isAddMappings() {
return this.addMappings;
}
public void setAddMappings(boolean addMappings) {
this.customized = true;
this.addMappings = addMappings;
}
public Chain getChain() {
return this.chain;
}
public Cache getCache() {
return this.cache;
}
public boolean hasBeenCustomized() {
return this.customized || getChain().hasBeenCustomized() || getCache().hasBeenCustomized();
}
/**
* Configuration for the Spring Resource Handling chain.
*/
public static class Chain {
boolean customized = false;
/**
* Whether to enable the Spring Resource Handling chain. By default, disabled
* unless at least one strategy has been enabled.
*/
private Boolean enabled;
/**
* Whether to enable caching in the Resource chain.
*/
private boolean cache = true;
/**
* Whether to enable resolution of already compressed resources (gzip,
* brotli). Checks for a resource name with the '.gz' or '.br' file
* extensions.
*/
private boolean compressed = false;
private final Strategy strategy = new Strategy();
/**
* Return whether the resource chain is enabled. Return {@code null} if no
* specific settings are present.
* @return whether the resource chain is enabled or {@code null} if no
* specified settings are present.
*/
public Boolean getEnabled() {
return getEnabled(getStrategy().getFixed().isEnabled(), getStrategy().getContent().isEnabled(),
this.enabled);
}
private boolean hasBeenCustomized() {
return this.customized || getStrategy().hasBeenCustomized();
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
this.customized = true;
}
public boolean isCache() {
return this.cache;
}
public void setCache(boolean cache) {
this.cache = cache;
this.customized = true;
}
public Strategy getStrategy() {
return this.strategy;
}
public boolean isCompressed() {
return this.compressed;
}
public void setCompressed(boolean compressed) {
this.compressed = compressed;
this.customized = true;
}
static Boolean getEnabled(boolean fixedEnabled, boolean contentEnabled, Boolean chainEnabled) {
return (fixedEnabled || contentEnabled) ? Boolean.TRUE : chainEnabled;
}
/**
* Strategies for extracting and embedding a resource version in its URL path.
*/
public static class Strategy {
private final Fixed fixed = new Fixed();
private final Content content = new Content();
public Fixed getFixed() {
return this.fixed;
}
public Content getContent() {
return this.content;
}
private boolean hasBeenCustomized() {
return getFixed().hasBeenCustomized() || getContent().hasBeenCustomized();
}
/**
* Version Strategy based on content hashing.
*/
public static class Content {
private boolean customized = false;
/**
* Whether to enable the content Version Strategy.
*/
private boolean enabled;
/**
* Comma-separated list of patterns to apply to the content Version
* Strategy.
*/
private String[] paths = new String[] { "/**" };
public boolean isEnabled() {
return this.enabled;
}
public void setEnabled(boolean enabled) {
this.customized = true;
this.enabled = enabled;
}
public String[] getPaths() {
return this.paths;
}
public void setPaths(String[] paths) {
this.customized = true;
this.paths = paths;
}
private boolean hasBeenCustomized() {
return this.customized;
}
}
/**
* Version Strategy based on a fixed version string.
*/
public static class Fixed {
private boolean customized = false;
/**
* Whether to enable the fixed Version Strategy.
*/
private boolean enabled;
/**
* Comma-separated list of patterns to apply to the fixed Version
* Strategy.
*/
private String[] paths = new String[] { "/**" };
/**
* Version string to use for the fixed Version Strategy.
*/
private String version;
public boolean isEnabled() {
return this.enabled;
}
public void setEnabled(boolean enabled) {
this.customized = true;
this.enabled = enabled;
}
public String[] getPaths() {
return this.paths;
}
public void setPaths(String[] paths) {
this.customized = true;
this.paths = paths;
}
public String getVersion() {
return this.version;
}
public void setVersion(String version) {
this.customized = true;
this.version = version;
}
private boolean hasBeenCustomized() {
return this.customized;
}
}
}
}
/**
* Cache configuration.
*/
public static class Cache {
private boolean customized = false;
/**
* Cache period for the resources served by the resource handler. If a
* duration suffix is not specified, seconds will be used. Can be overridden
* by the 'spring.web.resources.cache.cachecontrol' properties.
*/
@DurationUnit(ChronoUnit.SECONDS)
private Duration period;
/**
* Cache control HTTP headers, only allows valid directive combinations.
* Overrides the 'spring.web.resources.cache.period' property.
*/
private final Cachecontrol cachecontrol = new Cachecontrol();
/**
* Whether we should use the "lastModified" metadata of the files in HTTP
* caching headers.
*/
private boolean useLastModified = true;
public Duration getPeriod() {
return this.period;
}
public void setPeriod(Duration period) {
this.customized = true;
this.period = period;
}
public Cachecontrol getCachecontrol() {
return this.cachecontrol;
}
public boolean isUseLastModified() {
return this.useLastModified;
}
public void setUseLastModified(boolean useLastModified) {
this.useLastModified = useLastModified;
}
private boolean hasBeenCustomized() {
return this.customized || getCachecontrol().hasBeenCustomized();
}
/**
* Cache Control HTTP header configuration.
*/
public static class Cachecontrol {
private boolean customized = false;
/**
* Maximum time the response should be cached, in seconds if no duration
* suffix is not specified.
*/
@DurationUnit(ChronoUnit.SECONDS)
private Duration maxAge;
/**
* Indicate that the cached response can be reused only if re-validated
* with the server.
*/
private Boolean noCache;
/**
* Indicate to not cache the response in any case.
*/
private Boolean noStore;
/**
* Indicate that once it has become stale, a cache must not use the
* response without re-validating it with the server.
*/
private Boolean mustRevalidate;
/**
* Indicate intermediaries (caches and others) that they should not
* transform the response content.
*/
private Boolean noTransform;
/**
* Indicate that any cache may store the response.
*/
private Boolean cachePublic;
/**
* Indicate that the response message is intended for a single user and
* must not be stored by a shared cache.
*/
private Boolean cachePrivate;
/**
* Same meaning as the "must-revalidate" directive, except that it does
* not apply to private caches.
*/
private Boolean proxyRevalidate;
/**
* Maximum time the response can be served after it becomes stale, in
* seconds if no duration suffix is not specified.
*/
@DurationUnit(ChronoUnit.SECONDS)
private Duration staleWhileRevalidate;
/**
* Maximum time the response may be used when errors are encountered, in
* seconds if no duration suffix is not specified.
*/
@DurationUnit(ChronoUnit.SECONDS)
private Duration staleIfError;
/**
* Maximum time the response should be cached by shared caches, in seconds
* if no duration suffix is not specified.
*/
@DurationUnit(ChronoUnit.SECONDS)
private Duration sMaxAge;
public Duration getMaxAge() {
return this.maxAge;
}
public void setMaxAge(Duration maxAge) {
this.customized = true;
this.maxAge = maxAge;
}
public Boolean getNoCache() {
return this.noCache;
}
public void setNoCache(Boolean noCache) {
this.customized = true;
this.noCache = noCache;
}
public Boolean getNoStore() {
return this.noStore;
}
public void setNoStore(Boolean noStore) {
this.customized = true;
this.noStore = noStore;
}
public Boolean getMustRevalidate() {
return this.mustRevalidate;
}
public void setMustRevalidate(Boolean mustRevalidate) {
this.customized = true;
this.mustRevalidate = mustRevalidate;
}
public Boolean getNoTransform() {
return this.noTransform;
}
public void setNoTransform(Boolean noTransform) {
this.customized = true;
this.noTransform = noTransform;
}
public Boolean getCachePublic() {
return this.cachePublic;
}
public void setCachePublic(Boolean cachePublic) {
this.customized = true;
this.cachePublic = cachePublic;
}
public Boolean getCachePrivate() {
return this.cachePrivate;
}
public void setCachePrivate(Boolean cachePrivate) {
this.customized = true;
this.cachePrivate = cachePrivate;
}
public Boolean getProxyRevalidate() {
return this.proxyRevalidate;
}
public void setProxyRevalidate(Boolean proxyRevalidate) {
this.customized = true;
this.proxyRevalidate = proxyRevalidate;
}
public Duration getStaleWhileRevalidate() {
return this.staleWhileRevalidate;
}
public void setStaleWhileRevalidate(Duration staleWhileRevalidate) {
this.customized = true;
this.staleWhileRevalidate = staleWhileRevalidate;
}
public Duration getStaleIfError() {
return this.staleIfError;
}
public void setStaleIfError(Duration staleIfError) {
this.customized = true;
this.staleIfError = staleIfError;
}
public Duration getSMaxAge() {
return this.sMaxAge;
}
public void setSMaxAge(Duration sMaxAge) {
this.customized = true;
this.sMaxAge = sMaxAge;
}
public CacheControl toHttpCacheControl() {
PropertyMapper map = PropertyMapper.get();
CacheControl control = createCacheControl();
map.from(this::getMustRevalidate).whenTrue().toCall(control::mustRevalidate);
map.from(this::getNoTransform).whenTrue().toCall(control::noTransform);
map.from(this::getCachePublic).whenTrue().toCall(control::cachePublic);
map.from(this::getCachePrivate).whenTrue().toCall(control::cachePrivate);
map.from(this::getProxyRevalidate).whenTrue().toCall(control::proxyRevalidate);
map.from(this::getStaleWhileRevalidate).whenNonNull()
.to((duration) -> control.staleWhileRevalidate(duration.getSeconds(), TimeUnit.SECONDS));
map.from(this::getStaleIfError).whenNonNull()
.to((duration) -> control.staleIfError(duration.getSeconds(), TimeUnit.SECONDS));
map.from(this::getSMaxAge).whenNonNull()
.to((duration) -> control.sMaxAge(duration.getSeconds(), TimeUnit.SECONDS));
// check if cacheControl remained untouched
if (control.getHeaderValue() == null) {
return null;
}
return control;
}
private CacheControl createCacheControl() {
if (Boolean.TRUE.equals(this.noStore)) {
return CacheControl.noStore();
}
if (Boolean.TRUE.equals(this.noCache)) {
return CacheControl.noCache();
}
if (this.maxAge != null) {
return CacheControl.maxAge(this.maxAge.getSeconds(), TimeUnit.SECONDS);
}
return CacheControl.empty();
}
private boolean hasBeenCustomized() {
return this.customized;
}
}
}
}
}
可以看到setStaticLocations() 方法其实就是将我们设置的值规范化后设置为该类的staticLocations属性作为上下文供 Spring 使用,并且当我们不设置时staticLocations属性的默认值为
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/" };
由此可见SpringBoot 中约定的 4 个默认静态资源配置地址是什么,但真的就这么简单吗?让我们继续往下看
可以看到WebProperties其实是被配置文件所注入的类, 并且属于package org.springframework.boot.autoconfigure.web;该包下,进入org.springframework.boot.autoconfigure.AutoConfiguration.imports看一下该配置文件究竟是何方神圣
可以看到 web前缀后只有一个WebMvcAutoConfiguration是关于 web 配置的,进入代码
/*
* Copyright 2012-2022 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.servlet;
import java.time.Duration;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.function.Consumer;
import javax.servlet.Servlet;
import javax.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.beans.factory.annotation.Qualifier;
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.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.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration;
import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders;
import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration;
import org.springframework.boot.autoconfigure.validation.ValidatorAdapter;
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.format.DateTimeFormatters;
import org.springframework.boot.autoconfigure.web.format.WebConversionService;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcProperties.Format;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.convert.ApplicationConversionService;
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.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.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.ClassUtils;
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.bind.support.ConfigurableWebBindingInitializer;
import org.springframework.web.context.ServletContextAware;
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.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.ThemeResolver;
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.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.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;
import org.springframework.web.util.pattern.PathPatternParser;
/**
* {@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 2.0.0
*/
@AutoConfiguration(after = { DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
ValidationAutoConfiguration.class })
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
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 = "";
/**
* Instance of {@link PathPatternParser} shared across MVC and actuator configuration.
*/
public static final PathPatternParser pathPatternParser = new PathPatternParser();
private static final String SERVLET_LOCATION = "/";
@Bean
@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled")
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new OrderedHiddenHttpMethodFilter();
}
@Bean
@ConditionalOnMissingBean(FormContentFilter.class)
@ConditionalOnProperty(prefix = "spring.mvc.formcontent.filter", name = "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
@SuppressWarnings("deprecation")
@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;
this.mvcProperties.checkConfiguration();
}
@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) {
configurer.setTaskExecutor(((AsyncTaskExecutor) taskExecutor));
}
}
Duration timeout = this.mvcProperties.getAsync().getRequestTimeout();
if (timeout != null) {
configurer.setDefaultTimeout(timeout.toMillis());
}
}
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
if (this.mvcProperties.getPathmatch()
.getMatchingStrategy() == WebMvcProperties.MatchingStrategy.PATH_PATTERN_PARSER) {
configurer.setPatternParser(pathPatternParser);
}
configurer.setUseSuffixPatternMatch(this.mvcProperties.getPathmatch().isUseSuffixPattern());
configurer.setUseRegisteredSuffixPatternMatch(
this.mvcProperties.getPathmatch().isUseRegisteredSuffixPattern());
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.favorPathExtension(contentnegotiation.isFavorPathExtension());
configurer.favorParameter(contentnegotiation.isFavorParameter());
if (contentnegotiation.getParameterName() != null) {
configurer.parameterName(contentnegotiation.getParameterName());
}
Map<String, MediaType> mediaTypes = this.mvcProperties.getContentnegotiation().getMediaTypes();
mediaTypes.forEach(configurer::mediaType);
}
@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, "/webjars/**", "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(RequestContextFilter.class)
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;
}
@Bean
@Override
public RequestMappingHandlerAdapter requestMappingHandlerAdapter(
@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
@Qualifier("mvcConversionService") FormattingConversionService conversionService,
@Qualifier("mvcValidator") Validator validator) {
RequestMappingHandlerAdapter adapter = super.requestMappingHandlerAdapter(contentNegotiationManager,
conversionService, validator);
adapter.setIgnoreDefaultModelOnRedirect(
this.mvcProperties == null || this.mvcProperties.isIgnoreDefaultModelOnRedirect());
return adapter;
}
@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) {
WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(
new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(),
this.mvcProperties.getStaticPathPattern());
welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider));
welcomePageHandlerMapping.setCorsConfigurations(getCorsConfigurations());
return welcomePageHandlerMapping;
}
@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.THEME_RESOLVER_BEAN_NAME)
public ThemeResolver themeResolver() {
return super.themeResolver();
}
@Override
@Bean
@ConditionalOnMissingBean(name = DispatcherServlet.FLASH_MAP_MANAGER_BEAN_NAME)
public FlashMapManager flashMapManager() {
return super.flashMapManager();
}
private Resource getWelcomePage() {
for (String location : this.resourceProperties.getStaticLocations()) {
Resource indexHtml = getIndexHtml(location);
if (indexHtml != null) {
return indexHtml;
}
}
ServletContext servletContext = getServletContext();
if (servletContext != null) {
return getIndexHtml(new ServletContextResource(servletContext, SERVLET_LOCATION));
}
return null;
}
private Resource getIndexHtml(String location) {
return getIndexHtml(this.resourceLoader.getResource(location));
}
private Resource getIndexHtml(Resource location) {
try {
Resource resource = location.createRelative("index.html");
if (resource.exists() && (resource.getURL() != null)) {
return resource;
}
}
catch (Exception ex) {
}
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("javax.validation.Validator", 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) {
((AbstractHandlerExceptionResolver) resolver).setWarnLogCategory(resolver.getClass().getName());
}
}
}
}
@Bean
@Override
@SuppressWarnings("deprecation")
public ContentNegotiationManager mvcContentNegotiationManager() {
ContentNegotiationManager manager = super.mvcContentNegotiationManager();
List<ContentNegotiationStrategy> strategies = manager.getStrategies();
ListIterator<ContentNegotiationStrategy> iterator = strategies.listIterator();
while (iterator.hasNext()) {
ContentNegotiationStrategy strategy = iterator.next();
if (strategy instanceof org.springframework.web.accept.PathExtensionContentNegotiationStrategy) {
iterator.set(new OptionalPathExtensionContentNegotiationStrategy(strategy));
}
}
return manager;
}
@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());
}
}
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;
}
}
/**
* Decorator to make
* {@link org.springframework.web.accept.PathExtensionContentNegotiationStrategy}
* optional depending on a request attribute.
*/
static class OptionalPathExtensionContentNegotiationStrategy implements ContentNegotiationStrategy {
@SuppressWarnings("deprecation")
private static final String SKIP_ATTRIBUTE = org.springframework.web.accept.PathExtensionContentNegotiationStrategy.class
.getName() + ".SKIP";
private final ContentNegotiationStrategy delegate;
OptionalPathExtensionContentNegotiationStrategy(ContentNegotiationStrategy delegate) {
this.delegate = delegate;
}
@Override
public List<MediaType> resolveMediaTypes(NativeWebRequest webRequest)
throws HttpMediaTypeNotAcceptableException {
Object skip = webRequest.getAttribute(SKIP_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
if (skip != null && Boolean.parseBoolean(skip.toString())) {
return MEDIA_TYPE_ALL_LIST;
}
return this.delegate.resolveMediaTypes(webRequest);
}
}
}
其中addResourceHandlers()方法就展示了默认情况下的另外两个静态资源扫描地址
addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
当然这两个地址在addMapping=false 时将不再运行
至此,SpringBoot 静态资源的默认扫描地址算是聊明白了
实践出真知
那么这几个地址的优先级是怎样的呢?最简单的方式就是实验,由于 webjars 存放静态资源的方式在 SpringBoot 现代开发中已经不再推荐,故此我们不在下面实验中添加这部分的测试
环境搭建
分别在以下 4 个目录中添加同名 js 文件,文件内容为当前路径名称
"classpath:/META-INF/resources/","classpath:/resources/", "classpath:/static/", "classpath:/public/"
application.yml 配置如下
spring: devtools: restart: enabled: true #设置开启热部署 additional-paths: src/main/java #重启目录 exclude: WEB-INF/** freemarker: cache: false #页面不加载缓存,修改即时生效 server: port: 8081 debug: trues
实验开始,访问 http://localhost:8081/1.js 展示如下
可以看见 META-INF/resources/下的资源优先级最高,去除该目录下 js 文件我们再次实验
由此可见resources 目录优先级最高,去除该目录下 js 文件我们再次实验
static 目录紧随其后,继续实验
public 目录优先级不如 static
总结
我们从 SpringBoot 默认静态资源加载的好奇心聊到了 SpringBoot 的自动配置类,再由该类的@ConfigurationProperties 注解顺藤摸瓜找到了WebMvcAutoConfiguration类与addResourceHandler处理方法,并简单了解到如果配置了add-mappings: false将会屏蔽默认的资源处理器,那样我们将需要自己配置属于自己的资源处理器
在测试阶段,我们用最简单的方法测试出了 SpringBoot 默认资源加载优先级,即
classpath:/META-INF/resources/>classpath:/resources/> classpath:/static/> classpath:/public/
至此,我觉得 SpringBoot 资源默认加载的问题算是聊明白了,有空我会继续探索自定义静态资源加载类,那么我们下次再见😉