首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java 相冊系統完整實戰:JSP+Servlet+XML 數據存儲全棧實現

     編輯:Java 相關知識

系統架構概覽

  • 展示層:JSP (index.jsp, upload.jsp) + 原生 JS
  • 控制層:Servlet (Upload, ShowAll, Delete, Download)
  • 數據層:DOM4J 解析 XML (photos.xml) 模擬數據庫
  • 文件存儲:Hash 目錄分佈 (兩級 16x16=256 子目錄) 避免單目錄文件過多
  • 上傳組件:Apache Commons FileUpload + Commons IO

核心功能模塊

1. 文件上傳

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

2. 圖片列表展示

@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>");
    }
}

3. 刪除功能 (權限校驗)

@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("刪除失敗或文件不存在");
        }
    }
}

4. 文件下載 (中文文件名處理)

@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 */ }
}

web.xml 註冊 (Servlet 3.0+ 可用 @WebServlet 替代)

<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 系統」的教學級參考。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved