1. 程式人生 > >基於Spring搭建檔案伺服器

基於Spring搭建檔案伺服器

檔案伺服器的搭建架構有很多種,如基於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對各種靜態資原始檔的高度封裝介面,主要方法及繼承類如下圖。 Resource繼承類

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) SpringBoot啟動時載入MimeMappings類 AbstractConfigurableEmbeddedServletContainer初始化環境時指定MimeMappings為DEFAULT,被Spring用來解析檔名,其中定義了大量的http content-type