標籤 : Java與Webjavascript
隨着3.0版本的發佈,文件上傳終於成爲Servlet規範的一項內置特性,再也不依賴於像Commons FileUpload之類組件,所以在服務端進行文件上傳編程變得不費吹灰之力.html
要上傳文件, 必須利用multipart/form-data
設置HTML表單的enctype屬性,且method必須爲POST
:java
<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
接口:web
處理上傳文件的Servlet必須用@MultipartConfig
註解標註:spring
@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
:apache
HttpServletRequest | 描述 |
---|---|
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
中提供了以下經常使用方法來獲取/操做上傳的文件/數據:編程
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. |
經過抓包獲取到客戶端上傳文件的數據格式:api
------WebKitFormBoundaryXJ6TxfJ9PX5hJHGh
Content-Disposition: form-data; name="author"
feiqing
------WebKitFormBoundaryXJ6TxfJ9PX5hJHGh
Content-Disposition: form-data; name="file"; filename="memcached.txt"
Content-Type: text/plain
------WebKitFormBoundaryXJ6TxfJ9PX5hJHGh--
<input type="text"/>
),將只包含一個請求頭
Content-Disposition
.
<input type="file"/>
), 則包含兩個頭:
Content-Disposition
與
Content-Type
.
在Servlet中處理上傳文件時, 須要:瀏覽器
- 經過查看是否存在`Content-Type`標頭, 檢驗一個Part是封裝的普通表單域,仍是文件域. - 如有`Content-Type`存在, 但文件名爲空, 則表示沒有選擇要上傳的文件. - 若是有文件存在, 則能夠調用`write()`方法來寫入磁盤, 調用同時傳遞一個絕對路徑, 或是相對於`@MultipartConfig`註解的`location`屬性的相對路徑.
/** * @author jifang. * @since 2016/5/8 16:27. */
@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("<h1>請確認上傳文件是否正確!");
} 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
相同:request.setCharacterEncoding("UTF-8");
private String generateUUID() {
return UUID.randomUUID().toString().replace("-", "_");
}
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個目錄,且分佈較爲均勻.ruby
需求: 提供上傳圖片功能, 爲其生成外鏈, 並提供下載功能(見下)
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>IFS</title>
</head>
<body>
<form action="ifs_upload.action" method="POST" enctype="multipart/form-data">
<table align="center" border="1" width="50%">
<tr>
<td>Select A Image to Upload:</td>
<td><input type="file" name="image"></td>
</tr>
<tr>
<td> </td>
<td><input type="submit" value="上傳"></td>
</tr>
</table>
</form>
</body>
</html>
@MultipartConfig
@WebServlet(name = "ImageFileUploadServlet", urlPatterns = "/ifs_upload.action")
public class ImageFileUploadServlet extends HttpServlet {
private Set<String> 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.
文件下載是向客戶端響應二進制數據(而非字符),瀏覽器不會直接顯示這些內容,而是會彈出一個下載框, 提示下載信息.
爲了將資源發送給瀏覽器, 須要在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;
}
/** * @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);
}
}
Servlet
/Filter
默認會一直佔用請求處理線程, 直到它完成任務.若是任務耗時長久, 且併發用戶請求量大, Servlet容器將會遇到超出線程數的風險.
Servlet 3.0 中新增了一項特性, 用來處理異步操做. 當Servlet
/Filter
應用程序中有一個/多個長時間運行的任務時, 你能夠選擇將任務分配給一個新的線程, 從而將當前請求處理線程返回到線程池中,釋放線程資源,準備爲下一個請求服務.
@WebServlet
/@WebFilter
註解提供了新的asyncSupport
屬性:@WebFilter(asyncSupported = true)
@WebServlet(asyncSupported = true)
一樣部署描述符中也添加了<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
能夠經過在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. |
注意:
1. 只能將原始的ServletRequest
/ServletResponse
或其包裝器(Wrapper/Decorator,詳見Servlet - Listener、Filter、Decorator)傳遞給第二個startAsync()
方法.
2. 重複調用startAsync()
方法會返回相同的AsyncContext
實例, 若是在不支持異步處理的Servlet
/Filter
中調用, 會拋出java.lang.IllegalStateException
異常.
3.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() {
@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
, 並當即返回便可.
Servlet 3.0 還新增了一個AsyncListener
接口, 以便通知用戶在異步處理期間發生的事件, 該接口會在異步操做的啓動/完成/失敗/超時狀況下調用其對應方法:
/** * @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
:
asyncContext.addListener(new ImageUploadListener());
動態註冊是Servlet 3.0新特性,它不須要從新加載應用即可安裝新的Web對象(
Servlet
/Filter
/Listener
等).
爲了使動態註冊成爲可能, ServletContext
接口添加了以下方法用於 建立/添加 Web對象:
ServletContext | 描述 |
---|---|
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, 注意: 並未使用web.xml或
@WebServlet
靜態註冊DynamicServlet
實例, 而是用DynRegListener
在服務器啓動時動態註冊.
/** * @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("<h1>DynamicServlet, MyDynamicName: " + getDynamicName() + "</h1>");
}
public String getDynamicName() {
return dynamicName;
}
public void setDynamicName(String dynamicName) {
this.dynamicName = dynamicName;
}
}
@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的轉發映射:
<!-- 配置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對象的首次註冊,所以能夠省略這個步驟.
容器初始化的核心是javax.servlet.ServletContainerInitializer
接口,他只包含一個方法:
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
註解標註, 以聲明該初始化程序能夠處理這些類型的類.
利用Servlet容器初始化, SpringMVC可實現容器的零配置註冊.
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
throws ServletException {
List<WebApplicationInitializer> initializers = new LinkedList<WebApplicationInitializer>();
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;
}
org.springframework.web.SpringServletContainerInitializer
元數據文件javax.servlet.ServletContainerInitializer只有一行內容(即實現了ServletContainerInitializer
類的全限定名),該文本文件必須放在jar包的META-INF/services目錄下.