SpringBoot对静态资源URL映射的初始化
DelegatingWebMvcConfiguration是一个springboot的MVC配置管理类,继承父类WebMvcConfigurationSupport
@Configuration public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport { private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite(); // 自动装配实现了WebMvcConfigurer的配置bean @Autowired(required = false) public void setConfigurers(List<WebMvcConfigurer> configurers) { if (!CollectionUtils.isEmpty(configurers)) { // 装配到WebMvcConfigurerComposite this.configurers.addWebMvcConfigurers(configurers); } } /** ...省略若干 */ @Override protected void addResourceHandlers(ResourceHandlerRegistry registry) { this.configurers.addResourceHandlers(registry); } /** ...省略若干 */ }
DelegatingWebMvcConfiguration在初始化时将所有实现了WebMvcConfigurer接口的配置bean装配到WebMvcConfigurerComposite中,在各个配置方法中均间接调用了WebMvcConfigurerComposite的配置方法。由于其继承了WebMvcConfigurationSupport所以在初始化时也会产生各种配置bean其中就包含了一个对静态资源处理的bean:
/** * Return a handler mapping ordered at Integer.MAX_VALUE-1 with mapped * resource handlers. To configure resource handling, override * {@link #addResourceHandlers}. */ @Bean public HandlerMapping resourceHandlerMapping() { Assert.state(this.applicationContext != null, "No ApplicationContext set"); Assert.state(this.servletContext != null, "No ServletContext set"); ResourceHandlerRegistry registry = new ResourceHandlerRegistry(this.applicationContext, this.servletContext, mvcContentNegotiationManager(), mvcUrlPathHelper()); // 此处初始化对静态资源处理的ResourceHandlerRegistry实例(在父类中是抽象方法,此处调用的是DelegatingWebMvcConfiguration自己的实现) addResourceHandlers(registry); // 此处初始化url -> handler映射的HandlerMapping实例(实际是SimpleUrlHandlerMapping) AbstractHandlerMapping handlerMapping = registry.getHandlerMapping(); if (handlerMapping != null) { handlerMapping.setPathMatcher(mvcPathMatcher()); handlerMapping.setUrlPathHelper(mvcUrlPathHelper()); handlerMapping.setInterceptors(getInterceptors()); handlerMapping.setCorsConfigurations(getCorsConfigurations()); } else { // 如果registry中没有注册任何url映射则会返回默认HandlerMapping实例(即EmptyHandlerMapping) // 这个EmptyHandlerMapping不会处理任何url均映射 handlerMapping = new EmptyHandlerMapping(); } return handlerMapping; }
下面主要从3个点来分析一下resourceHandlerMapping构建HandlerMapping实例的过程:
初始化ResourceHandlerRegistry
DelegatingWebMvcConfiguration初始化ResourceHandlerRegistry实例是通过addResourceHandlers()方法,实际是调用了WebMvcConfigurerComposite的addResourceHandlers()方法并对所有WebMvcConfigurer实例调用addResourceHandlers()方法,如下:
/** * A {@link WebMvcConfigurer} that delegates to one or more others. * * @author Rossen Stoyanchev * @since 3.1 */ class WebMvcConfigurerComposite implements WebMvcConfigurer { private final List<WebMvcConfigurer> delegates = new ArrayList<>(); public void addWebMvcConfigurers(List<WebMvcConfigurer> configurers) { if (!CollectionUtils.isEmpty(configurers)) { this.delegates.addAll(configurers); } } /** 省略若干... */ @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { for (WebMvcConfigurer delegate : this.delegates) { delegate.addResourceHandlers(registry); } } /** 省略若干... */ }
springboot提供了一个WebMvcAutoConfiguration配置bean,对springMVC做了默认配置定制,其中的静态内部类WebMvcAutoConfigurationAdapter实现了WebMvcConfigurer并在addResourceHandlers方法中对静态资源的ResourceHandlerRegistry进行了初始化:
// Defined as a nested config to ensure WebMvcConfigurer is not read when not on the classpath @Configuration @Import(EnableWebMvcConfiguration.class) @EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class }) @Order(0) public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ResourceLoaderAware { private static final Log logger = LogFactory.getLog(WebMvcConfigurer.class); private final ResourceProperties resourceProperties; private final WebMvcProperties mvcProperties; private final ListableBeanFactory beanFactory; private final HttpMessageConverters messageConverters; final ResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer; private ResourceLoader resourceLoader; 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 addResourceHandlers(ResourceHandlerRegistry registry) { if (!this.resourceProperties.isAddMappings()) { logger.debug("Default resource handling disabled"); return; } Duration cachePeriod = this.resourceProperties.getCache().getPeriod(); CacheControl cacheControl = this.resourceProperties.getCache() .getCachecontrol().toHttpCacheControl(); // 添加对webjars资源处理的映射 if (!registry.hasMappingForPattern("/webjars/**")) { customizeResourceHandlerRegistration(registry .addResourceHandler("/webjars/**") .addResourceLocations("classpath:/META-INF/resources/webjars/") .setCachePeriod(getSeconds(cachePeriod)) .setCacheControl(cacheControl)); } // 添加对静态资源处理的映射 String staticPathPattern = this.mvcProperties.getStaticPathPattern(); if (!registry.hasMappingForPattern(staticPathPattern)) { customizeResourceHandlerRegistration(registry // 静态资源映射路径:/** .addResourceHandler(staticPathPattern) // 映射静态资源位置:locations:classpath:/META-INF/resources/, // classpath:/resources/, classpath:/static/, classpath:/public/, / .addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations())) .setCachePeriod(getSeconds(cachePeriod)) .setCacheControl(cacheControl)); } } /** ...省略 */ }
初始化HandlerMapping
主要是通过ResourceHandlerRegistry的getHandlerMapping()方法,将每一个注册在ResourceHandlerRegistry的url映射转换为<url, ResourceHttpRequestHandler>映射交给SimpleUrlHandlerMapping的实例管理,其中ResourceHttpRequestHandler封装了响应url的静态资源的位置。
/** * Encapsulates information required to create a resource handler. * * @author Rossen Stoyanchev * @author Keith Donald * @author Brian Clozel * @since 3.1 */ public class ResourceHandlerRegistration { private final String[] pathPatterns; private final List<String> locationValues = new ArrayList<>(); @Nullable private Integer cachePeriod; @Nullable private CacheControl cacheControl; @Nullable private ResourceChainRegistration resourceChainRegistration; /** * Create a {@link ResourceHandlerRegistration} instance. * @param pathPatterns one or more resource URL path patterns */ public ResourceHandlerRegistration(String... pathPatterns) { Assert.notEmpty(pathPatterns, "At least one path pattern is required for resource handling."); this.pathPatterns = pathPatterns; } /** * Add one or more resource locations from which to serve static content. * Each location must point to a valid directory. Multiple locations may * be specified as a comma-separated list, and the locations will be checked * for a given resource in the order specified. * <p>For example, {{@code "/"}, {@code "classpath:/META-INF/public-web-resources/"}} * allows resources to be served both from the web application root and * from any JAR on the classpath that contains a * {@code /META-INF/public-web-resources/} directory, with resources in the * web application root taking precedence. * <p>For {@link org.springframework.core.io.UrlResource URL-based resources} * (e.g. files, HTTP URLs, etc) this method supports a special prefix to * indicate the charset associated with the URL so that relative paths * appended to it can be encoded correctly, e.g. * {@code [charset=Windows-31J]http://example.org/path}. * @return the same {@link ResourceHandlerRegistration} instance, for * chained method invocation */ public ResourceHandlerRegistration addResourceLocations(String... resourceLocations) { this.locationValues.addAll(Arrays.asList(resourceLocations)); return this; } /** * Specify the cache period for the resources served by the resource handler, in seconds. The default is to not * send any cache headers but to rely on last-modified timestamps only. Set to 0 in order to send cache headers * that prevent caching, or to a positive number of seconds to send cache headers with the given max-age value. * @param cachePeriod the time to cache resources in seconds * @return the same {@link ResourceHandlerRegistration} instance, for chained method invocation */ public ResourceHandlerRegistration setCachePeriod(Integer cachePeriod) { this.cachePeriod = cachePeriod; return this; } /** * Specify the {@link org.springframework.http.CacheControl} which should be used * by the resource handler. * <p>Setting a custom value here will override the configuration set with {@link #setCachePeriod}. * @param cacheControl the CacheControl configuration to use * @return the same {@link ResourceHandlerRegistration} instance, for chained method invocation * @since 4.2 */ public ResourceHandlerRegistration setCacheControl(CacheControl cacheControl) { this.cacheControl = cacheControl; return this; } /** * Configure a chain of resource resolvers and transformers to use. This * can be useful, for example, to apply a version strategy to resource URLs. * <p>If this method is not invoked, by default only a simple * {@link PathResourceResolver} is used in order to match URL paths to * resources under the configured locations. * @param cacheResources whether to cache the result of resource resolution; * setting this to "true" is recommended for production (and "false" for * development, especially when applying a version strategy) * @return the same {@link ResourceHandlerRegistration} instance, for chained method invocation * @since 4.1 */ public ResourceChainRegistration resourceChain(boolean cacheResources) { this.resourceChainRegistration = new ResourceChainRegistration(cacheResources); return this.resourceChainRegistration; } /** * Configure a chain of resource resolvers and transformers to use. This * can be useful, for example, to apply a version strategy to resource URLs. * <p>If this method is not invoked, by default only a simple * {@link PathResourceResolver} is used in order to match URL paths to * resources under the configured locations. * @param cacheResources whether to cache the result of resource resolution; * setting this to "true" is recommended for production (and "false" for * development, especially when applying a version strategy * @param cache the cache to use for storing resolved and transformed resources; * by default a {@link org.springframework.cache.concurrent.ConcurrentMapCache} * is used. Since Resources aren't serializable and can be dependent on the * application host, one should not use a distributed cache but rather an * in-memory cache. * @return the same {@link ResourceHandlerRegistration} instance, for chained method invocation * @since 4.1 */ public ResourceChainRegistration resourceChain(boolean cacheResources, Cache cache) { this.resourceChainRegistration = new ResourceChainRegistration(cacheResources, cache); return this.resourceChainRegistration; } /** * Return the URL path patterns for the resource handler. */ protected String[] getPathPatterns() { return this.pathPatterns; } /** * Return a {@link ResourceHttpRequestHandler} instance. */ // 构建ResourceHttpRequestHandler实例为其指定处理资源位置 protected ResourceHttpRequestHandler getRequestHandler() { ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler(); if (this.resourceChainRegistration != null) { handler.setResourceResolvers(this.resourceChainRegistration.getResourceResolvers()); handler.setResourceTransformers(this.resourceChainRegistration.getResourceTransformers()); } // 指定静态资源处理位置 handler.setLocationValues(this.locationValues); if (this.cacheControl != null) { // 缓存控制 handler.setCacheControl(this.cacheControl); } else if (this.cachePeriod != null) { // 缓存时间 handler.setCacheSeconds(this.cachePeriod); } return handler; } }
/** * Stores registrations of resource handlers for serving static resources such as images, css files and others * through Spring MVC including setting cache headers optimized for efficient loading in a web browser. * Resources can be served out of locations under web application root, from the classpath, and others. * * <p>To create a resource handler, use {@link #addResourceHandler(String...)} providing the URL path patterns * for which the handler should be invoked to serve static resources (e.g. {@code "/resources/**"}). * * <p>Then use additional methods on the returned {@link ResourceHandlerRegistration} to add one or more * locations from which to serve static content from (e.g. {{@code "/"}, * {@code "classpath:/META-INF/public-web-resources/"}}) or to specify a cache period for served resources. * * @author Rossen Stoyanchev * @since 3.1 * @see DefaultServletHandlerConfigurer */ public class ResourceHandlerRegistry { private final ServletContext servletContext; private final ApplicationContext applicationContext; @Nullable private final ContentNegotiationManager contentNegotiationManager; @Nullable private final UrlPathHelper pathHelper; private final List<ResourceHandlerRegistration> registrations = new ArrayList<>(); private int order = Ordered.LOWEST_PRECEDENCE - 1; /** * Create a new resource handler registry for the given application context. * @param applicationContext the Spring application context * @param servletContext the corresponding Servlet context */ public ResourceHandlerRegistry(ApplicationContext applicationContext, ServletContext servletContext) { this(applicationContext, servletContext, null); } /** * Create a new resource handler registry for the given application context. * @param applicationContext the Spring application context * @param servletContext the corresponding Servlet context * @param contentNegotiationManager the content negotiation manager to use * @since 4.3 */ public ResourceHandlerRegistry(ApplicationContext applicationContext, ServletContext servletContext, @Nullable ContentNegotiationManager contentNegotiationManager) { this(applicationContext, servletContext, contentNegotiationManager, null); } /** * A variant of * {@link #ResourceHandlerRegistry(ApplicationContext, ServletContext, ContentNegotiationManager)} * that also accepts the {@link UrlPathHelper} used for mapping requests to static resources. * @since 4.3.13 */ public ResourceHandlerRegistry(ApplicationContext applicationContext, ServletContext servletContext, @Nullable ContentNegotiationManager contentNegotiationManager, @Nullable UrlPathHelper pathHelper) { Assert.notNull(applicationContext, "ApplicationContext is required"); this.applicationContext = applicationContext; this.servletContext = servletContext; this.contentNegotiationManager = contentNegotiationManager; this.pathHelper = pathHelper; } /** * Add a resource handler for serving static resources based on the specified URL path patterns. * The handler will be invoked for every incoming request that matches to one of the specified * path patterns. * <p>Patterns like {@code "/static/**"} or {@code "/css/{filename:\\w+\\.css}"} are allowed. * See {@link org.springframework.util.AntPathMatcher} for more details on the syntax. * @return a {@link ResourceHandlerRegistration} to use to further configure the * registered resource handler */ // 每添加一个url映射即实例化一个registration(通过它的addResourceLocations方法封装url映射的资源位置) public ResourceHandlerRegistration addResourceHandler(String... pathPatterns) { ResourceHandlerRegistration registration = new ResourceHandlerRegistration(pathPatterns); this.registrations.add(registration); return registration; } /** * Whether a resource handler has already been registered for the given path pattern. */ public boolean hasMappingForPattern(String pathPattern) { for (ResourceHandlerRegistration registration : this.registrations) { if (Arrays.asList(registration.getPathPatterns()).contains(pathPattern)) { return true; } } return false; } /** * Specify the order to use for resource handling relative to other {@link HandlerMapping}s * configured in the Spring MVC application context. * <p>The default value used is {@code Integer.MAX_VALUE-1}. */ public ResourceHandlerRegistry setOrder(int order) { this.order = order; return this; } /** * Return a handler mapping with the mapped resource handlers; or {@code null} in case * of no registrations. */ @Nullable protected AbstractHandlerMapping getHandlerMapping() { if (this.registrations.isEmpty()) { return null; } Map<String, HttpRequestHandler> urlMap = new LinkedHashMap<>(); for (ResourceHandlerRegistration registration : this.registrations) { for (String pathPattern : registration.getPathPatterns()) { // 获取ResourceHttpRequestHandler实例(支持静态资源解析) ResourceHttpRequestHandler handler = registration.getRequestHandler(); if (this.pathHelper != null) { handler.setUrlPathHelper(this.pathHelper); } if (this.contentNegotiationManager != null) { handler.setContentNegotiationManager(this.contentNegotiationManager); } handler.setServletContext(this.servletContext); handler.setApplicationContext(this.applicationContext); try { handler.afterPropertiesSet(); } catch (Throwable ex) { throw new BeanInitializationException("Failed to init ResourceHttpRequestHandler", ex); } // 封装映射<url, HttpRequestHandler> urlMap.put(pathPattern, handler); } } // 构建SimpleUrlHandlerMapping实例并设置优先级 SimpleUrlHandlerMapping handlerMapping = new SimpleUrlHandlerMapping(); handlerMapping.setOrder(order); handlerMapping.setUrlMap(urlMap); return handlerMapping; } }
HandlerMapping实例注册
SimpleUrlHandlerMapping注册映射<url, ResourceHttpRequestHandler>是在initApplicationContext方法中完成的
SimpleUrlHandlerMapping的父类ApplicationObjectSupport实现了ApplicationContextAware接口,在spring容器上下文初始化完成后会回调所有实现了该接口的子类的setApplicationContext(ApplicationContext context)方法,ApplicationObjectSupport在setApplicationContext()方法中又调用了initApplicationContext(),具体实现如下:
@Override public final void setApplicationContext(@Nullable ApplicationContext context) throws BeansException { if (context == null && !isContextRequired()) { // Reset internal context state. this.applicationContext = null; this.messageSourceAccessor = null; } else if (this.applicationContext == null) { // Initialize with passed-in context. if (!requiredContextClass().isInstance(context)) { throw new ApplicationContextException( "Invalid application context: needs to be of type [" + requiredContextClass().getName() + "]"); } this.applicationContext = context; this.messageSourceAccessor = new MessageSourceAccessor(context); // 注:调用initApplicationContext方法进行初始化 initApplicationContext(context); } else { // Ignore reinitialization if same context passed in. if (this.applicationContext != context) { throw new ApplicationContextException( "Cannot reinitialize with different application context: current one is [" + this.applicationContext + "], passed-in one is [" + context + "]"); } } }
可知,SimpleUrlHandlerMapping的实例在spring上下文初始化完成后会在setApplicationContext回调方法中执行initApplicationContext()方法从而注册URL与Handler的映射关系,注册的映射关系保存在父类属性handlerMap集合中。
/** * Register all handlers specified in the URL map for the corresponding paths. * @param urlMap Map with URL paths as keys and handler beans or bean names as values * @throws BeansException if a handler couldn't be registered * @throws IllegalStateException if there is a conflicting handler registered */ /** SimpleUrlHandlerMapping注册url-handler映射关系方法*/ protected void registerHandlers(Map<String, Object> urlMap) throws BeansException { if (urlMap.isEmpty()) { logger.warn("Neither 'urlMap' nor 'mappings' set on SimpleUrlHandlerMapping"); } else { urlMap.forEach((url, handler) -> { // Prepend with slash if not already present. if (!url.startsWith("/")) { url = "/" + url; } // Remove whitespace from handler bean name. if (handler instanceof String) { handler = ((String) handler).trim(); } // 调用父类AbstractUrlHandlerMapping的注册方法 registerHandler(url, handler); }); } }
/** * Register the specified handler for the given URL path. * @param urlPath the URL the bean should be mapped to * @param handler the handler instance or handler bean name String * (a bean name will automatically be resolved into the corresponding handler bean) * @throws BeansException if the handler couldn't be registered * @throws IllegalStateException if there is a conflicting handler registered */ /** 父类AbstractUrlHandlerMapping的url-handler注册方法*/ protected void registerHandler(String urlPath, Object handler) throws BeansException, IllegalStateException { Assert.notNull(urlPath, "URL path must not be null"); Assert.notNull(handler, "Handler object must not be null"); Object resolvedHandler = handler; // Eagerly resolve handler if referencing singleton via name. if (!this.lazyInitHandlers && handler instanceof String) { String handlerName = (String) handler; ApplicationContext applicationContext = obtainApplicationContext(); if (applicationContext.isSingleton(handlerName)) { resolvedHandler = applicationContext.getBean(handlerName); } } Object mappedHandler = this.handlerMap.get(urlPath); if (mappedHandler != null) { if (mappedHandler != resolvedHandler) { throw new IllegalStateException( "Cannot map " + getHandlerDescription(handler) + " to URL path [" + urlPath + "]: There is already " + getHandlerDescription(mappedHandler) + " mapped."); } } else { if (urlPath.equals("/")) { if (logger.isInfoEnabled()) { logger.info("Root mapping to " + getHandlerDescription(handler)); } setRootHandler(resolvedHandler); } else if (urlPath.equals("/*")) { if (logger.isInfoEnabled()) { logger.info("Default mapping to " + getHandlerDescription(handler)); } setDefaultHandler(resolvedHandler); } else { // 注册到父类集合属性handlerMap中 this.handlerMap.put(urlPath, resolvedHandler); if (logger.isInfoEnabled()) { logger.info("Mapped URL path [" + urlPath + "] onto " + getHandlerDescription(handler)); } } } }
SpringBoot响应静态资源的处理流程
springMVC对请求的转发处理是通过DispatcherServlet实现的,首次访问工程时会初始化一些配置,其中包括对HandlerMapping的初始化,具体处理请求的是doDispatch()方法,其中会根据request在已注册的url-handler中找到能够处理该请求的Handler调用handleRequest方法进行处理
@SuppressWarnings("serial") public class DispatcherServlet extends FrameworkServlet { /** ...省略若干 */ /** Detect all HandlerMappings or just expect "handlerMapping" bean? */ private boolean detectAllHandlerMappings = true; /** ...省略若干 */ /** * This implementation calls {@link #initStrategies}. * 父类FrameworkServlet在initServletBean()中回调此方法 */ @Override protected void onRefresh(ApplicationContext context) { initStrategies(context); } /** * Initialize the strategy objects that this servlet uses. * <p>May be overridden in subclasses in order to initialize further strategy objects. */ protected void initStrategies(ApplicationContext context) { initMultipartResolver(context); initLocaleResolver(context); initThemeResolver(context); initHandlerMappings(context); initHandlerAdapters(context); initHandlerExceptionResolvers(context); initRequestToViewNameTranslator(context); initViewResolvers(context); initFlashMapManager(context); } /** * Initialize the HandlerMappings used by this class. * <p>If no HandlerMapping beans are defined in the BeanFactory for this namespace, * we default to BeanNameUrlHandlerMapping. */ private void initHandlerMappings(ApplicationContext context) { this.handlerMappings = null; // 默认查找所有HandlerMapping实例 if (this.detectAllHandlerMappings) { // Find all HandlerMappings in the ApplicationContext, including ancestor contexts. // 此处查找包括我们自己定义的HandlerMapping实例 Map<String, HandlerMapping> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false); if (!matchingBeans.isEmpty()) { this.handlerMappings = new ArrayList<HandlerMapping>(matchingBeans.values()); // We keep HandlerMappings in sorted order. AnnotationAwareOrderComparator.sort(this.handlerMappings); } } else { try { HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class); this.handlerMappings = Collections.singletonList(hm); } catch (NoSuchBeanDefinitionException ex) { // Ignore, we'll add a default HandlerMapping later. } } // Ensure we have at least one HandlerMapping, by registering // a default HandlerMapping if no other mappings are found. if (this.handlerMappings == null) { this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class); if (logger.isDebugEnabled()) { logger.debug("No HandlerMappings found in servlet '" + getServletName() + "': using default"); } } } /** * 实际处理分发请求的方法 * Process the actual dispatching to the handler. * <p>The handler will be obtained by applying the servlet's HandlerMappings in order. * The HandlerAdapter will be obtained by querying the servlet's installed HandlerAdapters * to find the first that supports the handler class. * <p>All HTTP methods are handled by this method. It's up to HandlerAdapters or handlers * themselves to decide which methods are acceptable. * @param request current HTTP request * @param response current HTTP response * @throws Exception in case of any kind of processing failure */ protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { HttpServletRequest processedRequest = request; HandlerExecutionChain mappedHandler = null; boolean multipartRequestParsed = false; WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); try { ModelAndView mv = null; Exception dispatchException = null; try { processedRequest = checkMultipart(request); multipartRequestParsed = (processedRequest != request); // Determine handler for the current request. // 调用此类的getHandler()获取请求对应的Handler执行链(包括对应的Handler和一些拦截器) mappedHandler = getHandler(processedRequest); if (mappedHandler == null || mappedHandler.getHandler() == null) { noHandlerFound(processedRequest, response); return; } // Determine handler adapter for the current request. // 获取对应Handler的适配器ResourceHttpRequestHandler HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); // Process last-modified header, if supported by the handler. String method = request.getMethod(); boolean isGet = "GET".equals(method); if (isGet || "HEAD".equals(method)) { long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); if (logger.isDebugEnabled()) { logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified); } if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) { return; } } // 执行拦截器preHandle方法,如果被拦截则返回 if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } // Actually invoke the handler. // 通过HttpRequestHandlerAdapter解析请求的资源 mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); if (asyncManager.isConcurrentHandlingStarted()) { return; } // 如果mv=null则设置默认渲染的模型视图 applyDefaultViewName(processedRequest, mv); // 执行拦截器postHandle方法 mappedHandler.applyPostHandle(processedRequest, response, mv); } catch (Exception ex) { dispatchException = ex; } catch (Throwable err) { // As of 4.3, we're processing Errors thrown from handler methods as well, // making them available for @ExceptionHandler methods and other scenarios. dispatchException = new NestedServletException("Handler dispatch failed", err); } processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); } catch (Exception ex) { triggerAfterCompletion(processedRequest, response, mappedHandler, ex); } catch (Throwable err) { triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", err)); } finally { if (asyncManager.isConcurrentHandlingStarted()) { // Instead of postHandle and afterCompletion if (mappedHandler != null) { mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response); } } else { // Clean up any resources used by a multipart request. if (multipartRequestParsed) { cleanupMultipart(processedRequest); } } } } /** * Return the HandlerExecutionChain for this request. * <p>Tries all handler mappings in order. * @param request current HTTP request * @return the HandlerExecutionChain, or {@code null} if no handler could be found */ protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { for (HandlerMapping hm : this.handlerMappings) { if (logger.isTraceEnabled()) { logger.trace( "Testing handler map [" + hm + "] in DispatcherServlet with name '" + getServletName() + "'"); } // 调用父类AbstractHandlerMapping的getHandler方法获取 // 已经注册在AbstractUrlHandlerMapping中的实际Handler // 并封装为HandlerExcutionChain执行链返回(添加了拦截器) HandlerExecutionChain handler = hm.getHandler(request); if (handler != null) { return handler; } } return null; } /** ... 省略 */ }
HttpRequestHandlerAdapter是如何对请求进行处理的?它会调用具体handler实例的handleRequest方法进行处理:
/** * Adapter to use the plain {@link org.springframework.web.HttpRequestHandler} * interface with the generic {@link org.springframework.web.servlet.DispatcherServlet}. * Supports handlers that implement the {@link LastModified} interface. * * <p>This is an SPI class, not used directly by application code. * * @author Juergen Hoeller * @since 2.0 * @see org.springframework.web.servlet.DispatcherServlet * @see org.springframework.web.HttpRequestHandler * @see LastModified * @see SimpleControllerHandlerAdapter */ public class HttpRequestHandlerAdapter implements HandlerAdapter { @Override public boolean supports(Object handler) { return (handler instanceof HttpRequestHandler); } @Override @Nullable public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 调用handler实例的handleRequest方法处理请求资源 ((HttpRequestHandler) handler).handleRequest(request, response); return null; } @Override public long getLastModified(HttpServletRequest request, Object handler) { if (handler instanceof LastModified) { return ((LastModified) handler).getLastModified(request); } return -1L; } }
这里贴出springboot处理静态资源的ResourceHttpRequestHandler的handleRequest方法:
/** * Processes a resource request. * <p>Checks for the existence of the requested resource in the configured list of locations. * If the resource does not exist, a {@code 404} response will be returned to the client. * If the resource exists, the request will be checked for the presence of the * {@code Last-Modified} header, and its value will be compared against the last-modified * timestamp of the given resource, returning a {@code 304} status code if the * {@code Last-Modified} value is greater. If the resource is newer than the * {@code Last-Modified} value, or the header is not present, the content resource * of the resource will be written to the response with caching headers * set to expire one year in the future. */ @Override public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // For very general mappings (e.g. "/") we need to check 404 first // 获取请求资源,没有找到返回404 Resource resource = getResource(request); if (resource == null) { logger.trace("No matching resource found - returning 404"); response.sendError(HttpServletResponse.SC_NOT_FOUND); return; } if (HttpMethod.OPTIONS.matches(request.getMethod())) { response.setHeader("Allow", getAllowHeader()); return; } // Supported methods and required session // 校验session是否必要(默认不校验),校验是否支持请求方式,默认只支持GET, HEAD // 如果此处校验不通过会抛出异常,如HttpRequestMethodNotSupportedException(页面405) checkRequest(request); // Header phase if (new ServletWebRequest(request, response).checkNotModified(resource.lastModified())) { logger.trace("Resource not modified - returning 304"); return; } // Apply cache settings, if any prepareResponse(response); // Check the media type for the resource MediaType mediaType = getMediaType(request, resource); if (mediaType != null) { if (logger.isTraceEnabled()) { logger.trace("Determined media type '" + mediaType + "' for " + resource); } } else { if (logger.isTraceEnabled()) { logger.trace("No media type found for " + resource + " - not sending a content-type header"); } } // Content phase if (METHOD_HEAD.equals(request.getMethod())) { setHeaders(response, resource, mediaType); logger.trace("HEAD request - skipping content"); return; } ServletServerHttpResponse outputMessage = new ServletServerHttpResponse(response); if (request.getHeader(HttpHeaders.RANGE) == null) { Assert.state(this.resourceHttpMessageConverter != null, "Not initialized"); setHeaders(response, resource, mediaType); // 输出返回信息 this.resourceHttpMessageConverter.write(resource, mediaType, outputMessage); } else { Assert.state(this.resourceRegionHttpMessageConverter != null, "Not initialized"); response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes"); ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(request); try { List<HttpRange> httpRanges = inputMessage.getHeaders().getRange(); response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); this.resourceRegionHttpMessageConverter.write( HttpRange.toResourceRegions(httpRanges, resource), mediaType, outputMessage); } catch (IllegalArgumentException ex) { response.setHeader("Content-Range", "bytes */" + resource.contentLength()); response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); } } }
拓展SpringMVC静态资源处理配置
根据以上分析我们可以自定义一个实现了WebMvcConfigurer接口的类SpringMvcConfigure来对springboot默认的静态资源处理做一些扩展。注:不必继承至WebMvcConfigurerAdapter因为java8支持接口的default方法。
具体可参考博文:https://blog.csdn.net/catoop/article/details/50501706
引申:springboot不支持使用post方式直接访问静态资源,例如在springboot中配置servlet,然后使用form表单以post方式提交到servlet,此时doPost方法会处理该请求,如果使用request.getRequestDispatcher("/静态页面").forward(req, resp)的方式请求转发到静态页面会报405,提示不支持POST访问,只能使用GET提交表单才行。下面给出配置方案:
import java.util.Arrays; import java.util.LinkedHashMap; import java.util.Map; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping; import org.springframework.web.servlet.resource.ResourceHttpRequestHandler; import org.springframework.web.servlet.support.WebContentGenerator; @Configuration public class SpringMvcConfigure implements WebMvcConfigurer { private int order = Ordered.LOWEST_PRECEDENCE - 2; /** 自定义HandlerMapping实例 */ @Bean("myHandlerMapping") public SimpleUrlHandlerMapping testHandlerMapping() { SimpleUrlHandlerMapping myHandlerMapping = new SimpleUrlHandlerMapping(); Map<String, Object> urlMap = new LinkedHashMap<>(); urlMap.put("/test/**", testHandler()); myHandlerMapping.setUrlMap(urlMap); // 重要:设置顺序优先级优于默认handleMapping,否则不生效(默认order=Ordered.LOWEST_PRECEDENCE - 1) myHandlerMapping.setOrder(order); // DispatcherServlet的initHandlerMappings方法会自动扫描容器中的所有HandlerMapping类型实例 return myHandlerMapping; } /** 加入Bean注解借助spring初始化一些关键属性例,如:afterPropertiesSet()方法 */ @Bean("myHandler") public ResourceHttpRequestHandler testHandler() { ResourceHttpRequestHandler myHandler = new ResourceHttpRequestHandler(); // 此处添加POST方式(默认只支持GET、HEAD) myHandler.setSupportedMethods(WebContentGenerator.METHOD_GET,WebContentGenerator.METHOD_HEAD, WebContentGenerator.METHOD_POST); // locationValue的前缀要与映射的url前缀相同,否则找不到资源(例如url前缀为test,资源文件的位置前缀也为test) myHandler.setLocationValues(Arrays.asList("classpath:/public/test/","classpath:/static/test/","/test/")); return myHandler; } }
配置静态资源交由WEB应用服务器处理
在springMVC中可配置如下标签将静态资源交回Web应用服务器进行处理:
<mvc:default-servlet-handler />
而springboot中默认将所有请求都交给springMVC进行处理(参考WebMvcAutoConfiguration类),默认没有开启DefaultServletHttpRequestHandler。如何手动开启?在上述分析中提到DelegatingWebMvcConfiguration继承了MvcConfigurationSupport中的配置Bean,其中就包括WEB应用服务器处理静态资源的配置Bean:
/** * Return a handler mapping ordered at Integer.MAX_VALUE with a mapped * default servlet handler. To configure "default" Servlet handling, * override {@link #configureDefaultServletHandling}. */ @Bean public HandlerMapping defaultServletHandlerMapping() { Assert.state(this.servletContext != null, "No ServletContext set"); DefaultServletHandlerConfigurer configurer = new DefaultServletHandlerConfigurer(this.servletContext); // 调用子类实现 configureDefaultServletHandling(configurer); // 如果子类实现没有调用configurer.enable()则会返回null HandlerMapping handlerMapping = configurer.buildHandlerMapping(); return (handlerMapping != null ? handlerMapping : new EmptyHandlerMapping()); } /** * Override this method to configure "default" Servlet handling. * @see DefaultServletHandlerConfigurer */ protected void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { }
上面配置Bean中configureDefaultServletHandling()就是配置DefaultServletHttpRequestHandler处理静态资源的接口方法,在执行该方法后会调用DefaultServletHandlerConfigurer实例的buildHandlerMapping方法构造相应的HandlerMapping实例,如果handlerMapping为null则返回EmptyHandlerMapping实例(不会处理任何资源请求),看下DefaultServletHandlerConfigurer是如何构造HandlerMapping实例的:
/** * Configures a request handler for serving static resources by forwarding * the request to the Servlet container's "default" Servlet. This is intended * to be used when the Spring MVC {@link DispatcherServlet} is mapped to "/" * thus overriding the Servlet container's default handling of static resources. * * <p>Since this handler is configured at the lowest precedence, effectively * it allows all other handler mappings to handle the request, and if none * of them do, this handler can forward it to the "default" Servlet. * * @author Rossen Stoyanchev * @author Juergen Hoeller * @since 3.1 * @see DefaultServletHttpRequestHandler */ public class DefaultServletHandlerConfigurer { private final ServletContext servletContext; @Nullable private DefaultServletHttpRequestHandler handler; /** * Create a {@link DefaultServletHandlerConfigurer} instance. * @param servletContext the ServletContext to use. */ public DefaultServletHandlerConfigurer(ServletContext servletContext) { Assert.notNull(servletContext, "ServletContext is required"); this.servletContext = servletContext; } /** * Enable forwarding to the "default" Servlet. * <p>When this method is used the {@link DefaultServletHttpRequestHandler} * will try to autodetect the "default" Servlet name. Alternatively, you can * specify the name of the default Servlet via {@link #enable(String)}. * @see DefaultServletHttpRequestHandler */ public void enable() { enable(null); } /** * Enable forwarding to the "default" Servlet identified by the given name. * <p>This is useful when the default Servlet cannot be autodetected, * for example when it has been manually configured. * @see DefaultServletHttpRequestHandler */ public void enable(@Nullable String defaultServletName) { // 初始化DefaultServletHttpRequestHandler this.handler = new DefaultServletHttpRequestHandler(); if (defaultServletName != null) { this.handler.setDefaultServletName(defaultServletName); } this.handler.setServletContext(this.servletContext); } /** * Return a handler mapping instance ordered at {@link Integer#MAX_VALUE} containing the * {@link DefaultServletHttpRequestHandler} instance mapped to {@code "/**"}; * or {@code null} if default servlet handling was not been enabled. * @since 4.3.12 */ @Nullable protected SimpleUrlHandlerMapping buildHandlerMapping() { if (this.handler == null) { return null; } // 返回封装了DefaultServletHttpRequestHandler实例的HandlingMapping SimpleUrlHandlerMapping handlerMapping = new SimpleUrlHandlerMapping(); handlerMapping.setUrlMap(Collections.singletonMap("/**", this.handler)); handlerMapping.setOrder(Integer.MAX_VALUE); return handlerMapping; } }
分析源码可知,只要DefaultServletHandlerConfigurer持有的DefaultServletHttpRequestHandler实例不为null就会在调用buildHandlerMapping时返回正确的HandlerMapping实例,即只需手动调用它的enable()方法。那么只需在我们自定义的实现了WebMvcConfigurer接口的配置Bean中重写方法configureDefaultServletHandling并调用configurer.enable()即可。
注意,虽然在buildHandlerMapping中返回的HandlerMapping实例匹配路径是/**,但是给它设置的优先级是Integer.MAX_VALUE也就是最低优先级,springBoot配置的springMVC静态资源处理HandlerMapping实例的优先级是Integer.MAX_VALUE-1并且匹配路径也是/**,所以配置的DefaultServletHttpRequestHandler不会生效。如何配置才能生效?只需在自定义配置Bean类上加上@EnableWebMvc注解即可,但这也意味着springBoot默认定制的静态资源处理配置全部失效。(注解控制器规则不受影响)
@Configuration /** 该注解会使SpringBoot静态资源处理配置失效 */ @EnableWebMvc public class MvcConfiguration implements WebMvcConfigurer { /** 自定义静态资源路径解析规则*/ @Bean public ViewResolver getViewResolver() { InternalResourceViewResolver resolver = new InternalResourceViewResolver(); resolver.setPrefix("/WEB-INF/jsp/"); resolver.setSuffix(".html"); return resolver; } /** 开启WEB应用服务器处理静态资源 */ @Override public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { // 开启DefaultServletHttpRequestHandler configurer.enable(); } }