摘要:上篇文章介紹了通用文件服務組件實現(xiàn)版本,本文介紹基于與的方式實現(xiàn)。根據(jù)官網(wǎng)信息可知,它是一個高性能,可為我們的應用提供文件上傳服務的組件。程序可通過該對象獲取服務器的響應內(nèi)容。
上篇文章介紹了通用文件服務組件(Netty實現(xiàn)版本),本文介紹基于HTTP與Apache FileUpload的方式實現(xiàn)。1. 實現(xiàn)原理及功能簡介 1.1 服務端實現(xiàn)原理
代碼地址:https://github.com/landy8530/...
服務端采用了Spring MVC文件上傳的方式,其底層采用了Apache FileUpload組件。
1.1.1 Apache FileUpload根據(jù)Apache FileUpload官網(wǎng)信息可知,它是一個高性能,可為我們的web應用提供文件上傳服務的組件。
The Commons FileUpload package makes it easy to add robust, high-performance, file upload capability to your servlets and web applications.FileUpload parses HTTP requests which conform to RFC 1867, "Form-based File Upload in HTML". That is, if an HTTP request is submitted using the POST method, and with a content type of "multipart/form-data", then FileUpload can parse that request, and make the results available in a manner easily used by the caller.
Starting with version 1.3, FileUpload handles RFC 2047 encoded header values.
Apache Commons FileUpload主要依賴Commons-io組件,詳情可參考 Apache FileUpload Dependencies。
使用Commons FileUpload需要注意以下幾點:
Form表單內(nèi),需要添加控件
Form表單的內(nèi)容格式需要定義成multipart/form-data
需要類庫
commons-io
commons-fileupload
通過閱讀Apache Commons FileUpload官方文檔,可以發(fā)現(xiàn)以下幾個常見的對象,
文件解析對象
DiskFileUpload diskFileUpload = new DiskFileUpload();//磁盤中讀取 ServletFileUpload servletFileUpload = new ServletFileUpload();//web servlet中讀取
進行文件解析后放在List中,因為這個類庫支持多個文件上傳,因此把結(jié)果會存在List中
Listlist = diskFileUpload.parseRequest(request);
獲取上傳文件,進行分析(不是必須)
File remoteFile = new File(new String(fileItem.getName().getBytes(),"UTF-8"));
創(chuàng)建新對象,進行流拷貝
file1 = new File(this.getServletContext().getRealPath("attachment"),remoteFile.getName()); file1.getParentFile().mkdirs(); file1.createNewFile(); InputStream ins = fileItem.getInputStream(); OutputStream ous = new FileOutputStream(file1); try{ byte[] buffer = new byte[1024]; int len = 0; while((len = ins.read(buffer)) > -1) ous.write(buffer,0,len); out.println("以保存文件"+file1.getAbsolutePath()+"
"); }finally{ ous.close(); ins.close(); }
關(guān)于其他問題可以自行Google或者參考官方文檔。
1.1.2 Spring MVC文件上傳原理我們大家都知道Spring MVC的入口類為 org.springframework.web.servlet.DispatcherServlet,入口方法為doDispatch。通過這個方法可以找到文件處理相關(guān)的方法 checkMultipart ,如下所示:
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { ... processedRequest = checkMultipart(request); multipartRequestParsed = (processedRequest != request); ... }
該方法定義如下:
/** * Convert the request into a multipart request, and make multipart resolver available. *If no multipart resolver is set, simply use the existing request. * @param request current HTTP request * @return the processed request (multipart wrapper if necessary) * @see MultipartResolver#resolveMultipart */ protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException { if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) { ...... try { return this.multipartResolver.resolveMultipart(request); } catch (MultipartException ex) { ...... } } // If not returned before: return original request. return request; }
上述方法中定義的multipartResolver對象即為 org.springframework.web.multipart.MultipartResolver類型,其中一個實現(xiàn)類為org.springframework.web.multipart.commons.CommonsMultipartResolver,需要在配置類org.fortune.doc.server.conf.web.SpringWebMvcConfiguration 加入Bean定義。如下所示:
@Bean public CommonsMultipartResolver multipartResolver() { CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver(); return multipartResolver; }
我們進入到 CommonsMultipartResolver中可以發(fā)現(xiàn)其底層就是利用了Apache FileUpload組件。
Servlet-based {@link MultipartResolver} implementation for Servlet-based {@link MultipartResolver} implementation for 1.2 or above.
通過 this.multipartResolver.resolveMultipart(request)方法我們可以知道,這個方法就是對 MultipartHttpServletRequest類的一系列初始化操作。MultipartHttpServletRequest接口具有兩個實現(xiàn)類(DefaultMultipartHttpServletRequest和StandardMultipartHttpServletRequest),此處使用 DefaultMultipartHttpServletRequest,兩種加載方法, 一種是即時加載(初始化),一種是懶加載(初始化),通過變量resolveLazily實現(xiàn)自由切換,默認為即時加載(初始化),實現(xiàn)方法如下:
@Override public MultipartHttpServletRequest resolveMultipart(final HttpServletRequest request) throws MultipartException { Assert.notNull(request, "Request must not be null"); if (this.resolveLazily) { return new DefaultMultipartHttpServletRequest(request) { @Override protected void initializeMultipart() { MultipartParsingResult parsingResult = parseRequest(request); setMultipartFiles(parsingResult.getMultipartFiles()); setMultipartParameters(parsingResult.getMultipartParameters()); setMultipartParameterContentTypes(parsingResult.getMultipartParameterContentTypes()); } }; } else { MultipartParsingResult parsingResult = parseRequest(request); return new DefaultMultipartHttpServletRequest(request, parsingResult.getMultipartFiles(), parsingResult.getMultipartParameters(), parsingResult.getMultipartParameterContentTypes()); } }
上述方法我們可以發(fā)現(xiàn)方法 MultipartParsingResult parsingResult = parseRequest(request);,這個方法其實就是讀取文件上傳相關(guān)的對象,比如文件列表List
* Parse the given servlet request, resolving its multipart elements. * @param request the request to parse * @return the parsing result * @throws MultipartException if multipart resolution failed. */ protected MultipartParsingResult parseRequest(HttpServletRequest request) throws MultipartException { String encoding = determineEncoding(request); FileUpload fileUpload = prepareFileUpload(encoding); try { ListfileItems = ((ServletFileUpload) fileUpload).parseRequest(request); return parseFileItems(fileItems, encoding); } ...... }
上述代碼很清晰的說明了我們在前一小節(jié)講到的Apache FileUpload中的用法。
通過上述介紹,我們已經(jīng)得到了上傳文件需要的一系列依賴對象,這時候我們就可以利用這些對象進行文件上傳相關(guān)的操作了,比如本組件中文件上傳時候需要得到request中的文件對象,就可以通過以下方法很方便的得到文件對象。
if (request instanceof MultipartHttpServletRequest) { MultipartHttpServletRequest mreqeust = (MultipartHttpServletRequest)request; String fileName = mreqeust.getParameter(Constants.FILE_NAME_KEY); MultipartFile file = mreqeust.getFile(Constants.FILE_DATA_KEY); ..... }
然后利用方法 org.springframework.web.multipart.MultipartFile#transferTo(java.io.File) 就可以獲取到客戶端傳過來的真正的文件內(nèi)容了。
1.2 客戶端實現(xiàn)原理為了能夠使客戶端發(fā)送Http請求變得容易,而且也方便開發(fā)人員測試接口(基于Http協(xié)議的),即提高了開發(fā)的效率,也方便提高代碼的健壯性,這時候我們自然就想到了Apache的一個基于Http的客戶端組件,它在3.x之前稱為Apache HttpClient,在4.x之后成為Apache HttpComponents。
關(guān)于Apache HttpClient組件的官方說明如下:
The Commons HttpClient project is now end of life, and is no longer being developed. It has been replaced by the Apache HttpComponents project in its HttpClient and HttpCore modules, which offer better performance and more flexibility.
關(guān)于Apache HttpComponents組件的官方說明如下:
The Apache HttpComponents? project is responsible for creating and maintaining a toolset of low level Java components focused on HTTP and associated protocols.1.2.1 HttpComponents 介紹
通過其官網(wǎng)可知,HttpComponents組件具有以下幾個主要的modules,
HttpComponents Core:核心包
HttpComponents Client:客戶端處理包
HttpComponents AsyncClient:具有異步功能的客戶端處理包
1.2.2 HttpCoreHttpCore組件就是整個Http客戶端組件的核心包,它包括兩種I/O模型,阻塞I/O模型(Java IO)和非阻塞,事件驅(qū)動的I/O模型(Java NIO)。
HttpCore is a set of low level HTTP transport components that can be used to build custom client and server side HTTP services with a minimal footprint. HttpCore supports two I/O models: blocking I/O model based on the classic Java I/O and non-blocking, event driven I/O model based on Java NIO.1.2.3 HttpClient
HttpClient組件是基于Http協(xié)議的一個組件,它依賴于HttpCore組件,它提供了可重用的在客戶端權(quán)限驗證,HTTP狀態(tài)管理,HTTP連接管理等功能。
HttpClient is a HTTP/1.1 compliant HTTP agent implementation based on HttpCore. It also provides reusable components for client-side authentication, HTTP state management, and HTTP connection management. HttpComponents Client is a successor of and replacement for Commons HttpClient 3.x. Users of Commons HttpClient are strongly encouraged to upgrade.
使用HttpClient發(fā)送請求、接收響應很簡單,一般需要如下幾步即可:
創(chuàng)建HttpClient對象,HttpClients.createDefault()。
創(chuàng)建請求方法的實例,并指定請求URL。如果需要發(fā)送GET請求,創(chuàng)建HttpGet對象;如果需要發(fā)送POST請求,創(chuàng)建HttpPost對象,HttpPost httpPost = new HttpPost(url)。
如果需要發(fā)送請求參數(shù),可調(diào)用HttpGet、HttpPost共同的setParams(HetpParams params)方法來添加請求參數(shù);對于HttpPost對象而言,也可調(diào)用setEntity(HttpEntity entity)方法來設(shè)置請求參數(shù)。
ListvaluePairs = new LinkedList (); valuePairs.add(new BasicNameValuePair(entry.getKey(),entry.getValue()));httpPost.setEntity(formEntity)
調(diào)用HttpClient對象的execute(HttpUriRequest request)發(fā)送請求,該方法返回一個HttpResponse。
調(diào)用HttpResponse的getAllHeaders()、getHeaders(String name)等方法可獲取服務器的響應頭;調(diào)用HttpResponse的getEntity()方法可獲取HttpEntity對象,該對象包裝了服務器的響應內(nèi)容。程序可通過該對象獲取服務器的響應內(nèi)容。
釋放連接。無論執(zhí)行方法是否成功,都必須釋放連接。
1.3 組件功能介紹該組件基于Apache HTTPComponents組件實現(xiàn),具有如下功能:文件上傳,文件替換,文件刪除,如果是圖片的話,還可以生成縮略圖等功能。使用簡單,只需要引入commons-doc-client-http,即可以實現(xiàn)文件的以上操作。
本組件分為三個module,分別為:
commons-doc-server-http:Http實現(xiàn)文件服務組件的服務端
commons-doc-common:Http文件服務組件公共組件
commons-doc-client-http:Http文件服務組件的客戶端
2. 服務端 2.1 功能簡介服務端組件實現(xiàn)功能與Netty實現(xiàn)版本一致,具有以下功能:文件上傳,文件替換,文件刪除,如果是圖片的話,還可以生成縮略圖等功能。
2.2 實現(xiàn)步驟同樣的,不管是普通文件,還是圖片處理,他們都有一個共同的基類 org.fortune.doc.server.handler.DocServerHandler,然后文件處理基類 org.fortune.doc.server.handler.attachment.AttachmentServerHandler繼承自DocServerHandler,同理,圖片處理基類 org.fortune.doc.server.handler.image.ImageServerHandler也繼承它。以文件處理為例,主要有以下幾個實現(xiàn)類:
UploadAttachmentServerHandler:文檔上傳處理類
ReplaceAttachmentServerHandler:文檔替換處理類
DeleteAttachmentServerHandler:文檔刪除處理類
實現(xiàn)以圖片上傳為例。
實現(xiàn)方法如下:
public ImageDocResult doUpload(HttpServletRequest request) { ImageDocResult result = new ImageDocResult(); DocAccountBean accountBean = super.getAccount(request); if (accountBean == null) { result.buildFailed(); result.buildCustomMsg("賬號信息不對,請重新確認"); return result; } else { if (request instanceof MultipartHttpServletRequest) { MultipartHttpServletRequest mreqeust = (MultipartHttpServletRequest)request; String fileName = mreqeust.getParameter(Constants.FILE_NAME_KEY); MultipartFile file = mreqeust.getFile(Constants.FILE_DATA_KEY); if (!file.isEmpty()) { try { String rootPath = super.getImageRootPath(); LOGGER.info("圖片上傳的根目錄:{}",rootPath); this.checkRootPath(rootPath); String fileExt = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase(); String savePath = super.getImageBasePath() + File.separator + FileUtil.generateFileSavePath(accountBean); LOGGER.info("圖片上傳需要保存到數(shù)據(jù)庫的目錄:{}",savePath); String dirPath = super.getRootPath() + File.separator + savePath; LOGGER.info("圖片上傳絕對路徑:{}",dirPath); String newFileName = FileUtil.generateFileNameOfTime(fileName); LOGGER.info("圖片上傳動態(tài)生成的圖片文件名稱:{}",newFileName); String newFileName0 = newFileName.substring(0, newFileName.lastIndexOf(".")); this.checkStorePath(dirPath); File uploadedFile3 = new File(dirPath, newFileName); file.transferTo(uploadedFile3); String thumbMark = request.getParameter(Constants.THUMB_MARK_KEY); if (Constants.THUMB_MARK_VAL.equals(thumbMark)) { ListthumbBeans = accountBean.getThumbConfig(); if (thumbBeans != null) { Iterator thumbBean = thumbBeans.iterator(); while(thumbBean.hasNext()) { ImageDocThumbBean thumb = (ImageDocThumbBean)thumbBean.next(); File uploadedFile1 = new File(dirPath, newFileName0 + thumb.getSuffix() + "." + fileExt); LOGGER.info("圖片上傳動態(tài)生成的縮略圖圖片文件:{}",uploadedFile1); ImageUtils.ratioZoom2(uploadedFile3, uploadedFile1, thumb.getRatio()); } } } result.setFilePath(savePath + File.separator + newFileName); result.buildSuccess(); LOGGER.info("圖片上傳成功,路徑為{}",result.getFilePath()); } catch (Exception ex) { result.buildFailed(); LOGGER.error("圖片上傳失敗", ex); } } else { result.buildFailed(); LOGGER.error("圖片上傳失敗,上傳的文件未提供"); } } return result; } }
由上述代碼可知,其核心邏輯就是判斷當前的request是否是 MultipartHttpServletRequest類型,如果是,則說明當前是文件處理邏輯,根據(jù)前面章節(jié)的說明,我們可以根據(jù)此對象獲取到相應的 MultipartFile對象進行相應的文件操作,如 file.transferTo(uploadedFile3);這句代碼即為文件上傳核心代碼。
3. 客戶端 3.1 功能簡介客戶端組件也是跟Netty實現(xiàn)版本一致,主要提供對外訪問服務端組件的接口,提供以下接口:文件上傳,文件替換,文件刪除,如果是圖片的話,還可以生成縮略圖等功能。
3.2 實現(xiàn)步驟不管是普通的文件處理,還是圖片處理,他們都有一個抽象類 org.fortune.doc.client.handler.AbstractDocClientHandler該類主要是一些參數(shù)配置等公共方法,然后其有三個子抽象類,分別如下:
AbstractUploadDocClientHandler: 上傳功能抽象句柄
AbstractReplaceDocClientHandler:替換功能抽象句柄
AbstractDeleteDocClientHandler:刪除功能抽象句柄
然后文件處理和圖片處理分別有對應的實現(xiàn)類,大家可以參考Github實現(xiàn),在此說明一下,不管是哪個實現(xiàn)類,最后都會調(diào)用一個包裝類 org.fortune.doc.client.support.DocClientWrapper,該類封裝了Apache HttpClient的基本配置和基本操作。核心方法doPost實現(xiàn)如下:
public DocResponseContent doPost(String uri, String urlEncoding) throws HttpException, IOException { HttpEntity entity = null; HttpRequestBase request = null; CloseableHttpResponse response = null; try { String url = this.buildUrl(uri); HttpPost httpPost = new HttpPost(url); MultipartEntityBuilder entityBuilder = MultipartEntityBuilder.create(); entityBuilder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE); IteratornameValuePairIterator = this.getNVBodies().iterator(); while(nameValuePairIterator.hasNext()) { NameValuePair nameValuePair = nameValuePairIterator.next(); entityBuilder.addPart(nameValuePair.getName(), new StringBody(nameValuePair.getValue(), ContentType.create("text/plain", urlEncoding))); } Iterator it = this.getContentBodies().entrySet().iterator(); while(it.hasNext()) { Map.Entry item = (Map.Entry)it.next(); entityBuilder.addPart(item.getKey(), item.getValue()); } entityBuilder.setCharset(CharsetUtils.get(urlEncoding)); httpPost.setEntity(entityBuilder.build()); request = httpPost; response = this.client.execute(httpPost); StatusLine statusLine = response.getStatusLine(); entity = response.getEntity(); DocResponseContent ret = new DocResponseContent(); ret.setStatusCode(statusLine.getStatusCode()); this.getResponseContent(entity, ret); DocResponseContent var12 = ret; return var12; } finally { this.close(entity, request, response); } }
以上方法其實就是我們前面講解的客戶端實現(xiàn)原理章節(jié)中的內(nèi)容了。
4. 操作指引該文件服務組件的使用需要分為兩個部分,一個是服務端配置與啟動,一個是客戶端的配置與啟動。
配置基本上與Netty實現(xiàn)版本是一致的,只是細微的差別。4.1 服務端配置與啟動 4.1.1 配置
服務端的配置采用yml文件的配置,更加的簡潔明了,主要的注意點是文件存放位置的配置,在開發(fā)過程中,可以有兩種方式配置:
Idea自啟動方式:如果采用此種方式則需要把rootPath配置到工程路徑下(target目錄),如下所示:
# 在idea中執(zhí)行的話,需要配置target目錄下的打包文件 rootPath: C: