我们知道,拦截器是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。