java-Servlet实现上传和下载文件,使用XHR请求数据,登录和注册功能
·
我们将从零开始,构建一个使用Maven管理的Java Web项目,其中包含以下功能:
- 用户注册 (POST, XHR)
- 用户登录 (POST, XHR)
- 文件上传 (POST, XHR,
multipart/form-data) - 文件下载 (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-api的scope是provided,因为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中运行
-
配置Tomcat服务器:
- 点击IDEA右上角的
Add Configuration...。 - 点击
+号,在列表中找到并选择Tomcat Server->Local。 - 在
Server标签页,点击Application server旁边的Configure...,选择你本地Tomcat的安装目录。 - IDEA会自动配置好大部分设置。
- 点击IDEA右上角的
-
部署项目 (Artifact):
- 切换到
Deployment标签页。 - 点击
+号,选择Artifact...。 - 选择你的项目,通常是
servlet-full-demo:war exploded。exploded模式支持热部署,修改前端代码或Java代码后可以快速生效。 - 在
Application context中,可以设置访问路径,比如设置为/demo。那么你的访问URL就是http://localhost:8080/demo。如果留空,则为根路径http://localhost:8080。建议设置为/demo。
- 切换到
-
运行:
- 点击绿色的 “Run” 按钮(▶️)启动Tomcat。
- IDEA的控制台会显示Tomcat的启动日志。
- 启动成功后,IDEA会自动在浏览器中打开
http://localhost:8080/demo/。 - 现在你就可以在页面上测试注册、登录、上传和下载所有功能了!
更多推荐
所有评论(0)