SpringMVC静态资源发布流程(静态如何从文件加载到Resource,最后传给response,以及其专用处理器ResourceHttpRequestHandler源码深入追究)

我们知道,拦截器是SpringMVC提供的一种AOP的实现,而SpringMVC体系默认是只有DispatcherServlet一个Servlet的,所以拦截器并不能拦截自定义Servlet的情况(虽然我们自定义的Servlet可以有处理请求的功能)。
SpringMVC讲究所有网络都由handler提供,所以,我们尝试了一下静态资源发布的时候,到底用的是哪些handler。

我们通过拦截器方法中给出的handler,也就是像这样:

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("handler:" + handler);
    }

首先,静态资源目录,也就是classpath,是可以直接通过路径关系去访问的,这时候我们访问资源,输出信息,得到其handler是:

handler:ResourceHttpRequestHandler [classpath [META-INF/resources/], classpath [resources/], classpath [static/], classpath [public/], ServletContext [/]]

然后我们再尝试Controller发布静态网页:

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class PageController {

    @GetMapping("/page_3")
    public String index() {
        return "page_3";
    }
}

得到的结果是:

handler:com.micah.demo.controller.PageController#index()

我们知道,有些静态资源放在一些冷门的路径中,这时我们可以自己去定义资源路径。

import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        System.out.println("addResourceHandlers...");
        registry.addResourceHandler("/d/**").addResourceLocations("file:D:/");
    }
}

这时候我们访问d盘的一个资源,得到的处理器handler信息:

handler:ResourceHttpRequestHandler [URL [file:D:/]]

我们用自定义Servlet去发布静态资源,连DispatcherServlet都用不上,更别说用了什么handler了。

值得注意的是,加入我们将拦截器postHandle方法中的ModelAndView(未解析的view)打印,会得到一个更加有趣的结果,也就是只有当请求的静态资源是通过Controller获取的时候,ModelAndView才不为null,打印结果如下:

modelAndView:ModelAndView [view="page_3"; model={}]

也就是view为当时return的那个字符串。


我们接下来分析一下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
        Resource resource = getResource(request);
        if (resource == null) {
            logger.debug("Resource not found");
            response.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        if (HttpMethod.OPTIONS.matches(request.getMethod())) {
            response.setHeader("Allow", getAllowHeader());
            return;
        }

        // Supported methods and required session
        checkRequest(request);

        // Header phase
        if (isUseLastModified() && new ServletWebRequest(request, response).checkNotModified(resource.lastModified())) {
            logger.trace("Resource not modified");
            return;
        }

        // Apply cache settings, if any
        prepareResponse(response);

        // Check the media type for the resource
        MediaType mediaType = getMediaType(request, resource);
        setHeaders(response, resource, mediaType);

        // Content phase
        ServletServerHttpResponse outputMessage = new ServletServerHttpResponse(response);
        if (request.getHeader(HttpHeaders.RANGE) == null) {
            Assert.state(this.resourceHttpMessageConverter != null, "Not initialized");
            this.resourceHttpMessageConverter.write(resource, mediaType, outputMessage);
        }
        else {
            Assert.state(this.resourceRegionHttpMessageConverter != null, "Not initialized");
            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(HttpHeaders.CONTENT_RANGE, "bytes */" + resource.contentLength());
                response.sendError(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE);
            }
        }
    }

值得关注的方法总结:

方法作用
Resource resource = getResource(request):加载静态资源
checkRequest(request);检查请求方式和session
setHeaders(response, resource, mediaType):设置请求头
this.resourceHttpMessageConverter.write(resource, mediaType, outputMessage):输出资源

我们深入getResource方法

    @Nullable
    protected Resource getResource(HttpServletRequest request) throws IOException {
        String path = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
        if (path == null) {
            throw new IllegalStateException("Required request attribute '" +
                    HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE + "' is not set");
        }

        path = processPath(path);
        if (!StringUtils.hasText(path) || isInvalidPath(path)) {
            return null;
        }
        if (isInvalidEncodedPath(path)) {
            return null;
        }

        Assert.notNull(this.resolverChain, "ResourceResolverChain not initialized.");
        Assert.notNull(this.transformerChain, "ResourceTransformerChain not initialized.");

		// 关键!
        Resource resource = this.resolverChain.resolveResource(request, path, getLocations());
        if (resource != null) {
            resource = this.transformerChain.transform(request, resource);
        }
        return resource;
    }
        /**
     * Process the given resource path.
     * <p>The default implementation replaces:
     * <ul>
     * <li>Backslash with forward slash.
     * <li>Duplicate occurrences of slash with a single slash.
     * <li>Any combination of leading slash and control characters (00-1F and 7F)
     * with a single "/" or "". For example {@code "  / // foo/bar"}
     * becomes {@code "/foo/bar"}.
     * </ul>
     * @since 3.2.12
     */
    protected String processPath(String path) {
        path = StringUtils.replace(path, "\\", "/");
        path = cleanDuplicateSlashes(path);
        return cleanLeadingSlash(path);
    }

processPath是替换路径斜杆成为一个统一规范的

最后几行的resolverChain和transformerChain,将是资源从何而来的重点。
引出了两个类:DefaultResourceResolverChain 和 DefaultResourceTransformerChain

package org.springframework.web.servlet.resource;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.ListIterator;

import javax.servlet.http.HttpServletRequest;

import org.springframework.core.io.Resource;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;

/**
 * Default immutable implementation of {@link ResourceResolverChain}.
 *
 * @author Rossen Stoyanchev
 * @since 4.1
 */
class DefaultResourceResolverChain implements ResourceResolverChain {

	@Nullable
	private final ResourceResolver resolver;

	@Nullable
	private final ResourceResolverChain nextChain;


	public DefaultResourceResolverChain(@Nullable List<? extends ResourceResolver> resolvers) {
		resolvers = (resolvers != null ? resolvers : Collections.emptyList());
		DefaultResourceResolverChain chain = initChain(new ArrayList<>(resolvers));
		this.resolver = chain.resolver;
		this.nextChain = chain.nextChain;
	}

	private static DefaultResourceResolverChain initChain(ArrayList<? extends ResourceResolver> resolvers) {
		DefaultResourceResolverChain chain = new DefaultResourceResolverChain(null, null);
		ListIterator<? extends ResourceResolver> it = resolvers.listIterator(resolvers.size());
		while (it.hasPrevious()) {
			chain = new DefaultResourceResolverChain(it.previous(), chain);
		}
		return chain;
	}

	private DefaultResourceResolverChain(@Nullable ResourceResolver resolver, @Nullable ResourceResolverChain chain) {
		Assert.isTrue((resolver == null && chain == null) || (resolver != null && chain != null),
				"Both resolver and resolver chain must be null, or neither is");
		this.resolver = resolver;
		this.nextChain = chain;
	}


	@Override
	@Nullable
	public Resource resolveResource(
			@Nullable HttpServletRequest request, String requestPath, List<? extends Resource> locations) {

		return (this.resolver != null && this.nextChain != null ?
				this.resolver.resolveResource(request, requestPath, locations, this.nextChain) : null);
	}

	@Override
	@Nullable
	public String resolveUrlPath(String resourcePath, List<? extends Resource> locations) {
		return (this.resolver != null && this.nextChain != null ?
				this.resolver.resolveUrlPath(resourcePath, locations, this.nextChain) : null);
	}

}

这个DefaultResourceResolverChain方法,我们直接深入它的resolveResource(刚刚就是调用了它),看看干了什么。
因为每个方法只有一个函数的调用而已,所以我们省去追溯的过程,直接来到最终的函数:

	@Nullable
	protected abstract Resource resolveResourceInternal(@Nullable HttpServletRequest request,
			String requestPath, List<? extends Resource> locations, ResourceResolverChain chain);

看看其实现类:
在这里插入图片描述
我们是通过路径查找资源的,那么就进入PathResourceResolver#resolveResourceInternal看看:
追溯到了getResource方法:

@Nullable
	private Resource getResource(String resourcePath, @Nullable HttpServletRequest request,
			List<? extends Resource> locations) {

		for (Resource location : locations) {
			try {
				String pathToUse = encodeOrDecodeIfNecessary(resourcePath, request, location);
				Resource resource = getResource(pathToUse, location);
				if (resource != null) {
					return resource;
				}
			}
			catch (IOException ex) {
				if (logger.isDebugEnabled()) {
					String error = "Skip location [" + location + "] due to error";
					if (logger.isTraceEnabled()) {
						logger.trace(error, ex);
					}
					else {
						logger.debug(error + ": " + ex.getMessage());
					}
				}
			}
		}
		return null;
	}
		/**
	 * Find the resource under the given location.
	 * <p>The default implementation checks if there is a readable
	 * {@code Resource} for the given path relative to the location.
	 * @param resourcePath the path to the resource
	 * @param location the location to check
	 * @return the resource, or {@code null} if none found
	 */
	@Nullable
	protected Resource getResource(String resourcePath, Resource location) throws IOException {
		Resource resource = location.createRelative(resourcePath);
		if (resource.isReadable()) {
			if (checkResource(resource, location)) {
				return resource;
			}
			else if (logger.isWarnEnabled()) {
				Resource[] allowed = getAllowedLocations();
				logger.warn(LogFormatUtils.formatValue(
						"Resource path \"" + resourcePath + "\" was successfully resolved " +
								"but resource \"" + resource.getURL() + "\" is neither under " +
								"the current location \"" + location.getURL() + "\" nor under any of " +
								"the allowed locations " + (allowed != null ? Arrays.asList(allowed) : "[]"), -1, true));
			}
		}
		return null;
	}

注意到Resource resource = location.createRelative(resourcePath);这个方法,我们回溯,location什么时候传进来的?在ResourceHttpRequestHandler的getLocations这个方法得到的locations,被传了进来。看看getLocations

    /**
     * Return the configured {@code List} of {@code Resource} locations including
     * both String-based locations provided via
     * {@link #setLocationValues(List) setLocationValues} and pre-resolved
     * {@code Resource} locations provided via {@link #setLocations(List) setLocations}.
     * <p>Note that the returned list is fully initialized only after
     * initialization via {@link #afterPropertiesSet()}.
     * <p><strong>Note:</strong> As of 5.3.11 the list of locations may be filtered to
     * exclude those that don't actually exist and therefore the list returned from this
     * method may be a subset of all given locations. See {@link #setOptimizeLocations}.
     * @see #setLocationValues
     * @see #setLocations
     */
    public List<Resource> getLocations() {
        if (this.locationsToUse.isEmpty()) {
            // Possibly not yet initialized, return only what we have so far
            return this.locationResources;
        }
        return this.locationsToUse;
    }

this.locationResources什么时候被初始化。追溯一番,没有被初始化,而this.locationsToUse却被初始化了。resolveResourceLocations在afterPropertiesSet得到调用。

private void resolveResourceLocations() {
		List<Resource> result = new ArrayList<>();
		if (!this.locationValues.isEmpty()) {
			ApplicationContext applicationContext = obtainApplicationContext();
			for (String location : this.locationValues) {
				if (this.embeddedValueResolver != null) {
					String resolvedLocation = this.embeddedValueResolver.resolveStringValue(location);
					if (resolvedLocation == null) {
						throw new IllegalArgumentException("Location resolved to null: " + location);
					}
					location = resolvedLocation;
				}
				Charset charset = null;
				location = location.trim();
				if (location.startsWith(URL_RESOURCE_CHARSET_PREFIX)) {
					int endIndex = location.indexOf(']', URL_RESOURCE_CHARSET_PREFIX.length());
					if (endIndex == -1) {
						throw new IllegalArgumentException("Invalid charset syntax in location: " + location);
					}
					String value = location.substring(URL_RESOURCE_CHARSET_PREFIX.length(), endIndex);
					charset = Charset.forName(value);
					location = location.substring(endIndex + 1);
				}
				Resource resource = applicationContext.getResource(location);
				if (location.equals("/") && !(resource instanceof ServletContextResource)) {
					throw new IllegalStateException(
							"The String-based location \"/\" should be relative to the web application root " +
							"but resolved to a Resource of type: " + resource.getClass() + ". " +
							"If this is intentional, please pass it as a pre-configured Resource via setLocations.");
				}
				result.add(resource);
				if (charset != null) {
					if (!(resource instanceof UrlResource)) {
						throw new IllegalArgumentException("Unexpected charset for non-UrlResource: " + resource);
					}
					this.locationCharsets.put(resource, charset);
				}
			}
		}

		result.addAll(this.locationResources);
		if (isOptimizeLocations()) {
			result = result.stream().filter(Resource::exists).collect(Collectors.toList());
		}

		this.locationsToUse.clear();
		this.locationsToUse.addAll(result);
	}

最后两行很关键:
this.locationsToUse.clear();
this.locationsToUse.addAll(result);

result就是一个数组,它有这些东西:
Resource resource = applicationContext.getResource(location);
result.add(resource);
result.addAll(this.locationResources);

我们知道this.locationResources是没有东西的,所以可以忽略。

resource我们追溯过去,发现ResourceLoader :

package org.springframework.core.io;

import org.springframework.lang.Nullable;

public interface ResourceLoader {
    String CLASSPATH_URL_PREFIX = "classpath:";

    Resource getResource(String location);

    @Nullable
    ClassLoader getClassLoader();
}

选择追溯GenericApplicationContext进去,因为一开始调用它的applicationContext,我发现也是一个Context。

    public Resource getResource(String location) {
        return this.resourceLoader != null ? this.resourceLoader.getResource(location) : super.getResource(location);
    }

DefaultResourceLoader:

public Resource getResource(String location) {
        Assert.notNull(location, "Location must not be null");
        Iterator var2 = this.getProtocolResolvers().iterator();

        Resource resource;
        do {
            if (!var2.hasNext()) {
                if (location.startsWith("/")) {
                    return this.getResourceByPath(location);
                }

                if (location.startsWith("classpath:")) {
                    return new ClassPathResource(location.substring("classpath:".length()), this.getClassLoader());
                }

                try {
                    URL url = new URL(location);
                    return (Resource)(ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url));
                } catch (MalformedURLException var5) {
                    return this.getResourceByPath(location);
                }
            }

            ProtocolResolver protocolResolver = (ProtocolResolver)var2.next();
            resource = protocolResolver.resolve(location, this);
        } while(resource == null);

        return resource;
    }

它是将所有getProtocolResolvers得到的解决器,如果刚好是可以对应使用的,就返回。
例如Path的、url的、classpath的。

到这里,我们就已经得到了一个资源Resource,这个资源其实有很多的我们所想要的静态资源组成。这个资源其实被放进来result,最后放进locationsToUse。

拿到了资源Resource,我们就继续
resource = this.transformerChain.transform(request, resource);
transformerChain是类DefaultResourceTransformerChain

package org.springframework.web.servlet.resource;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.ListIterator;

import javax.servlet.http.HttpServletRequest;

import org.springframework.core.io.Resource;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;

/**
 * Default immutable implementation of {@link ResourceTransformerChain}.
 *
 * @author Rossen Stoyanchev
 * @since 4.1
 */
class DefaultResourceTransformerChain implements ResourceTransformerChain {

	private final ResourceResolverChain resolverChain;

	@Nullable
	private final ResourceTransformer transformer;

	@Nullable
	private final ResourceTransformerChain nextChain;


	public DefaultResourceTransformerChain(
			ResourceResolverChain resolverChain, @Nullable List<ResourceTransformer> transformers) {

		Assert.notNull(resolverChain, "ResourceResolverChain is required");
		this.resolverChain = resolverChain;
		transformers = (transformers != null ? transformers : Collections.emptyList());
		DefaultResourceTransformerChain chain = initTransformerChain(resolverChain, new ArrayList<>(transformers));
		this.transformer = chain.transformer;
		this.nextChain = chain.nextChain;
	}

	private DefaultResourceTransformerChain initTransformerChain(ResourceResolverChain resolverChain,
			ArrayList<ResourceTransformer> transformers) {

		DefaultResourceTransformerChain chain = new DefaultResourceTransformerChain(resolverChain, null, null);
		ListIterator<? extends ResourceTransformer> it = transformers.listIterator(transformers.size());
		while (it.hasPrevious()) {
			chain = new DefaultResourceTransformerChain(resolverChain, it.previous(), chain);
		}
		return chain;
	}

	public DefaultResourceTransformerChain(ResourceResolverChain resolverChain,
			@Nullable ResourceTransformer transformer, @Nullable ResourceTransformerChain chain) {

		Assert.isTrue((transformer == null && chain == null) || (transformer != null && chain != null),
				"Both transformer and transformer chain must be null, or neither is");

		this.resolverChain = resolverChain;
		this.transformer = transformer;
		this.nextChain = chain;
	}


	@Override
	public ResourceResolverChain getResolverChain() {
		return this.resolverChain;
	}

	@Override
	public Resource transform(HttpServletRequest request, Resource resource) throws IOException {
		return (this.transformer != null && this.nextChain != null ?
				this.transformer.transform(request, resource, this.nextChain) : resource);
	}

}

可以理解为做资源内容转换的。

获得静态资源后需要写入(this.resourceHttpMessageConverter.write(resource, mediaType, outputMessage);),写入逻辑在此:

	/**
	 * This implementation sets the default headers by calling {@link #addDefaultHeaders},
	 * and then calls {@link #writeInternal}.
	 */
	@Override
	public final void write(final T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
			throws IOException, HttpMessageNotWritableException {

		final HttpHeaders headers = outputMessage.getHeaders();
		addDefaultHeaders(headers, t, contentType);

		if (outputMessage instanceof StreamingHttpOutputMessage) {
			StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
			streamingOutputMessage.setBody(outputStream -> writeInternal(t, new HttpOutputMessage() {
				@Override
				public OutputStream getBody() {
					return outputStream;
				}
				@Override
				public HttpHeaders getHeaders() {
					return headers;
				}
			}));
		}
		else {
			writeInternal(t, outputMessage);
			outputMessage.getBody().flush();
		}
	}

可以看到我们之前新建的
new ServletServerHttpResponse(response);
后面成为了outputMessage,刚好祖先接口又不一致(经深入探究)
所以后面开始调用writeInternal
我们进去ResourceHttpMessageConverter的writeInternal

	@Override
	protected void writeInternal(Resource resource, HttpOutputMessage outputMessage)
			throws IOException, HttpMessageNotWritableException {

		writeContent(resource, outputMessage);
	}
	protected void writeContent(Resource resource, HttpOutputMessage outputMessage)
			throws IOException, HttpMessageNotWritableException {
		// We cannot use try-with-resources here for the InputStream, since we have
		// custom handling of the close() method in a finally-block.
		try {
			InputStream in = resource.getInputStream();
			try {
				StreamUtils.copy(in, outputMessage.getBody());
			}
			catch (NullPointerException ex) {
				// ignore, see SPR-13620
			}
			finally {
				try {
					in.close();
				}
				catch (Throwable ex) {
					// ignore, see SPR-12999
				}
			}
		}
		catch (FileNotFoundException ex) {
			// ignore, see SPR-12999
		}
	}

InputStream in = resource.getInputStream();
然后StreamUtils.copy(in, outputMessage.getBody());
这时候,资源才传给了outputMessage。
提醒outputMessage的来源:
ServletServerHttpResponse outputMessage = new ServletServerHttpResponse(response);

至此,完成了file->Resource->response。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
这个问题可能是由于配置不正确或文件路径错误导致的。下面是一些可能的原因和解决方案: 1. 静态资源目录配置错误:请确保你在SpringMVC配置文件(通常是web.xml或者Java配置类)中正确配置了静态资源处理器(例如,ResourceHttpRequestHandler)。你可以检查配置文件中是否有类似下面的配置: ```xml <mvc:resources mapping="/static/**" location="/static/"/> ``` 2. 静态资源目录位置错误:请确保你的静态资源文件(例如,CSS、JavaScript、图片等)位于项目的正确目录下。默认情况下,SpringMVC会将静态资源放在web应用的根目录(通常是src/main/webapp)下的静态资源目录中。你可以检查一下这个目录是否存在,并且资源文件是否在其中。 3. DispatcherServlet映射错误:如果你的DispatcherServlet映射路径设置为“/”,那么它会拦截所有请求,包括静态资源请求。这可能导致静态资源无法被正确处理。你可以考虑将DispatcherServlet的映射路径设置为其他值(例如“/app”),以避免拦截静态资源请求。 4. 缓存问题:有时候浏览器会缓存静态资源,导致页面上的资源文件无法及时更新。你可以尝试清除浏览器缓存,或者通过在资源文件的URL中添加一个随机参数(例如,加上时间戳)来强制浏览器重新获取最新的资源文件。 如果以上解决方案都无效,你可以提供更详细的错误信息和相关配置,以便我更好地帮助你解决这个问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值