我们将从零开始,构建一个使用Maven管理的Java Web项目,其中包含以下功能:

  1. 用户注册 (POST, XHR)
  2. 用户登录 (POST, XHR)
  3. 文件上传 (POST, XHR, multipart/form-data)
  4. 文件下载 (GET)

为了让项目能在IDEA中顺利运行,我将采用现代的@WebServlet注解方式,而不是传统的web.xml配置,这样更简洁。


第一部分:项目准备和结构

在IDEA中运行Servlet项目最简单的方式是使用Tomcat服务器。

1. 项目结构

首先,在IDEA中创建一个新的Maven项目,选择 maven-archetype-webapp 原型。创建后,项目结构应该如下:

your-project-name
├── pom.xml
└── src
    └── main
        ├── java
        │   └── com
        │       └── example
        │           └── servlet
        │               ├── LoginServlet.java
        │               ├── RegisterServlet.java
        │               ├── UploadServlet.java
        │               ├── DownloadServlet.java
        │               └── storage
        │                   └── UserStore.java  (用于模拟数据库)
        └── webapp
            ├── WEB-INF
            │   └── web.xml  (我们只保留一个空的,主要用注解)
            ├── index.html
            └── script.js

uploads文件夹:这个文件夹会在项目运行时自动创建,用于存放上传的文件。

2. pom.xml 配置

这是项目的核心配置文件,用于管理依赖。请将pom.xml`内容替换为以下代码,它包含了Servlet API和文件上传所需的库。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>servlet-full-demo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>war</packaging>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <dependencies>
        <!-- Java Servlet API -->
        <dependency>
            <groupId>jakarta.servlet</groupId>
            <artifactId>jakarta.servlet-api</artifactId>
            <version>5.0.0</version>
            <scope>provided</scope>
        </dependency>

        <!-- Apache Commons FileUpload for handling multipart requests -->
        <dependency>
            <groupId>commons-fileupload</groupId>
            <artifactId>commons-fileupload</artifactId>
            <version>1.5</version>
        </dependency>

        <!-- Apache Commons IO, a dependency of FileUpload -->
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.11.0</version>
        </dependency>
    </dependencies>

    <build>
        <finalName>servlet-full-demo</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>3.3.2</version>
            </plugin>
        </plugins>
    </build>
</project>
  • 注意jakarta.servlet-apiscopeprovided,因为Tomcat服务器本身会提供这个库,我们不需要打包进去。

第二部分:前端代码 (HTML & JavaScript)

这是用户直接交互的界面,所有的请求都将由这里的JavaScript(XHR)发起。

1. webapp/index.html

这个文件包含了所有功能的UI表单。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>Servlet综合示例</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; }
        .container { border: 1px solid #ccc; padding: 20px; margin-bottom: 20px; border-radius: 8px; }
        h2 { border-bottom: 2px solid #007BFF; padding-bottom: 5px; }
        input[type="text"], input[type="password"], input[type="file"] {
            width: 300px; padding: 8px; margin-bottom: 10px; border: 1px solid #ccc; border-radius: 4px;
        }
        button { padding: 10px 15px; background-color: #007BFF; color: white; border: none; border-radius: 4px; cursor: pointer; }
        button:hover { background-color: #0056b3; }
        #status { margin-top: 20px; padding: 15px; border: 1px solid transparent; border-radius: 4px; font-weight: bold; }
        .success { background-color: #d4edda; border-color: #c3e6cb; color: #155724; }
        .error { background-color: #f8d7da; border-color: #f5c6cb; color: #721c24; }
        #file-list { margin-top: 10px; }
    </style>
</head>
<body>

<h1>Java Servlet 完整功能演示</h1>
<div id="status"></div>

<!-- 注册功能 -->
<div class="container">
    <h2>用户注册</h2>
    <form id="registerForm">
        <label for="reg-username">用户名:</label><br>
        <input type="text" id="reg-username" name="username" required><br>
        <label for="reg-password">密码:</label><br>
        <input type="password" id="reg-password" name="password" required><br>
        <button type="submit">注册</button>
    </form>
</div>

<!-- 登录功能 -->
<div class="container">
    <h2>用户登录</h2>
    <form id="loginForm">
        <label for="log-username">用户名:</label><br>
        <input type="text" id="log-username" name="username" required><br>
        <label for="log-password">密码:</label><br>
        <input type="password" id="log-password" name="password" required><br>
        <button type="submit">登录</button>
    </form>
</div>

<!-- 文件上传功能 -->
<div class="container">
    <h2>文件上传</h2>
    <form id="uploadForm">
        <label for="fileToUpload">选择文件:</label><br>
        <input type="file" id="fileToUpload" name="fileToUpload" required><br>
        <progress id="progressBar" value="0" max="100" style="width:300px;"></progress><br><br>
        <button type="submit">上传文件</button>
    </form>
</div>

<!-- 文件下载功能 -->
<div class="container">
    <h2>文件下载</h2>
    <p>上传文件后,可以在此下载。例如,下载一个名为 "example.txt" 的文件。</p>
    <a href="download?filename=example.txt" target="_blank">
        <button type="button">下载 example.txt</button>
    </a>
    <p>(请确保服务器上存在此文件,您可以先上传一个同名文件)</p>
</div>

<script src="script.js"></script>

</body>
</html>
2. webapp/script.js

这个文件处理所有的异步XHR请求,并更新UI。

document.addEventListener('DOMContentLoaded', function() {
    const statusDiv = document.getElementById('status');
    const progressBar = document.getElementById('progressBar');

    // 通用函数来更新状态显示
    const updateStatus = (message, isError = false) => {
        statusDiv.textContent = message;
        statusDiv.className = isError ? 'error' : 'success';
    };

    // 注册表单处理
    document.getElementById('registerForm').addEventListener('submit', function(event) {
        event.preventDefault(); // 阻止表单默认提交
        const formData = new FormData(this);

        const xhr = new XMLHttpRequest();
        xhr.open('POST', 'register', true);

        xhr.onload = function() {
            if (xhr.status === 200) {
                updateStatus(xhr.responseText);
            } else {
                updateStatus(`注册失败: ${xhr.responseText}`, true);
            }
        };
        xhr.onerror = function() {
            updateStatus('网络错误,无法连接到服务器', true);
        };
        xhr.send(formData);
    });

    // 登录表单处理
    document.getElementById('loginForm').addEventListener('submit', function(event) {
        event.preventDefault();
        const formData = new FormData(this);

        const xhr = new XMLHttpRequest();
        xhr.open('POST', 'login', true);

        xhr.onload = function() {
            if (xhr.status === 200) {
                updateStatus(xhr.responseText);
            } else {
                updateStatus(`登录失败: ${xhr.responseText}`, true);
            }
        };
        xhr.onerror = function() {
            updateStatus('网络错误,无法连接到服务器', true);
        };
        xhr.send(formData);
    });

    // 文件上传表单处理
    document.getElementById('uploadForm').addEventListener('submit', function(event) {
        event.preventDefault();
        const formData = new FormData(this);
        const fileInput = document.getElementById('fileToUpload');
        
        if (fileInput.files.length === 0) {
            updateStatus('请先选择一个文件', true);
            return;
        }

        const xhr = new XMLHttpRequest();
        xhr.open('POST', 'upload', true);

        // 上传进度条
        xhr.upload.onprogress = function(event) {
            if (event.lengthComputable) {
                const percentComplete = (event.loaded / event.total) * 100;
                progressBar.value = percentComplete;
            }
        };

        xhr.onload = function() {
            progressBar.value = 0; // 重置进度条
            if (xhr.status === 200) {
                updateStatus(xhr.responseText);
            } else {
                updateStatus(`上传失败: ${xhr.responseText}`, true);
            }
        };
        
        xhr.onerror = function() {
             progressBar.value = 0;
             updateStatus('网络错误,上传中断', true);
        };

        xhr.send(formData);
    });
});

演示
在这里插入图片描述

第三部分:后端Java代码 (Servlets)

这是处理业务逻辑的核心。

1. 模拟数据库 storage/UserStore.java

为了简单,我们用一个静态Map来存储用户信息,避免配置真实数据库的麻烦。

package com.example.servlet.storage;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 一个简单的内存用户存储,用于模拟数据库。
 * 使用 ConcurrentHashMap 来确保线程安全。
 */
public class UserStore {

    private static final Map<String, String> users = new ConcurrentHashMap<>();

    // 静态初始化块,可以预置一些用户
    static {
        users.put("admin", "password123");
    }

    /**
     * 验证用户凭证
     * @param username 用户名
     * @param password 密码
     * @return 如果凭证有效返回 true,否则返回 false
     */
    public static boolean isValidUser(String username, String password) {
        String storedPassword = users.get(username);
        return storedPassword != null && storedPassword.equals(password);
    }

    /**
     * 添加新用户
     * @param username 用户名
     * @param password 密码
     * @return 如果用户不存在并成功添加返回 true,如果用户已存在返回 false
     */
    public static boolean addUser(String username, String password) {
        // putIfAbsent 是一个原子操作,如果key不存在则放入,并返回null
        // 如果key已存在,则不放入,并返回已存在的值
        return users.putIfAbsent(username, password) == null;
    }
}
2. RegisterServlet.java

处理用户注册请求。

package com.example.servlet;

import com.example.servlet.storage.UserStore;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.MultipartConfig;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

@WebServlet("/register")
@MultipartConfig
public class RegisterServlet extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String username = req.getParameter("username");
        String password = req.getParameter("password");

        resp.setContentType("text/plain");
        resp.setCharacterEncoding("UTF-8");

        if (username == null || username.trim().isEmpty() || password == null || password.isEmpty()) {
            resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            resp.getWriter().write("用户名和密码不能为空!");
            return;
        }

        if (UserStore.addUser(username.trim(), password)) {
            resp.setStatus(HttpServletResponse.SC_OK);
            resp.getWriter().write("用户 '" + username + "' 注册成功!");
        } else {
            resp.setStatus(HttpServletResponse.SC_CONFLICT); // 409 Conflict
            resp.getWriter().write("用户 '" + username + "' 已存在!");
        }
    }
}
3. LoginServlet.java

处理用户登录请求。

package com.example.servlet;

import com.example.servlet.storage.UserStore;
import jakarta.servlet.annotation.MultipartConfig;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;

import java.io.IOException;

@WebServlet("/login")
@MultipartConfig
public class LoginServlet extends HttpServlet {

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
        String username = req.getParameter("username");
        String password = req.getParameter("password");

        resp.setContentType("text/plain");
        resp.setCharacterEncoding("UTF-8");
        
        if (username == null || username.trim().isEmpty() || password == null || password.isEmpty()) {
            resp.setStatus(HttpServletResponse.SC_BAD_REQUEST); // 400 Bad Request
            resp.getWriter().write("用户名和密码不能为空!");
            return;
        }

        if (UserStore.isValidUser(username, password)) {
            // 登录成功,创建一个 Session
            HttpSession session = req.getSession(true);
            session.setAttribute("user", username);
            session.setMaxInactiveInterval(30 * 60); // Session有效期30分钟

            resp.setStatus(HttpServletResponse.SC_OK);
            resp.getWriter().write("欢迎, " + username + "! 登录成功。");
        } else {
            resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401 Unauthorized
            resp.getWriter().write("用户名或密码错误!");
        }
    }
}
4. UploadServlet.java

处理文件上传。这是最复杂的一个。

package com.example.servlet;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.MultipartConfig;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.Part;

import java.io.File;
import java.io.IOException;
import java.nio.file.Paths;

@WebServlet("/upload")
@MultipartConfig(
    fileSizeThreshold = 1024 * 1024, // 1 MB. 超过这个大小的文件会直接写入磁盘,而不是内存
    maxFileSize = 1024 * 1024 * 10,  // 10 MB. 单个文件的最大大小
    maxRequestSize = 1024 * 1024 * 50 // 50 MB. 整个请求的最大大小
)
public class UploadServlet extends HttpServlet {
    
    // 定义一个固定的上传目录名
    private static final String UPLOAD_DIR = "uploads";

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 获取应用的真实路径
        String applicationPath = req.getServletContext().getRealPath("");
        // 构造上传文件的完整路径
        String uploadFilePath = applicationPath + File.separator + UPLOAD_DIR;

        // 创建上传目录(如果不存在)
        File uploadDir = new File(uploadFilePath);
        if (!uploadDir.exists()) {
            uploadDir.mkdirs();
        }

        resp.setContentType("text/plain");
        resp.setCharacterEncoding("UTF-8");
        
        try {
            // 从请求中获取名为 "fileToUpload" 的文件部分
            Part filePart = req.getPart("fileToUpload");
            
            // 获取原始文件名
            String originalFileName = getFileName(filePart);
            
            // 为了安全,我们只使用文件名,去掉路径信息
            String fileName = Paths.get(originalFileName).getFileName().toString();

            // 将文件写入服务器的指定目录
            filePart.write(uploadFilePath + File.separator + fileName);

            resp.setStatus(HttpServletResponse.SC_OK);
            resp.getWriter().write("文件 '" + fileName + "' 上传成功!");

        } catch (Exception e) {
            resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            resp.getWriter().write("文件上传失败: " + e.getMessage());
        }
    }
    
    /**
     * 从 Part 的 header 中提取文件名
     * 这是一个辅助方法,因为不同浏览器提交的 header 内容可能不同
     */
    private String getFileName(Part part) {
        String contentDisposition = part.getHeader("content-disposition");
        for (String content : contentDisposition.split(";")) {
            if (content.trim().startsWith("filename")) {
                return content.substring(content.indexOf('=') + 1).trim().replace("\"", "");
            }
        }
        return null;
    }
}
5. DownloadServlet.java

处理文件下载。

package com.example.servlet;

import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

@WebServlet("/download")
public class DownloadServlet extends HttpServlet {
    
    private static final String UPLOAD_DIR = "uploads";

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String fileName = req.getParameter("filename");

        if (fileName == null || fileName.trim().isEmpty()) {
            resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "缺少 filename 参数。");
            return;
        }
        
        // --- 安全性检查 ---
        // 防止目录遍历攻击 (e.g., filename=../../etc/passwd)
        if (fileName.contains("..")) {
            resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "无效的文件名。");
            return;
        }

        // 获取文件在服务器上的真实路径
        String applicationPath = req.getServletContext().getRealPath("");
        String filePath = applicationPath + File.separator + UPLOAD_DIR + File.separator + fileName;

        File downloadFile = new File(filePath);
        if (!downloadFile.exists() || !downloadFile.isFile()) {
            resp.sendError(HttpServletResponse.SC_NOT_FOUND, "文件未找到: " + fileName);
            return;
        }

        // 设置响应头
        ServletContext context = getServletContext();
        String mimeType = context.getMimeType(filePath);
        if (mimeType == null) {
            // 如果无法确定MIME类型,则设置为通用的二进制流类型
            mimeType = "application/octet-stream";
        }
        resp.setContentType(mimeType);
        
        // 对文件名进行URL编码,以支持中文等特殊字符
        String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.toString()).replace("+", "%20");
        resp.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + encodedFileName);
        
        resp.setContentLength((int) downloadFile.length());
        
        // 使用 try-with-resources 确保流被正确关闭
        try (FileInputStream inStream = new FileInputStream(downloadFile);
             OutputStream outStream = resp.getOutputStream()) {

            byte[] buffer = new byte[4096];
            int bytesRead;
            while ((bytesRead = inStream.read(buffer)) != -1) {
                outStream.write(buffer, 0, bytesRead);
            }
        }
    }
}

第四部分:如何在IDEA中运行

  1. 配置Tomcat服务器

    • 点击IDEA右上角的 Add Configuration...
    • 点击 + 号,在列表中找到并选择 Tomcat Server -> Local
    • Server 标签页,点击 Application server 旁边的 Configure...,选择你本地Tomcat的安装目录。
    • IDEA会自动配置好大部分设置。
  2. 部署项目 (Artifact)

    • 切换到 Deployment 标签页。
    • 点击 + 号,选择 Artifact...
    • 选择你的项目,通常是 servlet-full-demo:war explodedexploded模式支持热部署,修改前端代码或Java代码后可以快速生效。
    • Application context 中,可以设置访问路径,比如设置为 /demo。那么你的访问URL就是 http://localhost:8080/demo。如果留空,则为根路径 http://localhost:8080建议设置为 /demo
  3. 运行

    • 点击绿色的 “Run” 按钮(▶️)启动Tomcat。
    • IDEA的控制台会显示Tomcat的启动日志。
    • 启动成功后,IDEA会自动在浏览器中打开 http://localhost:8080/demo/
    • 现在你就可以在页面上测试注册、登录、上传和下载所有功能了!
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐