@WebServlet("/servlet/uploadServlet")
public class UploadServlet extends HttpServlet {
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
DiskFileItemFactory factory = new DiskFileItemFactory(1024 * 8, new File("d:/temp")); // 8KB 內存閾值
ServletFileUpload upload = new ServletFileUpload(factory);
upload.setFileSizeMax(8 * 1024 * 1024); // 8MB
upload.setHeaderEncoding("UTF-8");
PhotoModel photo = new PhotoModel();
InputStream fileStream = null;
try {
List<FileItem> items = upload.parseRequest(req);
for (FileItem item : items) {
if (item.isFormField()) {
photo.setDesc(item.getString("UTF-8")); // 普通表單字段
} else {
fileStream = item.getInputStream(); // 文件流
String fileName = item.getName();
if (fileName == null || fileName.trim().isEmpty()) {
resp.sendRedirect(req.getContextPath() + "/jsps/upload.jsp");
return;
}
// 提取真實文件名(去除路徑)、擴展名
photo.setRealName(fileName.substring(fileName.lastIndexOf("\\") + 1));
photo.setExt(fileName.substring(fileName.lastIndexOf(".")));
}
}
// 補全元數據
photo.setId(MyUtils.getUUID());
photo.setDt(MyUtils.getCurrentDatetime());
photo.setIp(req.getRemoteAddr());
photo.setDir(MyUtils.getDir(photo.getId())); // Hash 分佈目錄
// 1) 存 XML 元數據
if (new PhotoDaoImpl().save(photo)) {
// 2) 存物理文件
String relativePath = "photos/" + photo.getDir();
String absolutePath = getServletContext().getRealPath(relativePath);
new File(absolutePath).mkdirs();
FileUtils.copyInputStreamToFile(fileStream, new File(absolutePath + "/" + photo.getId() + photo.getExt()));
resp.sendRedirect(req.getContextPath() + "/index.jsp"); // 重定向防重複提交
} else {
req.getRequestDispatcher("/jsps/upload.jsp").forward(req, resp);
}
} catch (FileUploadException e) {
throw new RuntimeException("上傳解析失敗", e);
} finally {
// 清理臨時文件
File tempDir = new File("d:/temp");
for (File f : tempDir.listFiles()) f.delete();
}
}
}
@WebServlet("/servlet/showAllImg")
public class ShowAllImgServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=UTF-8");
PrintWriter out = resp.getWriter();
List<PhotoModel> list = new PhotoDaoImpl().getAllPhotos();
out.println("<table border='1' width='100%' style='text-align:center'>");
out.println("<tr><th>文件名</th><th>上傳時間</th><th>縮略圖</th><th>描述</th><th>操作</th></tr>");
for (PhotoModel p : list) {
String url = req.getContextPath() + "/photos/" + p.getDir() + "/" + p.getId() + p.getExt();
out.println("<tr>");
out.println("<td>" + p.getRealName() + "</td>");
out.println("<td>" + p.getDt() + "</td>");
out.println("<td><a href='" + url + "' target='_blank'><img src='" + url + "' width='100' height='100'></a></td>");
out.println("<td>" + p.getDesc() + "</td>");
out.println("<td>");
out.println("<a href='" + req.getContextPath() + "/servlet/delPhoto?id=" + p.getId() + "'>刪除</a><br/>");
out.println("<a href='" + req.getContextPath() + "/servlet/down?id=" + p.getId() + "'>下載</a>");
out.println("</td></tr>");
}
out.println("</table>");
}
}
@WebServlet("/servlet/delPhoto")
public class DelPhotoServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String id = req.getParameter("id");
PhotoModel photo = new PhotoDaoImpl().getById(id);
// 權限校驗:只能刪除自己上傳的
if (photo != null && !photo.getIp().equals(req.getRemoteAddr())) {
resp.getWriter().println("無權限刪除他人圖片");
return;
}
if (photo != null && new PhotoDaoImpl().delete(id)) {
// 刪除物理文件
String path = getServletContext().getRealPath("photos/" + photo.getDir() + "/" + photo.getId() + photo.getExt());
new File(path).delete();
resp.sendRedirect(req.getContextPath() + "/servlet/showAllImg");
} else {
resp.getWriter().println("刪除失敗或文件不存在");
}
}
}
@WebServlet("/servlet/down")
public class DownServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
PhotoModel photo = new PhotoDaoImpl().getById(req.getParameter("id"));
if (photo == null) { resp.getWriter().println("文件不存在"); return; }
String filePath = getServletContext().getRealPath("photos/" + photo.getDir() + "/" + photo.getId() + photo.getExt());
File file = new File(filePath);
if (!file.exists()) { resp.getWriter().println("物理文件丟失"); return; }
// 關鍵:中文文件名 RFC 5987 編碼
String fileName = URLEncoder.encode(photo.getRealName(), "UTF-8").replace("+", "%20");
resp.setContentType("application/force-download");
resp.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + fileName);
resp.setContentLengthLong(file.length());
try (FileInputStream in = new FileInputStream(file);
OutputStream out = resp.getOutputStream()) {
byte[] buf = new byte[1024];
int len;
while ((len = in.read(buf)) != -1) out.write(buf, 0, len);
}
}
}
// MyUtils.java
public class MyUtils {
public static String getUUID() { return UUID.randomUUID().toString().replace("-", ""); }
public static String getCurrentDatetime() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); }
// Hash 目錄:UUID hashCode 低 8 位拆為兩級 16 進制目錄
public static String getDir(String uuid) {
int h = uuid.hashCode();
return Integer.toHexString(h & 0xF) + "/" + Integer.toHexString((h & 0xF0) >> 4);
}
}
// PhotoDaoImpl.java (DOM4J 操作 XML)
public class PhotoDaoImpl {
private Document getDoc() { /* 緩存 Document 對象 */ }
public boolean save(PhotoModel p) { /* 添加 元素屬性及 子元素 */ }
public List<PhotoModel> getAllPhotos() { /* 遍歷根元素構建 List */ }
public PhotoModel getById(String id) { /* XPath 查詢 //photo[@id='...'] */ }
public boolean delete(String id) { /* 定位元素 remove */ }
}
<servlet>
<servlet-name>UploadServlet</servlet-name>
<servlet-class>cn.hncu.servlet.UploadServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>UploadServlet</servlet-name>
<url-pattern>/servlet/uploadServlet</url-pattern>
</servlet-mapping>
... (其餘 Servlet 同理)
| 問題 | 解決方案 |
|---|---|
| 中文文件名亂碼 | 上傳時 upload.setHeaderEncoding("UTF-8");下載時 URLEncoder.encode + filename*=UTF-8'' |
| 大文件上傳失敗 | 調整 upload.setFileSizeMax、setSizeMax;配置 Tomcat maxPostSize |
| 單目錄文件過多導致 IO 變慢 | Hash 目錄分佈 (本項目 256 子目錄),生產可用 MD5 前 4 位做 3 級目錄 |
| XML 併發寫入衝突 | DOM4J 非線程安全,生產必須換 MySQL/PostgreSQL + 連接池 |
| 臨時文件洩漏 | FileUpload 會在臨時目錄產生文件,finally 中必須清理 |
本項目展示了 JSP/Servlet 時代的完整相冊系統:上傳、列表、刪除、下載、權限控制、Hash 存儲、XML 持久化。雖然技術棧較老,但分層架構、工具類封裝、邊界處理、安全校驗等工程思想完全通用,值得作為「從零手寫 MVC 系統」的教學級參考。