SpringBoot静态资源访问URL映射的初始化过程及请求响应流程浅析

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();
    }
}

猜你喜欢

转载自blog.csdn.net/w47_csdn/article/details/80090637