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

Java Servlet 實現文件上傳下載:打造輕量級局域網文件服務器

     編輯:Java 相關知識

專案目標與技術棧

  • 功能:文件上傳、列表展示、下載、刪除
  • 技術:Servlet 3.0+、Apache Commons FileUpload、Commons IO
  • 場景:局域網內輕量級文件共享,無需 Nginx/MinIO 等重組件

依賴準備

<!-- Maven 坐標 -->
<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.4</version>
</dependency>
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.11.0</version>
</dependency>

核心 Servlet 實現

@WebServlet("/*") // 匹配所有路徑
@MultipartConfig // Servlet 3.0 原生支援 (可選,本例用 FileUpload)
public class FileServerServlet extends HttpServlet {
    
    // 路徑常量
    private static final String UPLOAD_DIR = "upload";
    private static final String TEMP_DIR = "temp";
    private static final long MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
    private static final long MAX_REQUEST_SIZE = 100 * 1024 * 1024; // 100MB
    
    // Action 路由
    private static final String ACTION_LIST = "/list";
    private static final String ACTION_UPLOAD = "/upload";
    private static final String ACTION_DOWNLOAD = "/download";
    private static final String ACTION_DELETE = "/delete";
    
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) 
            throws ServletException, IOException {
        
        String action = req.getRequestURI().substring(req.getContextPath().length());
        
        try {
            switch (action) {
                case ACTION_LIST: listFiles(req, resp); break;
                case ACTION_UPLOAD: uploadFile(req, resp); break;
                case ACTION_DOWNLOAD: downloadFile(req, resp); break;
                case ACTION_DELETE: deleteFile(req, resp); break;
                default: show404(resp);
            }
        } catch (Exception e) {
            log("處理請求異常: " + action, e);
            resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        }
    }
    
    // ========== 文件上傳 ==========
    private void uploadFile(HttpServletRequest req, HttpServletResponse resp) 
            throws ServletException, IOException {
        
        // 1. 檢查是否 multipart
        if (!ServletFileUpload.isMultipartContent(req)) {
            resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "非上傳請求");
            return;
        }
        
        // 2. 配置工廠與解析器
        DiskFileItemFactory factory = new DiskFileItemFactory();
        factory.setSizeThreshold(1024 * 100); // 100KB 閾值,超過寫臨時文件
        factory.setRepository(getTempDir(req));
        
        ServletFileUpload upload = new ServletFileUpload(factory);
        upload.setHeaderEncoding(StandardCharsets.UTF_8.name());
        upload.setFileSizeMax(MAX_FILE_SIZE);
        upload.setSizeMax(MAX_REQUEST_SIZE);
        upload.setProgressListener((bytesRead, contentLength, items) -> 
            log("上傳進度: " + bytesRead + "/" + contentLength));
        
        // 3. 解析請求
        List<FileItem> items;
        try {
            items = upload.parseRequest(req);
        } catch (FileUploadException e) {
            throw new ServletException("解析失敗", e);
        }
        
        // 4. 處理每個表單項
        String uploadDir = getUploadDir(req);
        for (FileItem item : items) {
            if (item.isFormField()) {
                // 普通字段處理
                String value = item.getString(StandardCharsets.UTF_8);
                log("表單字段: " + item.getFieldName() + "=" + value);
            } else {
                // 文件處理
                String fileName = sanitizeFileName(item.getName());
                if (fileName == null) continue;
                
                // 生成唯一文件名防覆蓋
                String savedName = UUID.randomUUID() + "_" + fileName;
                // Hash 分散存儲避免單目錄文件過多
                String savePath = getHashPath(uploadDir, savedName);
                
                try (InputStream in = item.getInputStream();
                     OutputStream out = new FileOutputStream(savePath)) {
                    
                    byte[] buffer = new byte[8192];
                    int len;
                    while ((len = in.read(buffer)) != -1) {
                        out.write(buffer, 0, len);
                    }
                }
                log("文件上傳成功: " + savePath);
            }
        }
        
        // 重定向到列表頁
        resp.sendRedirect(req.getContextPath() + ACTION_LIST);
    }
    
    // ========== 文件列表 ==========
    private void listFiles(HttpServletRequest req, HttpServletResponse resp) 
            throws IOException {
        
        String uploadDir = getUploadDir(req);
        File root = new File(uploadDir);
        List<FileInfo> files = new ArrayList<>();
        
        // 遞歸掃描 (實際項目建議用數據庫記錄元數據)
        scanFiles(root, files);
        
        // 生成 HTML 響應
        resp.setContentType("text/html;charset=UTF-8");
        try (PrintWriter out = resp.getWriter()) {
            out.println("文件列表");
            out.println("");
            out.println("");
            out.println("

文件管理

"); out.println("

上傳新文件

"); out.println(""); int idx = 1; for (FileInfo fi : files) { String downloadUrl = req.getContextPath() + ACTION_DOWNLOAD + "?id=" + fi.id; String deleteUrl = req.getContextPath() + ACTION_DELETE + "?id=" + fi.id; out.println(""); out.println(""); out.println(""); out.println(""); out.println(""); out.println(""); } out.println("
#文件名大小操作
" + idx++ + "" + fi.originalName + "" + formatSize(fi.size) + "下載 | " + "刪除
"); } } // ========== 文件下載 ========== private void downloadFile(HttpServletRequest req, HttpServletResponse resp) throws IOException { String id = req.getParameter("id"); if (id == null) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST); return; } File file = findFileById(getUploadDir(req), id); if (file == null || !file.exists()) { resp.sendError(HttpServletResponse.SC_NOT_FOUND, "文件不存在"); return; } // 解析原始文件名 (UUID_原始名) String originalName = file.getName().substring(file.getName().indexOf('_') + 1); // 設置響應頭 resp.reset(); resp.setContentType("application/octet-stream"); resp.setContentLengthLong(file.length()); String encodedName = URLEncoder.encode(originalName, StandardCharsets.UTF_8); resp.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + encodedName); // 傳輸文件 try (InputStream in = new FileInputStream(file); OutputStream out = resp.getOutputStream()) { byte[] buffer = new byte[8192]; int len; while ((len = in.read(buffer)) != -1) { out.write(buffer, 0, len); } } } // ========== 文件刪除 ========== private void deleteFile(HttpServletRequest req, HttpServletResponse resp) throws IOException { String id = req.getParameter("id"); if (id == null) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST); return; } File file = findFileById(getUploadDir(req), id); boolean deleted = (file != null && file.delete()); if (deleted) { resp.sendRedirect(req.getContextPath() + ACTION_LIST); } else { resp.sendError(HttpServletResponse.SC_NOT_FOUND, "文件不存在或刪除失敗"); } } // ========== 輔助方法 ========== private File getUploadDir(HttpServletRequest req) { return new File(req.getServletContext().getRealPath(UPLOAD_DIR)); } private File getTempDir(HttpServletRequest req) { File temp = new File(req.getServletContext().getRealPath(TEMP_DIR)); if (!temp.exists()) temp.mkdirs(); return temp; } // Hash 分散存儲:UUID hashCode 取低 8 位 -> 兩級目錄 (0-15) private String getHashPath(String baseDir, String fileName) { int hash = fileName.hashCode(); int d1 = hash & 0xF; int d2 = (hash >> 4) & 0xF; File dir = new File(baseDir, d1 + "/" + d2); if (!dir.exists()) dir.mkdirs(); return new File(dir, fileName).getAbsolutePath(); } private File findFileById(String baseDir, String id) { // 實際項目應用數據庫查詢,此處為演示遞歸查找 return findFileRecursive(new File(baseDir), id); } private File findFileRecursive(File dir, String id) { File[] list = dir.listFiles(); if (list == null) return null; for (File f : list) { if (f.isDirectory()) { File found = findFileRecursive(f, id); if (found != null) return found; } else if (f.getName().endsWith("_" + id)) { return f; } } return null; } private void scanFiles(File dir, List<FileInfo> result) { File[] list = dir.listFiles(); if (list == null) return; for (File f : list) { if (f.isDirectory()) scanFiles(f, result); else if (f.getName().contains("_")) { String id = f.getName().substring(f.getName().lastIndexOf('_') + 1); result.add(new FileInfo(id, f.getName().substring(0, f.getName().lastIndexOf('_')), f.length())); } } } private String sanitizeFileName(String name) { if (name == null) return null; name = name.substring(name.lastIndexOf(File.separator) + 1); // 去除路徑 return name.replaceAll("[^\\w\\-. ]", "_"); // 過濾特殊字符 } private String formatSize(long bytes) { if (bytes < 1024) return bytes + " B"; int z = (int) (Math.log(bytes) / Math.log(1024)); return String.format("%.1f %sB", bytes / Math.pow(1024, z), "KMGTPE".charAt(z-1)); } private void show404(HttpServletResponse resp) throws IOException { resp.sendError(HttpServletResponse.SC_NOT_FOUND); } // 內部類 static class FileInfo { String id, originalName; long size; FileInfo(String id, String originalName, long size) { this.id = id; this.originalName = originalName; this.size = size; } } }

前端上傳頁面 (upload.html)

<!DOCTYPE html>
<html><head><meta charset="UTF-8"><title>文件上傳</title></head><body>
<h2>上傳文件</h2>
<form action="upload" method="post" enctype="multipart/form-data">
    <input type="file" name="file" multiple><br><br>
    <input type="submit" value="上傳">
</form>
<p><a href="list">返回列表</a></p>
</body></html>

部署與測試

  1. 打包為 WAR 部署到 Tomcat/Jetty
  2. 訪問 https://host:port/app/list 查看文件列表
  3. 點擊「上傳新文件」跳轉上傳頁
  4. 局域網內其他設備訪問同一地址即可共享

生產環境強化建議

  • 元數據持久化:MySQL/Redis 存儲文件 ID、原始名、路徑、上傳者、時間,替代遞歸掃描
  • 權限控制:集成 Spring Security/Shiro,基於角色控制上傳/下載/刪除
  • 大文件支援:分片上傳、斷點續傳 (WebUploader/Plupload + 後端合併)
  • 存儲分離:對象存儲 (MinIO/S3/OSS) 替代本地磁碟
  • 安全防護:文件類型白名單、病毒掃描、路徑遍歷防護、限流

總結

基於 Servlet + Commons FileUpload 實現的文件服務器,代碼量少、依賴輕、部署簡單,極適合內網臨時文件共享、中小項目附件管理、原型驗證場景。掌握其核心流程(解析、存儲、分散、下載、刪除),為後續遷移到專業對象存儲奠定堅實基礎。

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