Servlet - Upload、Download、Async、动态注册
標簽 : Java與Web
Upload-上傳
隨著3.0版本的發布,文件上傳終于成為Servlet規范的一項內置特性,不再依賴于像Commons FileUpload之類組件,因此在服務端進行文件上傳編程變得不費吹灰之力.
客戶端
要上傳文件, 必須利用multipart/form-data設置HTML表單的enctype屬性,且method必須為POST:
<form action="simple_file_upload_servlet.do" method="POST" enctype="multipart/form-data"><table align="center" border="1" width="50%"><tr><td>Author:</td><td><input type="text" name="author"></td></tr><tr><td>Select file to Upload:</td><td><input type="file" name="file"></td></tr><tr><td><input type="submit" value="上傳"></td></tr></table> </form>服務端
服務端Servlet主要圍繞著@MultipartConfig注解和Part接口:
處理上傳文件的Servlet必須用@MultipartConfig注解標注:
| fileSizeThreshold | The size threshold after which the file will be written to disk |
| location | The directory location where files will be stored |
| maxFileSize | The maximum size allowed for uploaded files. |
| maxRequestSize | The maximum size allowed for multipart/form-data requests |
在一個由多部件組成的請求中, 每一個表單域(包括非文件域), 都會被封裝成一個Part,HttpServletRequest中提供如下兩個方法獲取封裝好的Part:
| Part getPart(String name) | Gets the Part with the given name. |
| Collection<Part> getParts() | Gets all the Part components of this request, provided that it is of type multipart/form-data. |
Part中提供了如下常用方法來獲取/操作上傳的文件/數據:
| InputStream getInputStream() | Gets the content of this part as an InputStream |
| void write(String fileName) | A convenience method to write this uploaded item to disk. |
| String getSubmittedFileName() | Gets the file name specified by the client(需要有Tomcat 8.x 及以上版本支持) |
| long getSize() | Returns the size of this fille. |
| void delete() | Deletes the underlying storage for a file item, including deleting any associated temporary disk file. |
| String getName() | Gets the name of this part |
| String getContentType() | Gets the content type of this part. |
| Collection<String> getHeaderNames() | Gets the header names of this Part. |
| String getHeader(String name) | Returns the value of the specified mime header as a String. |
文件流解析
通過抓包獲取到客戶端上傳文件的數據格式:
------WebKitFormBoundaryXJ6TxfJ9PX5hJHGh Content-Disposition: form-data; name="author"feiqing ------WebKitFormBoundaryXJ6TxfJ9PX5hJHGh Content-Disposition: form-data; name="file"; filename="memcached.txt" Content-Type: text/plain------WebKitFormBoundaryXJ6TxfJ9PX5hJHGh-- 可以看到:Content-Disposition與Content-Type.
在Servlet中處理上傳文件時, 需要:
- 通過查看是否存在`Content-Type`標頭, 檢驗一個Part是封裝的普通表單域,還是文件域. - 若有`Content-Type`存在, 但文件名為空, 則表示沒有選擇要上傳的文件. - 如果有文件存在, 則可以調用`write()`方法來寫入磁盤, 調用同時傳遞一個絕對路徑, 或是相對于`@MultipartConfig`注解的`location`屬性的相對路徑.- SimpleFileUploadServlet
優化
- 善用WEB-INF
存放在/WEB-INF/目錄下的資源無法在瀏覽器地址欄直接訪問, 利用這一特點可將某些受保護資源存放在WEB-INF目錄下, 禁止用戶直接訪問(如用戶上傳的可執行文件,如JSP等),以防被惡意執行, 造成服務器信息泄露等危險.
- 文件名亂碼
當文件名包含中文時,可能會出現亂碼,其解決方案與POST相同:
- 避免文件同名
如果上傳同名文件,會造成文件覆蓋.因此可以為每份文件生成一個唯一ID,然后連接原始文件名:
- 目錄打散
如果一個目錄下存放的文件過多, 會導致文件檢索速度下降,因此需要將文件打散存放到不同目錄中, 在此我們采用Hash打散法(根據文件名生成Hash值, 取Hash值的前兩個字符作為二級目錄名), 將文件分布到一個二級目錄中:
采用Hash打散的好處是:在根目錄下最多生成16個目錄,而每個子目錄下最多再生成16個子子目錄,即一共256個目錄,且分布較為均勻.
示例-簡易存儲圖片服務器
需求: 提供上傳圖片功能, 為其生成外鏈, 并提供下載功能(見下)
- 客戶端
- 服務端
由于getSubmittedFileName()方法需要有Tomcat 8.X以上版本的支持, 因此為了通用期間, 我們自己解析content-disposition請求頭, 獲取filename.
Download-下載
文件下載是向客戶端響應二進制數據(而非字符),瀏覽器不會直接顯示這些內容,而是會彈出一個下載框, 提示下載信息.
為了將資源發送給瀏覽器, 需要在Servlet中完成以下工作:
- 使用Content-Type響應頭來規定響應體的MIME類型, 如image/pjpeg、application/octet-stream;
- 添加Content-Disposition響應頭,賦值為attachment;filename=xxx.yyy, 設置文件名;
- 使用response.getOutputStream()給瀏覽器發送二進制數據;
文件名中文亂碼
當文件名包含中文時(attachment;filename=文件名.后綴名),在下載框中會出現亂碼, 需要對文件名編碼后在發送, 但不同的瀏覽器接收的編碼方式不同:
* FireFox: Base64編碼* 其他大部分Browser: URL編碼因此最好將其封裝成一個通用方法:
private String filenameEncoding(String filename, HttpServletRequest request) throws IOException {// 根據瀏覽器信息判斷if (request.getHeader("User-Agent").contains("Firefox")) {filename = String.format("=?utf-8?B?%s?=", BaseEncoding.base64().encode(filename.getBytes("UTF-8")));} else {filename = URLEncoder.encode(filename, "utf-8");}return filename; }示例-IFS下載功能
/*** @author jifang.* @since 2016/5/9 17:50.*/ @WebServlet(name = "ImageFileDownloadServlet", urlPatterns = "/ifs_download.action") public class ImageFileDownloadServlet extends HttpServlet {@Overrideprotected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {response.setContentType("application/octet-stream");String fileLocation = request.getParameter("location");String fileName = fileLocation.substring(fileLocation.lastIndexOf("/") + 1);response.setHeader("Content-Disposition", "attachment;filename=" + filenameEncoding(fileName, request));ByteStreams.copy(new FileInputStream(fileLocation), response.getOutputStream());}@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {doGet(req, resp);} }Async-異步處理
Servlet/Filter默認會一直占用請求處理線程, 直到它完成任務.如果任務耗時長久, 且并發用戶請求量大, Servlet容器將會遇到超出線程數的風險.
Servlet 3.0 中新增了一項特性, 用來處理異步操作. 當Servlet/Filter應用程序中有一個/多個長時間運行的任務時, 你可以選擇將任務分配給一個新的線程, 從而將當前請求處理線程返回到線程池中,釋放線程資源,準備為下一個請求服務.
異步Servlet/Filter
- 異步支持
@WebServlet/@WebFilter注解提供了新的asyncSupport屬性:
同樣部署描述符中也添加了<async-supportted/>標簽:
<servlet><servlet-name>HelloServlet</servlet-name><servlet-class>com.fq.web.servlet.HelloServlet</servlet-class><async-supported>true</async-supported> </servlet>- Servlet/Filter
支持異步處理的Servlet/Filter可以通過在ServletRequest中調用startAsync()方法來啟動新線程:
| AsyncContext startAsync() | Puts this request into asynchronous mode, and initializes its AsyncContext with the original (unwrapped) ServletRequest and ServletResponse objects. |
| AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) | Puts this request into asynchronous mode, and initializes its AsyncContext with the given request and response objects. |
注意:
1. 只能將原始的ServletRequest/ServletResponse或其包裝器(Wrapper/Decorator,詳見Servlet - Listener、Filter、Decorator)傳遞給第二個startAsync()方法.
2. 重復調用startAsync()方法會返回相同的AsyncContext實例, 如果在不支持異步處理的Servlet/Filter中調用, 會拋出java.lang.IllegalStateException異常.
3. AsyncContext的start()方法不會造成方法阻塞.
這兩個方法都返回AsyncContext實例, AsyncContext中提供了如下常用方法:
| void start(Runnable run) | Causes the container to dispatch a thread, possibly from a managed thread pool, to run the specified Runnable. |
| void dispatch(String path) | Dispatches the request and response objects of this AsyncContext to the given path. |
| void dispatch(ServletContext context, String path) | Dispatches the request and response objects of this AsyncContext to the given path scoped to the given context. |
| void addListener(AsyncListener listener) | Registers the given AsyncListener with the most recent asynchronous cycle that was started by a call to one of the ServletRequest.startAsync() methods. |
| ServletRequest getRequest() | Gets the request that was used to initialize this AsyncContext by calling ServletRequest.startAsync() or ServletRequest.startAsync(ServletRequest, ServletResponse). |
| ServletResponse getResponse() | Gets the response that was used to initialize this AsyncContext by calling ServletRequest.startAsync() or ServletRequest.startAsync(ServletRequest, ServletResponse). |
| boolean hasOriginalRequestAndResponse() | Checks if this AsyncContext was initialized with the original or application-wrapped request and response objects. |
| void setTimeout(long timeout) | Sets the timeout (in milliseconds) for this AsyncContext. |
在異步Servlet/Filter中需要完成以下工作, 才能真正達到異步的目的:
- 調用AsyncContext的start()方法, 傳遞一個執行長時間任務的Runnable;
- 任務完成時, 在Runnable內調用AsyncContext的complete()方法或dispatch()方法
示例-改造文件上傳
在前面的圖片存儲服務器中, 如果上傳圖片過大, 可能會耗時長久,為了提升服務器性能, 可將其改造為異步上傳(其改造成本較小):
@Override protected void doPost(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {final AsyncContext asyncContext = request.startAsync();asyncContext.start(new Runnable() {@Overridepublic void run() {try {request.setCharacterEncoding("UTF-8");response.setContentType("text/html;charset=UTF-8");PrintWriter writer = response.getWriter();Part image = request.getPart("image");final String fileName = getFileName(image);if (isFileValid(image, fileName) && isImageValid(fileName)) {String destFileName = generateDestFileName(fileName);String twoLevelDir = generateTwoLevelDir(destFileName);// 保存文件String saveDir = String.format("%s/%s/", getServletContext().getRealPath(SAVE_ROOT_DIR), twoLevelDir);makeDirs(saveDir);image.write(saveDir + destFileName);// 生成外鏈String ip = request.getLocalAddr();int port = request.getLocalPort();String path = request.getContextPath();String urlPrefix = String.format("http://%s:%s%s", ip, port, path);String urlSuffix = String.format("%s/%s/%s", SAVE_ROOT_DIR, twoLevelDir, destFileName);String url = urlPrefix + urlSuffix;String result = String.format("<a href=%s>%s</a><hr/><a href=ifs_download.action?location=%s>下載</a>",url,url,saveDir + destFileName);writer.print(result);} else {writer.print("Error : Image Type Error");}asyncContext.complete();} catch (ServletException | IOException e) {LOGGER.error("error: ", e);}}}); }注意: Servlet異步支持只適用于長時間運行,且想讓用戶知道執行結果的任務. 如果只有長時間, 但用戶不需要知道處理結果,那么只需提供一個Runnable提交給Executor, 并立即返回即可.
AsyncListener
Servlet 3.0 還新增了一個AsyncListener接口, 以便通知用戶在異步處理期間發生的事件, 該接口會在異步操作的啟動/完成/失敗/超時情況下調用其對應方法:
- ImageUploadListener
與其他監聽器不同, 他沒有@WebListener標注AsyncListener的實現, 因此必須對有興趣收到通知的每個AsyncContext都手動注冊一個AsyncListener:
asyncContext.addListener(new ImageUploadListener());動態注冊
動態注冊是Servlet 3.0新特性,它不需要重新加載應用便可安裝新的Web對象(Servlet/Filter/Listener等).
API支持
為了使動態注冊成為可能, ServletContext接口添加了如下方法用于 創建/添加 Web對象:
| Create | |
| <T extends Servlet> T createServlet(Class<T> clazz) | Instantiates the given Servlet class. |
| <T extends Filter> T createFilter(Class<T> clazz) | Instantiates the given Filter class. |
| <T extends EventListener> T createListener(Class<T> clazz) | Instantiates the given EventListener class. |
| Add | |
| ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet) | Registers the given servlet instance with this ServletContext under the given servletName. |
| FilterRegistration.Dynamic addFilter(String filterName, Filter filter) | Registers the given filter instance with this ServletContext under the given filterName. |
| <T extends EventListener> void addListener(T t) | Adds the given listener to this ServletContext. |
| Create & And | |
| ServletRegistration.Dynamic addServlet(String servletName, Class<? extends Servlet> servletClass) | Adds the servlet with the given name and class type to this servlet context. |
| ServletRegistration.Dynamic addServlet(String servletName, String className) | Adds the servlet with the given name and class name to this servlet context. |
| FilterRegistration.Dynamic addFilter(String filterName, Class<? extends Filter> filterClass) | Adds the filter with the given name and class type to this servlet context. |
| FilterRegistration.Dynamic addFilter(String filterName, String className) | Adds the filter with the given name and class name to this servlet context. |
| void addListener(Class<? extends EventListener> listenerClass) | Adds a listener of the given class type to this ServletContext. |
| void addListener(String className) | Adds the listener with the given class name to this ServletContext. |
其中addServlet()/addFilter()方法的返回值是ServletRegistration.Dynamic/FilterRegistration.Dynamic,他們都是Registration.Dynamic的子接口,用于動態配置Servlet/Filter實例.
示例-DynamicServlet
動態注冊DynamicServlet, 注意: 并未使用web.xml或@WebServlet靜態注冊DynamicServlet實例, 而是用DynRegListener在服務器啟動時動態注冊.
- DynamicServlet
- DynRegListener
容器初始化
在使用類似SpringMVC這樣的MVC框架時,需要首先注冊DispatcherServlet到web.xml以完成URL的轉發映射:
<!-- 配置SpringMVC --> <servlet><servlet-name>mvc</servlet-name><servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class><init-param><param-name>contextConfigLocation</param-name><param-value>classpath:spring/mvc-servlet.xml</param-value></init-param><load-on-startup>1</load-on-startup> </servlet> <servlet-mapping><servlet-name>mvc</servlet-name><url-pattern>*.do</url-pattern> </servlet-mapping>在Servlet 3.0中,通過Servlet容器初始化,可以自動完成Web對象的首次注冊,因此可以省略這個步驟.
API支持
容器初始化的核心是javax.servlet.ServletContainerInitializer接口,他只包含一個方法:
| void onStartup(Set<Class<?>> c, ServletContext ctx) | Notifies this ServletContainerInitializer of the startup of the application represented by the given ServletContext. |
在執行任何ServletContext監聽器之前, 由Servlet容器自動調用onStartup()方法.
注意: 任何實現了ServletContainerInitializer的類必須使用@HandlesTypes注解標注, 以聲明該初始化程序可以處理這些類型的類.
實例-SpringMVC初始化
利用Servlet容器初始化, SpringMVC可實現容器的零配置注冊.
- SpringServletContainerInitializer
SpringMVC為ServletContainerInitializer提供了實現類SpringServletContainerInitializer通過查看源代碼可以知道,我們只需提供WebApplicationInitializer的實現類到classpath下, 即可完成對所需Servlet/Filter/Listener的注冊.
public interface WebApplicationInitializer {void onStartup(ServletContext servletContext) throws ServletException; }詳細可參考springmvc基于java config的實現
- javax.servlet.ServletContainerInitializer
元數據文件javax.servlet.ServletContainerInitializer只有一行內容(即實現了ServletContainerInitializer類的全限定名),該文本文件必須放在jar包的META-INF/services目錄下.
轉載于:https://www.cnblogs.com/itrena/p/5926895.html
總結
以上是生活随笔為你收集整理的Servlet - Upload、Download、Async、动态注册的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 对于理想的团队模式的设想和对软件流程的理
- 下一篇: jQuery选择器大全(48个代码片段+