前言
在使用SpringBoot框架进行开发时,如果我们要在应用上放一下静态文件访问,那么只要遵守SpringBoot的规范,在指定目录下放我们的静态文件即可
使用
我们新建一个SpringBoot工程,在resources目录下新建了一个public目录,在public目录下创建一个a.txt,如图所示
启动项目,直接访问http://localhost:8080/a.txt
可以看到浏览器返回了我们的文件内容
源码
我们从springmvc的入口DispatcherServlet的doDispatch开始看
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
......
// Determine handler for the current request.
// 核心代码1
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
......
// Actually invoke the handler.
// 核心代码2
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
......
只看核心代码,核心代码1处根据请求获取可以处理的HandlerExecutionChain
@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}
默认有很多handlerMappings,从里面选择到适合当前request的,这里通过打断点可以找到最后返回的是这个HandlerMapping
我们看看为什么会返回这个,跟进断点
@Nullable
protected Object lookupHandler(String urlPath, HttpServletRequest request) throws Exception {
// Direct match?
// 核心代码1,handlerMap里面有/webjars/**和/**,所以get后为null
Object handler = this.handlerMap.get(urlPath);
......
// Pattern match?
// 核心代码2,根据正则来匹配下,最终发现/**的正则可以匹配我们当前的/a.txt,所以matchingPatterns放了一个/**
List<String> matchingPatterns = new ArrayList<>();
for (String registeredPattern : this.handlerMap.keySet()) {
if (getPathMatcher().match(registeredPattern, urlPath)) {
matchingPatterns.add(registeredPattern);
}
......
}
String bestMatch = null;
Comparator<String> patternComparator = getPathMatcher().getPatternComparator(urlPath);
if (!matchingPatterns.isEmpty()) {
......
// 给bestMatch 赋值为/**
bestMatch = matchingPatterns.get(0);
}
if (bestMatch != null) {
// 核心代码3 根据/**匹配到当前的规则了
handler = this.handlerMap.get(bestMatch);
......
}
从这里的代码可以看出来,先根据我们的路径在所有handlerMappings里面寻找,寻找是按照顺序来的,当前面几个HandlerMapping都匹配不到,则会走到规则为/webjars/**和/**的 SimpleUrlHandlerMapper了
调用了mapping.getHandler(request),获得了一个HandlerExecutionChain,可以看到这里的HandlerExecutionChain是一个ResourceHttpRequestHandler
然后我们看上述核心代码2
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
@Override
@Nullable
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
((HttpRequestHandler) handler).handleRequest(request, response);
return null;
}
进入了ResourceHttpRequestHandler的handleRequest
@Override
public void handleRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// For very general mappings (e.g. "/") we need to check 404 first
// 核心代码1
Resource resource = getResource(request);
......
// Header phase
// 核心代码2
if (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,这个是http请求response需要的
MediaType mediaType = getMediaType(request, resource);
// Content phase
if (METHOD_HEAD.equals(request.getMethod())) {
setHeaders(response, resource, mediaType);
return;
}
ServletServerHttpResponse outputMessage = new ServletServerHttpResponse(response);
if (request.getHeader(HttpHeaders.RANGE) == null) {
Assert.state(this.resourceHttpMessageConverter != null, "Not initialized");
setHeaders(response, resource, mediaType);
// 核心代码3
this.resourceHttpMessageConverter.write(resource, mediaType, outputMessage);
}
......
}
核心代码1,从名字也可以看出来是获取当前资源
@Nullable
protected Resource getResource(HttpServletRequest request) throws IOException {
......
// 核心代码
Resource resource = this.resolverChain.resolveResource(request, path, getLocations());
if (resource != null) {
resource = this.transformerChain.transform(request, resource);
}
return resource;
}
@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);
}
进入resolveResource
@Override
@Nullable
public Resource resolveResource(@Nullable HttpServletRequest request, String requestPath,
List<? extends Resource> locations, ResourceResolverChain chain) {
return resolveResourceInternal(request, requestPath, locations, chain);
}
@Override
protected Resource resolveResourceInternal(@Nullable HttpServletRequest request, String requestPath,
List<? extends Resource> locations, ResourceResolverChain chain) {
return getResource(requestPath, request, locations);
}
@Nullable
private Resource getResource(String resourcePath, @Nullable HttpServletRequest request,
List<? extends Resource> locations) {
// 核心代码
for (Resource location : locations) {
try {
String pathToUse = encodeIfNecessary(resourcePath, request, location);
Resource resource = getResource(pathToUse, location);
if (resource != null) {
return resource;
}
}
......
return null;
}
从这里就是从指定的所有locations前缀目录里面查找文件,看断点可见
也就是我们的静态资源其实放上面任意一个位置都可以被访问到
接下来获取到Resource资源了,然后我们看前面的核心代码2
// Header phase
if (new ServletWebRequest(request, response).checkNotModified(resource.lastModified())) {
logger.trace("Resource not modified");
return;
}
调用checkNotModified方法判断是否修改,这里尝试了几次发现第一次访问时会返回false,但是第二次就会返回true了,好像是在Header里面做了缓存,这里不做分析,继续往下走
看核心代码3
if (request.getHeader(HttpHeaders.RANGE) == null) {
Assert.state(this.resourceHttpMessageConverter != null, "Not initialized");
setHeaders(response, resource, mediaType);
// 核心代码
this.resourceHttpMessageConverter.write(resource, mediaType, outputMessage);
}
@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();
}
}
到这里就基本看完了,t是我们的Resource,outputMessage是一个Response,也就是把我们的文件写入到Response里面,然后调用flush,刷新数据。
总结
通过源码可以看到SpringBoot通过多个handerMapper来根据http请求的url来找到适合当前的handerMapper,比如我们的Controller方法路径则会有相应的handerMapper处理,如果匹配不到则由后面的handerMapper继续匹配,匹配到我们的资源文件的handerMapper则继续寻找,找到了最后还是通过字节的方式通过输出流输出
扩展:springboot还支持我们自定义静态文件路径,其实也就是支持我们修改locations的值,让handerMapper可以匹配到即可