Servlet – Upload、Download、Async、動態註冊

Java菜分享發表於2018-10-08

Upload-上傳 隨著3.0版本的釋出,檔案上傳終於成為Servlet規範的一項內建特性,不再依賴於像Commons FileUpload之類元件,因此在服務端進行檔案上傳程式設計變得不費吹灰之力.

客戶端 要上傳檔案, 必須利用multipart/form-data設定HTML表單的enctype屬性,且method必須為POST:

Author:
Select file to Upload:
服務端 服務端Servlet主要圍繞著@MultipartConfig註解和Part介面:

處理上傳檔案的Servlet必須用@MultipartConfig註解標註:

@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:

HttpServletRequest 描述 Part getPart(String name) Gets the Part with the given name. Collection getParts() Gets all the Part components of this request, provided that it is of type multipart/form-data. Part中提供瞭如下常用方法來獲取/操作上傳的檔案/資料:

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 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-- 可以看到: A. 如果HTML表單輸入項為文字(),將只包含一個請求頭Content-Disposition. B. 如果HTML表單輸入項為檔案(), 則包含兩個頭: Content-Disposition與Content-Type. 在Servlet中處理上傳檔案時, 需要:

- 通過檢視是否存在Content-Type標頭, 檢驗一個Part是封裝的普通表單域,還是檔案域. - 若有Content-Type存在, 但檔名為空, 則表示沒有選擇要上傳的檔案. - 如果有檔案存在, 則可以呼叫write()方法來寫入磁碟, 呼叫同時傳遞一個絕對路徑, 或是相對於@MultipartConfig註解的location屬性的相對路徑. SimpleFileUploadServlet

/**

@author jifang. @since 2016/5/8 16:27.br/>*/ @MultipartConfig @WebServlet(name = "SimpleFileUploadServlet", urlPatterns = "/simple_file_upload_servlet.do") public class SimpleFileUploadServlet extends HttpServlet {

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=UTF-8"); PrintWriter writer = response.getWriter(); Part file = request.getPart("file"); if (!isFileValid(file)) { writer.print("

請確認上傳檔案是否正確!"); } else { String fileName = file.getSubmittedFileName(); String saveDir = getServletContext().getRealPath("/WEB-INF/files/"); mkdirs(saveDir); file.write(saveDir + fileName);

writer.print("<h3>Uploaded file name: " + fileName);
writer.print("<h3>Size: " + file.getSize());
writer.print("<h3>Author: " + request.getParameter("author"));
複製程式碼

} }

private void mkdirs(String saveDir) { File dir = new File(saveDir); if (!dir.exists()) { dir.mkdirs(); } }

private boolean isFileValid(Part file) { // 上傳的並非檔案 if (file.getContentType() == null) { return false; } // 沒有選擇任何檔案 else if (Strings.isNullOrEmpty(file.getSubmittedFileName())) { return false; } return true; } } 優化 善用WEB-INF 存放在/WEB-INF/目錄下的資源無法在瀏覽器位址列直接訪問, 利用這一特點可將某些受保護資源存放在WEB-INF目錄下, 禁止使用者直接訪問(如使用者上傳的可執行檔案,如JSP等),以防被惡意執行, 造成伺服器資訊洩露等危險.

getServletContext().getRealPath("/WEB-INF/") 檔名亂碼 當檔名包含中文時,可能會出現亂碼,其解決方案與POST相同: 1 request.setCharacterEncoding("UTF-8"); 避免檔案同名 如果上傳同名檔案,會造成檔案覆蓋.因此可以為每份檔案生成一個唯一ID,然後連線原始檔名:

private String generateUUID() { return UUID.randomUUID().toString().replace("-", "_"); } 目錄打散 如果一個目錄下存放的檔案過多, 會導致檔案檢索速度下降,因此需要將檔案打散存放到不同目錄中, 在此我們採用Hash打散法(根據檔名生成Hash值, 取Hash值的前兩個字元作為二級目錄名), 將檔案分佈到一個二級目錄中: 1 2 3 4 private String generateTwoLevelDir(String destFileName) { String hash = Integer.toHexString(destFileName.hashCode()); return String.format("%s/%s", hash.charAt(0), hash.charAt(1)); } 採用Hash打散的好處是:在根目錄下最多生成16個目錄,而每個子目錄下最多再生成16個子子目錄,即一共256個目錄,且分佈較為均勻.

示例-簡易儲存圖片伺服器 需求: 提供上傳圖片功能, 為其生成外鏈, 並提供下載功能(見下)

客戶端

IFS
br/>服務端 @MultipartConfig @WebServlet(name = "ImageFileUploadServlet", urlPatterns = "/ifs_upload.action") public class ImageFileUploadServlet extends HttpServlet {

private Set imageSuffix = new HashSet<>();

private static final String SAVE_ROOT_DIR = "/images";

{ imageSuffix.add(".jpg"); imageSuffix.add(".png"); imageSuffix.add(".jpeg"); }

@Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); response.setContentType("text/html;charset=UTF-8"); PrintWriter writer = response.getWriter(); Part image = request.getPart("image"); 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");
}
複製程式碼

}

/**

  • 校驗檔案表單域有效

  • @param file

  • @param fileName

  • @return */ private boolean isFileValid(Part file, String fileName) { // 上傳的並非檔案 if (file.getContentType() == null) { return false; } // 沒有選擇任何檔案 else if (Strings.isNullOrEmpty(fileName)) { return false; }

    return true; }

/**

  • 校驗檔案字尾有效
  • @param fileName
  • @return */ private boolean isImageValid(String fileName) { for (String suffix : imageSuffix) { if (fileName.endsWith(suffix)) { return true; } } return false; }

/**

  • 加速圖片訪問速度, 生成兩級存放目錄
  • @param destFileName
  • @return */ private String generateTwoLevelDir(String destFileName) { String hash = Integer.toHexString(destFileName.hashCode()); return String.format("%s/%s", hash.charAt(0), hash.charAt(1)); }

private String generateUUID() { return UUID.randomUUID().toString().replace("-", "_"); }

private String generateDestFileName(String fileName) { String destFileName = generateUUID(); int index = fileName.lastIndexOf("."); if (index != -1) { destFileName += fileName.substring(index); } return destFileName; }

private String getFileName(Part part) { String[] elements = part.getHeader("content-disposition").split(";"); for (String element : elements) { if (element.trim().startsWith("filename")) { return element.substring(element.indexOf("=") + 1).trim().replace(""", ""); } } return null; }

private void makeDirs(String saveDir) { File dir = new File(saveDir); if (!dir.exists()) { dir.mkdirs(); } } } 由於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 {

@Override protected 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()); }

@Override protected 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屬性:

@WebFilter(asyncSupported = true) @WebServlet(asyncSupported = true) 同樣部署描述符中也新增了標籤:

HelloServlet com.fq.web.servlet.HelloServlet true Servlet/Filter 支援非同步處理的Servlet/Filter可以通過在ServletRequest中呼叫startAsync()方法來啟動新執行緒: ServletRequest 描述 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. 注意:

只能將原始的ServletRequest/ServletResponse或其包裝器(Wrapper/Decorator,詳見Servlet – Listener、Filter、Decorator)傳遞給第二個startAsync()方法. 重複呼叫startAsync()方法會返回相同的AsyncContext例項, 如果在不支援非同步處理的Servlet/Filter中呼叫, 會丟擲java.lang.IllegalStateException異常. AsyncContext的start()方法不會造成方法阻塞. 這兩個方法都返回AsyncContext例項, 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() {br/>@Override public 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 /**

@author jifang. @since 2016/5/10 17:33. */ public class ImageUploadListener implements AsyncListener {

@Override public void onComplete(AsyncEvent event) throws IOException { System.out.println("onComplete..."); }

@Override public void onTimeout(AsyncEvent event) throws IOException { System.out.println("onTimeout..."); }

@Override public void onError(AsyncEvent event) throws IOException { System.out.println("onError..."); }

@Override public void onStartAsync(AsyncEvent event) throws IOException { System.out.println("onStartAsync..."); } } 與其他監聽器不同, 他沒有@WebListener標註AsyncListener的實現, 因此必須對有興趣收到通知的每個AsyncContext都手動註冊一個AsyncListener:

1 asyncContext.addListener(new ImageUploadListener()); 動態註冊 動態註冊是Servlet 3.0新特性,它不需要重新載入應用便可安裝新的Web物件(Servlet/Filter/Listener等).

API支援 為了使動態註冊成為可能, ServletContext介面新增了如下方法用於 建立/新增 Web物件:

ServletContext 描述 Create T createServlet(Class clazz) Instantiates the given Servlet class. T createFilter(Class clazz) Instantiates the given Filter class. T createListener(Class 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. 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

/**

@author jifang. @since 2016/5/13 16:41. */ public class DynamicServlet extends HttpServlet {

private String dynamicName;

@Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.getWriter().print("

DynamicServlet, MyDynamicName: " + getDynamicName() + "

"); }

public String getDynamicName() { return dynamicName; }

public void setDynamicName(String dynamicName) { this.dynamicName = dynamicName; } } DynRegListener

@WebListener public class DynRegListener implements ServletContextListener {

@Override public void contextInitialized(ServletContextEvent sce) { ServletContext context = sce.getServletContext();

DynamicServlet servlet;
try {
    servlet = context.createServlet(DynamicServlet.class);
} catch (ServletException e) {
    servlet = null;
}

if (servlet != null) {
    servlet.setDynamicName("Hello fQ Servlet");
    ServletRegistration.Dynamic dynamic = context.addServlet("dynamic_servlet", servlet);
    dynamic.addMapping("/dynamic_servlet.do");
}
複製程式碼

}

@Override public void contextDestroyed(ServletContextEvent sce) { } } 容器初始化 在使用類似SpringMVC這樣的MVC框架時,需要首先註冊DispatcherServlet到web.xml以完成URL的轉發對映:

mvc org.springframework.web.servlet.DispatcherServlet contextConfigLocation classpath:spring/mvc-servlet.xml 1 mvc *.do 在Servlet 3.0中,通過Servlet容器初始化,可以自動完成Web物件的首次註冊,因此可以省略這個步驟. API支援 容器初始化的核心是javax.servlet.ServletContainerInitializer介面,他只包含一個方法: ServletContainerInitializer 描述 void onStartup(Set> 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 @HandlesTypes(WebApplicationInitializer.class) public class SpringServletContainerInitializer implements ServletContainerInitializer { @Override public void onStartup(Set> webAppInitializerClasses, ServletContext servletContext) throws ServletException { List initializers = new LinkedList(); if (webAppInitializerClasses != null) { for (Class waiClass : webAppInitializerClasses) { // Be defensive: Some servlet containers provide us with invalid classes, // no matter what @HandlesTypes says... if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) && WebApplicationInitializer.class.isAssignableFrom(waiClass)) { try { initializers.add((WebApplicationInitializer) waiClass.newInstance()); } catch (Throwable ex) { throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex); } } } } if (initializers.isEmpty()) { servletContext.log("No Spring WebApplicationInitializer types detected on classpath"); return; } AnnotationAwareOrderComparator.sort(initializers); servletContext.log("Spring WebApplicationInitializers detected on classpath: " + initializers); for (WebApplicationInitializer initializer : initializers) { initializer.onStartup(servletContext); } } } SpringMVC為ServletContainerInitializer提供了實現類SpringServletContainerInitializer通過檢視原始碼可以知道,我們只需提供WebApplicationInitializer的實現類到classpath下, 即可完成對所需Servlet/Filter/Listener的註冊. public interface WebApplicationInitializer { void onStartup(ServletContext servletContext) throws ServletException; } 詳細可參考springmvc基於java config的實現 javax.servlet.ServletContainerInitializer 1 org.springframework.web.SpringServletContainerInitializer 後設資料檔案javax.servlet.ServletContainerInitializer只有一行內容(即實現了ServletContainerInitializer類的全限定名),該文字檔案必須放在jar包的META-INF/services目錄下

Select A Image to Upload:

相關文章