圖書館後台管理系統需實現書籍封面圖片上傳功能:預設顯示「暫無圖片」佔位圖,點擊「上傳圖片」按鈕彈出模態視窗,選擇圖片上傳後自動更新預覽圖並回填隱藏欄位供表單提交。
<%@ page language="java" pageEncoding="UTF-8"%>
<% String root = request.getContextPath(); %>
<!DOCTYPE html>
<html>
<head>
<title>新增書籍</title>
<meta charset="UTF-8">
<script>
// 開啟上傳模態視窗
function openUploadDialog() {
// showModalDialog 僅 IE 支援,現代瀏覽器建議改用 window.open + postMessage 或 Bootstrap Modal
var returnVal = window.showModalDialog(
"<%=root%>/admin/bookUpload.jsp",
"",
"dialogWidth:400px;dialogHeight:300px;scroll:no;status:no"
);
if (returnVal != null) {
// 回填隱藏欄位 (供表單提交存入資料庫)
document.getElementById("photoPath").value = returnVal;
// 更新預覽圖
document.getElementById("previewImg").src = "<%=root%>/" + returnVal;
}
}
</script>
</head>
<body>
<h4>新增書籍資料</h4>
<hr/>
<form action="<%=root%>/BookServlet?type=add" method="post">
<div>
<label>書名:<input type="text" name="bookName" required/></label>
</div>
<div>
<label>作者:<input type="text" name="author" /></label>
</div>
<div>
<label>封面圖片:</label>
<input type="hidden" id="photoPath" name="coverPath" value="images/noimg.png"/>
<input type="button" value="上傳圖片" onclick="openUploadDialog()"/>
<br/>
<img id="previewImg" src="<%=root%>/images/noimg.png" width="200" height="200" style="border:1px solid #ccc;"/>
</div>
<div>
<input type="submit" value="儲存書籍"/>
</div>
</form>
</body>
</html>
<%@ page language="java" pageEncoding="UTF-8"%>
<% String root = request.getContextPath(); %>
<!DOCTYPE html>
<html>
<head>
<title>圖片上傳</title>
<meta charset="UTF-8">
<base target="_self"> <!-- 關鍵:確保表單提交在模態視窗內進行 -->
</head>
<body>
<h5>上傳書籍封面</h5>
<hr/>
<% if (request.getAttribute("errorMsg") != null) { %>
<p style="color:red;">${requestScope.errorMsg}</p>
<% } %>
<form action="<%=root%>/BookServlet?type=bookUpload" method="post" enctype="multipart/form-data">
<div>注意:圖片大小不得超過 3MB!</div>
<div><input type="file" name="fileUpload" accept="image/*" required/></div>
<div><input type="submit" value="上傳"/></div>
</form>
</body>
</html>
package com.library.servlet;
import org.apache.commons.fileupload.*;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;
import java.util.*;
public class BookServlet extends HttpServlet {
// 配置常數
private static final String UPLOAD_DIR = "eshop/upload/"; // 正式儲存目錄
private static final String TEMP_DIR = "eshop/upload_temp/"; // 暫存目錄
private static final int MAX_SIZE = 3; // MB
private static final String[] ALLOWED_EXTS = {".jpg", ".jpeg", ".png", ".gif", ".bmp", ".ico"};
private String serverRealPath;
@Override
public void init() throws ServletException {
serverRealPath = getServletContext().getRealPath("/").replace("\\", "/");
// 初始化建立目錄
new File(serverRealPath + UPLOAD_DIR).mkdirs();
new File(serverRealPath + TEMP_DIR).mkdirs();
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String type = request.getParameter("type");
if (!"bookUpload".equals(type)) {
// 其他業務邏輯...
return;
}
request.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
// 1. 建立工廠與解析器
DiskFileItemFactory factory = new DiskFileItemFactory();
factory.setSizeThreshold(5 * 1024); // 5KB 記憶體緩衝
factory.setRepository(new File(serverRealPath + TEMP_DIR));
ServletFileUpload upload = new ServletFileUpload(factory);
upload.setSizeMax(MAX_SIZE * 1024 * 1024); // 3MB
upload.setHeaderEncoding("UTF-8");
try {
List<FileItem> items = upload.parseRequest(request);
for (FileItem item : items) {
if (!item.isFormField()) { // 處理文件欄位
String fileName = item.getName();
if (fileName == null || fileName.isEmpty()) {
forwardError(request, response, "未選擇檔案");
return;
}
// 檔案類型驗證 (副檔名)
String lowerName = fileName.toLowerCase();
boolean validExt = false;
for (String ext : ALLOWED_EXTS) {
if (lowerName.endsWith(ext)) { validExt = true; break; }
}
if (!validExt) {
forwardError(request, response, "僅允許上傳圖片檔案 (JPG/PNG/GIF/BMP/ICO)");
return;
}
// 生成唯一檔名:UUID + 原始副檔名
String uuid = UUID.randomUUID().toString();
String ext = lowerName.substring(lowerName.lastIndexOf("."));
String savedPath = UPLOAD_DIR + uuid + ext;
String fullPath = serverRealPath + savedPath;
// 寫入檔案 (自動清理暫存)
item.write(new File(fullPath));
// 回傳成功腳本:alert + 設定 returnValue + 關閉視窗
out.write("<script>");
out.write("alert('圖片上傳成功!');");
out.write("window.returnValue = '" + savedPath + "';");
out.write("window.close();");
out.write("</script>");
out.flush();
return; // 結束處理
}
}
} catch (FileUploadBase.SizeLimitExceededException e) {
forwardError(request, response, "檔案大小超過 " + MAX_SIZE + "MB 限制");
} catch (Exception e) {
e.printStackTrace();
forwardError(request, response, "上傳失敗:" + e.getMessage());
}
}
private void forwardError(HttpServletRequest request, HttpServletResponse response, String msg)
throws ServletException, IOException {
request.setAttribute("errorMsg", msg);
request.getRequestDispatcher("/admin/bookUpload.jsp").forward(request, response);
}
}
window.showModalDialog(url, args, features):IE 專用,阻塞父視窗直到關閉window.returnValue = "資料" 並 window.close()var ret = showModalDialog(...)window.open() + postMessage() 跨視窗通訊getServletContext().getRealPath("/") 取得 Web 應用絕對路徑WEB-INF 外部目錄,確保可直接透過 URL 存取 (<img src="/contextPath/eshop/upload/xxx.jpg">)// 1. 檢查 Magic Number (防止偽裝副檔名)
public boolean isValidImage(FileItem item) throws IOException {
byte[] header = new byte[12];
try (InputStream in = item.getInputStream()) {
in.read(header);
}
// JPEG: FF D8 FF, PNG: 89 50 4E 47 0D 0A 1A 0A, GIF: 47 49 46 38
return (header[0] == (byte)0xFF && header[1] == (byte)0xD8) ||
(header[0] == (byte)0x89 && header[1] == (byte)0x50) ||
(header[0] == (byte)0x47 && header[1] == (byte)0x49);
}
// 2. 限制上傳目錄執行權限 (防止腳本執行)
// Nginx 配置: location /uploads { add_header X-Content-Type-Options nosniff; }
// 3. 檔案大小雙重驗證 (前端 JS + 後端 Servlet)
// 4. 防毒掃描整合 (ClamAV 等)
@RestController + MultipartFile 返回 JSON<servlet>
<servlet-name>BookServlet</servlet-name>
<servlet-class>com.library.servlet.BookServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>BookServlet</servlet-name>
<url-pattern>/BookServlet</url-pattern>
</servlet-mapping>
本方案展示了傳統 JSP/Servlet + Commons FileUpload 實現模態視窗圖片上傳的完整流程。雖然 showModalDialog 已被現代瀏覽器棄用,但核心上傳邏輯 (驗證、命名、儲存、回傳) 仍具參考價值。新專案強烈建議採用前後端分離架構,配合現代上傳組件與雲端儲存服務。