傳統表單提交上傳檔案時,頁面會整頁跳轉,使用者無法得知進度。隨著 Ajax 技術普及,異步上傳成為常態,但 Servlet 只有在檔案完全接收完畢才會回應,導致前端無法獲取即時進度。本文提供一套基於「單例模式 + 多執行緒 + 輪詢」的完整解決方案。
將功能拆解為三大模組:
public class ProgressSingleton {
private static Hashtable<Object, Object> table = new Hashtable<>();
public static void put(Object key, Object value) { table.put(key, value); }
public static Object get(Object key) { return table.get(key); }
public static Object remove(Object key) { return table.remove(key); }
}
@WebServlet("/UploadServlet")
public class UploadServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
DiskFileItemFactory factory = new DiskFileItemFactory();
factory.setSizeThreshold(4 * 1024);
ServletFileUpload upload = new ServletFileUpload(factory);
List<FileItem> items = upload.parseRequest(new ServletRequestContext(request));
FileItem fileItem = items.get(0);
// 以 sessionId + 檔名為唯一鍵
String key = request.getSession().getId() + fileItem.getName();
// 初始化進度
ProgressSingleton.put(key + "Size", fileItem.getSize());
long progress = 0;
// 以流方式讀取,每讀一塊更新進度
try (InputStream in = fileItem.getInputStream();
FileOutputStream out = new FileOutputStream("D:/test/" + fileItem.getName())) {
byte[] buffer = new byte[1024];
int len;
while ((len = in.read(buffer)) != -1) {
progress += len;
ProgressSingleton.put(key + "Progress", progress);
out.write(buffer, 0, len);
}
} finally {
// 完成後清理
ProgressSingleton.remove(key + "Size");
ProgressSingleton.remove(key + "Progress");
}
response.getWriter().print("done");
}
}
@WebServlet("/ProgressServlet")
public class ProgressServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String key = request.getSession().getId() + request.getParameter("filename");
Object size = ProgressSingleton.get(key + "Size");
Object progress = ProgressSingleton.get(key + "Progress");
JSONObject json = new JSONObject();
json.put("size", size == null ? 100 : size);
json.put("progress", progress == null ? 0 : progress);
response.getWriter().print(json.toString());
}
}
<form id="dataForm" enctype="multipart/form-data">
<input type="file" name="file" id="fileInput">
<input type="submit" value="上傳" id="submit">
</form>
<div id="progress">0%</div>
<script>
$("#submit").click(function(e) {
e.preventDefault();
var form = document.getElementById("dataForm");
var progressDiv = document.getElementById("progress");
// 每 500ms 輪詢一次進度
var timer = setInterval(function() {
$.post('ProgressServlet', { filename: fileInput.files[0].name }, function(res) {
var data = JSON.parse(res);
progressDiv.innerText = parseInt((data.progress / data.size) * 100) + '%';
});
}, 500);
// 異步上傳
$.ajax({
url: 'UploadServlet',
type: 'POST',
data: new FormData(form),
processData: false,
contentType: false,
success: function() {
clearInterval(timer);
progressDiv.innerText = '100%';
}
});
});
</script>

這套方案雖然經典,但在微服務與前後端分離架構下,建議升級為前端原生進度事件或 WebSocket 即時推送,體驗更流暢、架構更乾淨。