文件服務器的搭建架構有很多種,如基於nginx+vsftp、nginx+fastDFS等架構,其中vsftp或fastDFS用於文件讀寫、上傳存儲、下載,nginx用於映射文件 ,方便http訪問靜態文件,實現在線預覽圖片或下載等功能。這種架構使用起來很方便,而且fastDFS支持集羣、主從備份等,因而很採公司或開發人員的青睞,但這種構架是無法滿足某些場景的需求,如增加文件訪問權限校驗、時效等,僅憑nginx無法做到,一般可能需要結合luna針對nginx開發模塊,而採用Java、Spring則可以很方便實現這個功能。Spring對於靜態文件訪問有着較好的支持,並支持多種文件映射,如本地文件、jar、ftp等文件類型,Spring框架本身可解析大部分文件類型。
文件服務器的基本原理就是將文件以流或字節輸出到客戶端。
Spring靜態文件配置
相信熟悉tomcat的同學都知道tomcat可作爲靜態文件服務器,將文件放入webapps目錄下,即可用host+path來訪問或下載文件。
SpringMVC框架也支持對於靜態文件mapping的方式來實現文件服務器功能,配置有如下兩種。
1.xml配置
在主springmvc主配置文件中添加
<mvc:annotation-driven />
<mvc:resources mapping="/images/**" location="/images/" />
/images/**映射到 ResourceHttpRequestHandler進行處理,location指定靜態資源的位置.可以是web application根目錄下、jar包裏面,這樣可以把靜態資源壓縮到jar包中。這樣當訪問http://host/images/{file_path}
時,則會到/images/
目錄下去找相應的文件。
location配置支持系統文件、ftp文件、jar文件,對應的配置爲file://
、ftp://
、jar://
,支持Http網絡, DFS協議地址, VFS協議地址,jar包,可參考File、FTP等協議說明。
2.springboot配置
public class FileServerConfig extends WebMvcConfigurerAdapter{
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/images/**").addResourceLocations("file://");
}
}
以上配置則可實現對於系統文件的訪問,相當於一個小的文件服務器,採用springmvc做爲http訪問入口、本地文件系統或ftp文件系統做爲文件倉庫。
Spring Resources訪問原理
以上是利用spring做文件服務器的使用配置,那麼spring是如何做到這一點的?
其實在配置resources時,當訪問url時,spring 分發交給對應的mappingHandler去處理,而靜態文件則由ResourceHttpRequestHandler.handleRequest處理。主要過程是查找資源、解析資源類型、設置content-type,response輸出流。
public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1.獲取資源文件,Resource是spring對靜態資源的高度封裝,可以看成是文件流或字節
Resource resource = this.getResource(request);
if(resource == null) {
logger.trace("No matching resource found - returning 404");
response.sendError(404);
} else if(HttpMethod.OPTIONS.matches(request.getMethod())) {
response.setHeader("Allow", this.getAllowHeader());
} else {
//2.判斷Http請求頭,是否有斷點續傳、解析文件類型設置http返回流ContentType
this.checkRequest(request);
if((new ServletWebRequest(request, response)).checkNotModified(resource.lastModified())) {
logger.trace("Resource not modified - returning 304");
} else {
this.prepareResponse(response);
MediaType mediaType = this.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");
}
if("HEAD".equals(request.getMethod())) {
this.setHeaders(response, resource, mediaType);
logger.trace("HEAD request - skipping content");
} else {
ServletServerHttpResponse outputMessage = new ServletServerHttpResponse(response);
if(request.getHeader("Range") == null) {
this.setHeaders(response, resource, mediaType);
this.resourceHttpMessageConverter.write(resource, mediaType, outputMessage);
} else {
response.setHeader("Accept-Ranges", "bytes");
ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(request);
try {
List ex = inputMessage.getHeaders().getRange();
response.setStatus(206);
if(ex.size() == 1) {
ResourceRegion resourceRegion = ((HttpRange)ex.get(0)).toResourceRegion(resource);
this.resourceRegionHttpMessageConverter.write(resourceRegion, mediaType, outputMessage);
} else {
this.resourceRegionHttpMessageConverter.write(HttpRange.toResourceRegions(ex, resource), mediaType, outputMessage);
}
} catch (IllegalArgumentException var9) {
response.setHeader("Content-Range", "bytes */" + resource.contentLength());
response.sendError(416);
}
}
}
}
}
}
1.根據request獲取資源文件
org.springframework.web.servlet.resource.ResourceHttpRequestHandler#getResource(HttpServletRequest request)
protected Resource getResource(HttpServletRequest request) throws IOException {
// 1.獲取文件路徑,資源請求request在過濾鏈中解析了path並封裝到attribute中
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");
} else {
path = this.processPath(path);
if(StringUtils.hasText(path) && !this.isInvalidPath(path)) {
if(path.contains("%")) {
try {
if(this.isInvalidPath(URLDecoder.decode(path, "UTF-8"))) {
if(logger.isTraceEnabled()) {
logger.trace("Ignoring invalid resource path with escape sequences [" + path + "].");
}
return null;
}
} catch (IllegalArgumentException var6) {
;
}
}
DefaultResourceResolverChain resolveChain = new DefaultResourceResolverChain(this.getResourceResolvers());
//2.ResolveResource解析資源文件路徑,並將文件封裝到Resource並返回
Resource resource = resolveChain.resolveResource(request, path, this.getLocations());
if(resource != null && !this.getResourceTransformers().isEmpty()) {
DefaultResourceTransformerChain transformChain = new DefaultResourceTransformerChain(resolveChain, this.getResourceTransformers());
resource = transformChain.transform(request, resource);
return resource;
} else {
return resource;
}
} else {
if(logger.isTraceEnabled()) {
logger.trace("Ignoring invalid resource path [" + path + "]");
}
return null;
}
}
}
根據Resource resource = resolveChain.resolveResource(request, path, this.getLocations());
查找調用關係,可定準到抽象類org.springframework.web.servlet.resource.AbstractResourceResolver#resolveResourceInternal
,該抽象類是用於解析文件,不同文件協議對其有實現,如下圖。
繼續閱讀代碼,spring通過xml配置定義的locations遍歷(org.springframework.web.servlet.resource.PathResourceResolver#getResource(java.lang.String, javax.servlet.http.HttpServletRequest, java.util.List<? extends org.springframework.core.io.Resource>)
)查找對應的文件路徑,並封裝到Resource實現類。
2.Resource
Resource是spring對各種靜態資源文件的高度封裝接口,主要方法及繼承類如下圖。
Spring如何解析文件類型
以上是解決文件來源及文件流的問題,開發過文件下載接口的讀者可能比較熟悉,如果在http response返回時不設置content-type即採用默認的text/html
或*/*
則只能實現流下載,而不能實現如瀏覽器在線預覽的功能,這都是由於content-type未設置爲文件內容的具體類型,瀏覽器接收到response不能根據content-type正確解析流內容,無法調用內核插件如pdf來顯示相關內容,而彈出下載窗口。
org.springframework.web.servlet.resource.ResourceHttpRequestHandler#getMediaType(javax.servlet.http.HttpServletRequest, org.springframework.core.io.Resource)
找到調用鏈來看看Spring具體是如何解析content-type
1.getMediaType
方法
protected MediaType getMediaType(HttpServletRequest request, Resource resource) {
MediaType mediaType = this.getMediaType(resource);
// 查找具體實現類調用
return mediaType != null?mediaType:this.contentNegotiationStrategy.getMediaTypeForResource(resource);
}
2.org.springframework.web.accept.ServletPathExtensionContentNegotiationStrategy#getMediaTypeForResource
this.contentNegotiationStrategy.getMediaTypeForResource(resource)
的具體實現是由ServletPathExtensionContentNegotiationStrategy類覆蓋方法。
public MediaType getMediaTypeForResource(Resource resource) {
MediaType mediaType = null;
if(this.servletContext != null) {
// servletContext由tomcat或jetty容器實現
String superMediaType = this.servletContext.getMimeType(resource.getFilename());
if(StringUtils.hasText(superMediaType)) {
mediaType = MediaType.parseMediaType(superMediaType);
}
}
if(mediaType == null || MediaType.APPLICATION_OCTET_STREAM.equals(mediaType)) {
MediaType superMediaType1 = super.getMediaTypeForResource(resource);
if(superMediaType1 != null) {
mediaType = superMediaType1;
}
}
return mediaType;
}
Web容器在啓動的過程中,會爲每個Web應用程序創建一個對應的ServletContext對象,它代表了當前的Web應用,爲Spring IoC容器提供宿主環境,在部署Web工程的時候,Web容器會讀取web.xml,創建ServletContext,當前Web工程所有部分都共享這個Context。
ServletContext
的繼承關係如下圖
SpringBoot啓動容器時加載MimeMappings類
AbstractConfigurableEmbeddedServletContainer初始化ServletContext對象時指定MimeMappings爲DEFAULT,被Spring用來解析文件名,其中定義了大量的http content-type,有興趣的讀者可以順着以上的邏輯找到對應的實現。