Jakarta EE 课程 — 微型资料投递与分发(Mini Drop-off Box)
摘要:实验基于Jakarta EE 9+(兼容Tomcat 10+)、Maven作为构建工具,并在IntelliJ IDEA 2023.2(Community版免费)中进行。项目使用Maven Archetype WebApp模板生成基础结构,然后升级到Jakarta EE。
这个实验实现了一个简单的文件上传/下载系统,名为HttpExperiment,满足所有功能要求:
用户上传文件+描述,生成唯一ID,保存到内存。支持重定向(302)、JSON响应(201 + Location头)、下载(Content-Type等头)、缓存(ETag + 304)、错误处理(413/415)、XSS转义。路径:/drops(上传表单/列表),/drops/{id}(详情/下载)。
本文将提供:
实验步骤:从项目创建到测试的详细指导。代码:完整、可复制的pom.xml、Java类(Servlet)、JSP文件、web.xml等。代码注释:每个代码块中添加详细注释,解释实现逻辑、HTTP相关知识和关键点。
实验准备:
前提:IntelliJ IDEA 2023.2已安装,JDK 17+配置好(File > Project Structure > SDKs > Add JDK)。Tomcat 10+下载(https://tomcat.apache.org/),解压到本地目录(e.g., C:Tomcat10)。时间:约1-2小时(包括调试)。测试工具:浏览器(Chrome)、curl命令(Windows CMD安装curl或用Git Bash)。
1. 实验步骤
1.1: 在IntelliJ IDEA中使用Maven Archetype创建项目
打开IntelliJ IDEA,点击“File > New > Project”。选择“Maven Archetype”。配置:
Name:HttpExperiment。Location:选择保存目录。Archetype:点击“Add Archetype…”,填写:
GroupId:org.apache.maven.archetypesArtifactId:maven-archetype-webappVersion:1.4(稳定版)。 点击“OK”,然后“Next”。 项目属性:
GroupId:com.exampleArtifactId:httpexperimentVersion:1.0-SNAPSHOT 点击“Create”。IntelliJ生成项目,包括pom.xml和src/main/webapp/index.jsp。
1.2: 更新pom.xml到Jakarta EE并添加依赖
双击pom.xml,替换为以下内容(升级到Jakarta EE 9,添加必需依赖):
<?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>httpexperiment</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<!-- Jakarta EE Web API (Servlet, JSP等) -->
<dependency>
<groupId>jakarta.platform</groupId>
<artifactId>jakarta.jakartaee-web-api</artifactId>
<version>9.1.0</version>
<scope>provided</scope>
</dependency>
<!-- JSTL for JSP (标签支持) -->
<dependency>
<groupId>jakarta.servlet.jsp.jstl</groupId>
<artifactId>jakarta.servlet.jsp.jstl-api</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>org.glassfish.web</groupId>
<artifactId>jakarta.servlet.jsp.jstl</artifactId>
<version>2.0.0</version>
</dependency>
<!-- Jackson for JSON (可选, 用于JSON响应) -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.3</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.3.2</version>
</plugin>
</plugins>
</build>
</project>
右键pom.xml > Maven > Reload project(下载依赖)。
1.3: 更新web.xml(兼容Jakarta EE)
在
中替换为:
src/main/webapp/WEB-INF/web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
version="5.0">
<!-- 无需额外配置, Servlet用注解 -->
</web-app>
1.4: 创建Servlet类(DropsServlet.java)
在
下New > Java Class创建DropsServlet.java。
src/main/java/com/example
package com.example;
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.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import com.fasterxml.jackson.databind.ObjectMapper;
// 注解注册路径: /drops (上传/列表), /drops/* (详情/下载)
@WebServlet(name = "DropsServlet", value = {"/drops", "/drops/*"})
@MultipartConfig // 启用multipart/form-data支持 (文件上传)
public class DropsServlet extends HttpServlet {
// 内存存储: ID -> 文件数据 (Map<String, FileData>)
private Map<String, FileData> fileStore = new HashMap<>();
// 文件数据类 (内部类, 存储描述、内容、类型、ETag)
private static class FileData {
String description;
byte[] content;
String contentType;
String etag; // ETag for缓存
FileData(String desc, byte[] cont, String type) {
description = desc;
content = cont;
contentType = type;
etag = Base64.getEncoder().encodeToString(content).substring(0, 10); // 简单ETag (基于内容hash的前10位Base64)
}
}
// GET: /drops 显示上传表单和列表; /drops/{id} 显示详情/下载
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String path = req.getPathInfo(); // 获取/drops后的路径 (e.g., /id123)
if (path == null || path.equals("/")) {
// /drops: 显示表单和列表
resp.setContentType("text/html");
PrintWriter out = resp.getWriter();
out.println("<html><body>");
out.println("<h1>File Drops</h1>");
out.println("<form method='post' enctype='multipart/form-data'>");
out.println("Description: <input type='text' name='description'><br>");
out.println("File: <input type='file' name='file'><br>");
out.println("<input type='submit' value='Upload'>");
out.println("</form>");
out.println("<h2>Uploaded Files</h2><ul>");
for (String id : fileStore.keySet()) {
out.println("<li><a href='/drops/" + id + "'>" + id + "</a></li>");
}
out.println("</ul></body></html>");
} else {
// /drops/{id}: 详情或下载
String id = path.substring(1); // 移除/
FileData file = fileStore.get(id);
if (file == null) {
resp.setStatus(404); // 404 Not Found
return;
}
// 检查条件请求 (If-None-Match for ETag)
String ifNoneMatch = req.getHeader("If-None-Match");
if (ifNoneMatch != null && ifNoneMatch.equals(""" + file.etag + """)) {
resp.setStatus(304); // 304 Not Modified (缓存命中)
return;
}
// 返回文件 (下载)
resp.setContentType(file.contentType);
resp.setHeader("Content-Disposition", "attachment; filename="" + id + """); // 触发下载
resp.setHeader("Content-Length", String.valueOf(file.content.length)); // 长度
resp.setHeader("ETag", """ + file.etag + """); // ETag头
resp.getOutputStream().write(file.content); // 二进制输出
}
}
// POST: /drops 处理上传
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 获取multipart部分
Part filePart = req.getPart("file");
String description = req.getParameter("description");
// 错误处理
if (filePart == null || filePart.getSize() == 0) {
resp.setStatus(400); // 400 Bad Request (缺少文件)
return;
}
if (filePart.getSize() > 5 * 1024 * 1024) { // >5MB
resp.setStatus(413); // 413 Payload Too Large
return;
}
String contentType = filePart.getContentType();
if (contentType == null || (!contentType.startsWith("text/") && !contentType.startsWith("image/"))) { // 示例: 只支持text/image
resp.setStatus(415); // 415 Unsupported Media Type
return;
}
// 读取文件内容到字节数组
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (InputStream is = filePart.getInputStream()) {
byte[] buffer = new byte[1024];
int len;
while ((len = is.read(buffer)) > 0) {
baos.write(buffer, 0, len);
}
}
byte[] content = baos.toByteArray();
// XSS转义 (简单替换, 实际用库如OWASP)
description = description.replace("<", "<").replace(">", ">");
// 生成唯一ID并存储
String id = UUID.randomUUID().toString().substring(0, 8); // 短UUID
fileStore.put(id, new FileData(description, content, contentType));
// 根据Accept头响应
String accept = req.getHeader("Accept");
if (accept != null && accept.contains("application/json")) {
resp.setStatus(201); // 201 Created
resp.setHeader("Location", req.getContextPath() + "/drops/" + id); // Location头
resp.setContentType("application/json");
PrintWriter out = resp.getWriter();
out.println("{"id":"" + id + "", "message":"Created"}");
} else {
resp.setStatus(302); // 302 Found (重定向)
resp.setHeader("Location", req.getContextPath() + "/drops/" + id); // 重定向到详情
}
}
}
1.5: 创建JSP文件
在
下删除默认index.jsp,New > JSP创建drops.jsp(上传表单和列表)。
src/main/webapp
jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>File Drops</title>
</head>
<body>
<h1>File Drops</h1>
<form method="post" enctype="multipart/form-data">
Description: <input type="text" name="description"><br>
File: <input type="file" name="file"><br>
<input type="submit" value="Upload">
</form>
<h2>Uploaded Files</h2>
<ul>
<!-- 列表在Servlet中动态生成 (doGet打印HTML) -->
</ul>
</body>
</html>
注释:列表在Servlet的doGet中用PrintWriter动态生成(简化,无需JSP标签;实际可移到JSP)。
1.6: 配置Tomcat并运行项目
Run > Edit Configurations > + > Tomcat Server > Local。Server tab:选择Tomcat 10+目录。访问http://localhost:8080/drops:显示表单。上传文件(POST):选择文件,输入描述,提交。
成功:重定向到/drops/{id},显示详情/下载链接。 测试重定向/JSON:
curl -X POST -F “description=test” -F “file=@test.txt” -H “Accept: application/json” http://localhost:8080/drops (返回201 + Location)。
1.7: 测试下载:
访问/drops/{id},触发下载。测试缓存:第一次GET返回ETag;第二次加-H “If-None-Match: “{etag}”” 返回304。测试错误:上传>5MB文件(413);不支持类型(415)。用浏览器开发者工具(F12 > Network)查看请求/响应头、状态码。curl示例:
上传:curl -v -F “description=test” -F “file=@test.jpg” http://localhost:8080/drops下载:curl -v http://localhost:8080/drops/{id} 实验观察:用开发者工具分析请求体(multipart)、响应头(Location/ETag/Content-Disposition)、状态码(302/201/304/413/415)。
以下是针对实验“Jakarta EE + Maven + Archetype:WebApp实现的HTTP实验”的思考与扩展部分。我将基于实验代码(DropsServlet.java等)进行分析和扩展设计。扩展将保持与原实验一致(使用内存存储、Jakarta EE Servlet),便于集成。如果您需要完整项目修改或测试,请提供反馈。
这些扩展体现了HTTP协议的灵活性(如内容协商、状态码使用),并引入实际应用设计考虑(如安全性、资源管理)。代码示例假设您已熟悉原实验,如果直接复制,请在IntelliJ中Reload Maven并重新运行Tomcat测试。
4.1 目前接口只支持 HTML 和 JSON 两种响应。如果客户端请求 Accept: application/xml,如何扩展?
思路解释
当前实现回顾:原实验中,DropsServlet的doPost根据Accept头返回HTML重定向(默认)或JSON(Accept: application/json)。这是HTTP内容协商(Content Negotiation)的典型应用,服务器根据客户端的Accept头选择响应格式。扩展需求:添加对Accept: application/xml的支持,返回XML格式响应(e.g., <drop><description>test</description></drop>)。这符合RESTful API设计,允许客户端指定偏好格式(如浏览器偏好HTML,工具如Postman偏好JSON/XML)。关键点:
检查Accept头的值(可包含多个,如text/xml,application/xml)。生成XML:使用JAXB(Jakarta XML Binding)注解实体类自动序列化(简单高效),或手动构建字符串。优先级:如果Accept包含多个,服务器可选择(这里优先XML > JSON > HTML)。挑战:XML生成需处理转义(防XSS,原有已处理);添加依赖(JAXB)。
设计方案
步骤:
添加JAXB依赖到pom.xml(用于XML序列化)。修改FileData类添加JAXB注解。在doPost中检查Accept,如果包含application/xml,返回XML响应(201 Created + Location头)。测试:用curl -H “Accept: application/xml”发送POST,验证XML输出。 潜在扩展:支持更多格式(如YAML),或用内容协商库(如Spring的,但这里纯Jakarta EE)。
代码示例
更新pom.xml(添加JAXB依赖):
<dependencies>
<!-- ... 原有依赖 -->
<!-- JAXB for XML (Jakarta版) -->
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
<version>3.0.1</version>
</dependency>
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>3.0.2</version>
</dependency>
</dependencies>
Reload Maven。
修改FileData类(添加JAXB注解):
// 在DropsServlet中
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlRootElement;
// FileData类
@XmlRootElement(name = "drop") // XML根元素
private static class FileData {
@XmlElement // XML子元素
String description;
// ... 原有字段 (忽略content for简单, 或添加@XmlElement(name="contentBase64") String getContentBase64() { return Base64.getEncoder().encodeToString(content); })
// ... 构造函数等
}
修改DropsServlet的doPost(添加XML支持):
// 在doPost方法末尾 (上传成功后)
String accept = req.getHeader("Accept");
if (accept != null && accept.contains("application/xml")) {
// XML响应
resp.setStatus(201);
resp.setHeader("Location", req.getContextPath() + "/drops/" + id);
resp.setContentType("application/xml");
// 使用JAXB生成XML
ObjectMapper mapper = new ObjectMapper(); // 等, 等待, 实际用JAXB
jakarta.xml.bind.JAXBContext context = jakarta.xml.bind.JAXBContext.newInstance(FileData.class);
jakarta.xml.bind.Marshaller marshaller = context.createMarshaller();
marshaller.setProperty(jakarta.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.marshal(fileStore.get(id), resp.getWriter()); // 输出XML
} else if (accept != null && accept.contains("application/json")) {
// 原JSON逻辑
// ...
} else {
// 原重定向逻辑
// ...
}
测试:
curl -v -X POST -F “description=test” -F “file=@test.txt” -H “Accept: application/xml” http://localhost:8080/drops预期响应:201状态,XML body如<drop><description>test</description></drop>,Location头。
挑战与思考:XML更重(解析开销),但适合遗留系统;实际生产用内容协商框架避免手动if-else。
4.2 试着把“资料投递箱”改造为“临时分享链接”,每个文件只允许下载一次或在一定时间后过期,你会如何设计?
思路解释
当前实现回顾:原系统是永久存储/无限下载的“投递箱”,使用内存Map保存文件。改造需求:变为“临时分享链接”,添加限制:(1) 只允许下载一次(后删除);(2) 一定时间后过期(e.g., 1小时)。这模拟临时文件分享服务(如WeTransfer),提高安全性(防止滥用)。关键点:
存储扩展:添加过期时间和下载计数字段。检查逻辑:在doGet(下载)时验证(过期→410 Gone,一次下载后删除→后续404)。清理机制:定时任务删除过期文件(用ScheduledExecutorService)。安全:防止绕过(e.g., ID猜测);添加密码或时效token(扩展)。挑战:内存存储不持久(重启丢失),生产用数据库/Redis;并发安全(synchronized访问Map)。
设计方案
架构:
FileData扩展:添加expireTime (long, millis)和downloadCount (int, 初始化0)。上传(doPost):生成ID时设置expireTime = System.currentTimeMillis() + 3600000 (1小时)。下载(doGet):检查expireTime > now(否则410);downloadCount < 1(否则404);下载后增count=1,如果=1删除entry。定时清理:Servlet init()启动线程,每分钟扫描删除过期。错误处理:过期返回410 Gone(资源曾经存在但已删除)。 潜在扩展:用数据库持久化;添加链接分享(生成token URL);限制总存储大小。HTTP语义:使用合适状态码(410 for过期,404 for不存在),符合REST原则。
代码示例
修改FileData类:
private static class FileData {
// ... 原有字段
long expireTime;
int downloadCount = 0;
FileData(String desc, byte[] cont, String type) {
// ... 原有
expireTime = System.currentTimeMillis() + 3600000; // 1小时过期
}
}
在DropsServlet添加定时清理(init中):
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.Iterator;
private ScheduledExecutorService scheduler;
@Override
public void init() throws ServletException {
super.init();
scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.scheduleAtFixedRate(this::cleanExpired, 1, 1, TimeUnit.MINUTES); // 每分钟清理
}
private void cleanExpired() {
long now = System.currentTimeMillis();
Iterator<Map.Entry<String, FileData>> iterator = fileStore.entrySet().iterator();
while (iterator.hasNext()) {
FileData file = iterator.next().getValue();
if (file.expireTime < now) {
iterator.remove();
}
}
}
@Override
public void destroy() {
super.destroy();
scheduler.shutdown();
}
修改doGet(添加检查):
// 在doGet的详情/下载部分
FileData file = fileStore.get(id);
if (file == null) {
resp.setStatus(404);
return;
}
long now = System.currentTimeMillis();
if (file.expireTime < now) {
fileStore.remove(id); // 删除过期
resp.setStatus(410); // 410 Gone
return;
}
if (file.downloadCount >= 1) {
resp.setStatus(404); // 已下载一次
return;
}
// ... 原下载逻辑
// 下载后
file.downloadCount = 1; // 标记已下载
测试:
上传文件,获取ID。立即下载:成功,之后再下返回404。等待>1小时:返回410。用curl验证状态码:curl -v http://localhost:8080/drops/{id}
挑战与思考:内存存储不适合生产(用数据库);定时清理有延迟(用Redis TTL更好);安全:添加认证防止滥用。
基于之前的HTTP实验(DropsServlet等),如果要定时清理有延迟,建议用Redis TTL改进。这是一个很好的点:原内存存储+定时任务的清理方式可能因调度延迟导致过期文件未及时删除(e.g., 分钟级延迟)。使用数据库可以持久化数据,而Redis的TTL(Time To Live)机制允许设置键自动过期(精确到秒),无需手动清理,效率更高。
我会提供两种方案:
方案1: 使用H2数据库(嵌入式SQL,简单集成JPA,但仍需定时任务清理过期;适合关系型数据)。方案2: 使用Redis with TTL(推荐,NoSQL键值存储,TTL自动过期,无延迟;适合临时文件)。
两种方案都基于您的Jakarta EE + Maven项目,代码设计包括:
pom.xml依赖更新。实体/数据结构修改。DropsServlet更新(上传时设置过期,下载时检查)。测试步骤。
前提:
项目已在IntelliJ中运行Tomcat。对于Redis,需要本地安装Redis服务器(下载从https://redis.io/,Windows版用WSL或MSI安装器;运行`redis-server`启动,默认端口6379)。测试:上传文件,检查过期/删除行为。
如果您选择H2,需前述persistence.xml;Redis更轻量,无需SQL。
方案1: 使用H2数据库(SQL + JPA + 定时清理)
优点:结构化存储(易查询),集成现有JPA;缺点:仍需定时任务清理(可能有延迟),但可优化为数据库触发器。设计思路:
添加FileEntity实体(包含id、description、content、expireTime)。上传时插入记录,设置expireTime。下载时查询并检查expireTime。定时任务:每分钟查询并删除过期记录(改进原内存清理)。
1. 更新pom.xml(添加/确认H2和Hibernate依赖)
<dependencies>
<!-- ... 原有依赖 -->
<!-- Hibernate (JPA) -->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
<version>6.1.7.Final</version>
</dependency>
<!-- H2 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.1.214</version>
<scope>runtime</scope>
</dependency>
</dependencies>
Reload Maven。
2. 添加persistence.xml(src/main/resources/META-INF/,如果无)
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="3.0" xmlns="https://jakarta.ee/xml/ns/persistence">
<persistence-unit name="FilePU" transaction-type="RESOURCE_LOCAL">
<provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
<class>com.example.FileEntity</class>
<properties>
<property name="jakarta.persistence.jdbc.driver" value="org.h2.Driver"/>
<property name="jakarta.persistence.jdbc.url" value="jdbc:h2:mem:filedb"/> <!-- 内存模式; 用file:./filedb for持久化 -->
<property name="jakarta.persistence.jdbc.user" value="sa"/>
<property name="jakarta.persistence.jdbc.password" value=""/>
<property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
<property name="hibernate.hbm2ddl.auto" value="update"/>
<property name="hibernate.show_sql" value="true"/>
</properties>
</persistence-unit>
</persistence>
3. 创建FileEntity.java(src/main/java/com/example)
package com.example;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Lob;
@Entity
public class FileEntity {
@Id
private String id; // UUID字符串
private String description;
@Lob // 大对象字段 (byte[])
private byte[] content;
private String contentType;
private long expireTime; // 毫秒时间戳
// 构造函数
public FileEntity(String id, String desc, byte[] cont, String type, long expire) {
this.id = id;
this.description = desc;
this.content = cont;
this.contentType = type;
this.expireTime = expire;
}
// Getters/Setters (省略部分, 根据需要添加)
public String getId() { return id; }
public byte[] getContent() { return content; }
public long getExpireTime() { return expireTime; }
// ... 其他
}
4. 修改DropsServlet.java(集成JPA和定时清理)
// ... 原import
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.Persistence;
import jakarta.persistence.Query;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
// DropsServlet类
public class DropsServlet extends HttpServlet {
private EntityManagerFactory emf = Persistence.createEntityManagerFactory("FilePU");
private ScheduledExecutorService scheduler;
@Override
public void init() throws ServletException {
super.init();
scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.scheduleAtFixedRate(this::cleanExpired, 1, 1, TimeUnit.MINUTES); // 每分钟清理
}
private void cleanExpired() {
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
long now = System.currentTimeMillis();
Query query = em.createQuery("DELETE FROM FileEntity f WHERE f.expireTime < :now");
query.setParameter("now", now);
int deleted = query.executeUpdate();
em.getTransaction().commit();
em.close();
System.out.println("Cleaned " + deleted + " expired files");
}
@Override
public void destroy() {
super.destroy();
scheduler.shutdown();
emf.close();
}
// doPost: 上传 (设置expireTime)
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// ... 原上传逻辑, 获取filePart, description, content, contentType
String id = UUID.randomUUID().toString().substring(0, 8);
long expireTime = System.currentTimeMillis() + 3600000; // 1小时
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
FileEntity file = new FileEntity(id, description, content, contentType, expireTime);
em.persist(file);
em.getTransaction().commit();
em.close();
// ... 原响应逻辑
}
// doGet: 下载/详情 (检查expireTime)
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// ... 原逻辑
EntityManager em = emf.createEntityManager();
FileEntity file = em.find(FileEntity.class, id);
em.close();
if (file == null) {
resp.setStatus(404);
return;
}
long now = System.currentTimeMillis();
if (file.getExpireTime() < now) {
// 删除 (或标记)
em = emf.createEntityManager();
em.getTransaction().begin();
em.remove(em.merge(file));
em.getTransaction().commit();
em.close();
resp.setStatus(410); // Gone
return;
}
// ... 原下载逻辑, 使用file.getContent()
}
}
5. 测试
上传文件,查询数据库确认expireTime。等待>1小时,尝试下载返回410。定时任务日志显示清理。
方案2: 使用Redis with TTL(推荐,无延迟清理)
优点:TTL自动过期(精确到秒),无需定时任务;高性能(内存存储);缺点:需安装Redis服务器。设计思路:
用Redis Hash存储文件(键: id, 字段: description/content/type)。上传时setex设置TTL(e.g., 3600秒)。下载时get检查存在(TTL自动删除过期)。无需清理线程。
1. 安装Redis
Windows:下载MSI from https://github.com/MicrosoftArchive/redis/releases(e.g., Redis-x64-3.0.504.msi),安装后运行
(默认端口6379)。
redis-server.exe
2. 更新pom.xml(添加Redis客户端Lettuce)
<dependencies>
<!-- ... 原有 -->
<!-- Lettuce (Redis客户端) -->
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>6.2.0.RELEASE</version>
</dependency>
</dependencies>
Reload Maven。
3. 修改DropsServlet.java(集成Redis TTL)
// ... 原import
import io.lettuce.core.RedisClient;
import io.lettuce.core.api.StatefulRedisConnection;
import io.lettuce.core.api.sync.RedisCommands;
// DropsServlet类
public class DropsServlet extends HttpServlet {
private RedisClient redisClient;
private StatefulRedisConnection<String, String> connection;
private RedisCommands<String, String> syncCommands;
@Override
public void init() throws ServletException {
super.init();
redisClient = RedisClient.create("redis://localhost:6379"); // Redis URL
connection = redisClient.connect();
syncCommands = connection.sync();
}
@Override
public void destroy() {
super.destroy();
connection.close();
redisClient.shutdown();
}
// doPost: 上传 (用hset存储, setex设置TTL)
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// ... 原上传逻辑, 获取description, content (Base64编码存储), contentType
String id = UUID.randomUUID().toString().substring(0, 8);
String contentBase64 = Base64.getEncoder().encodeToString(content);
syncCommands.hset(id, "description", description);
syncCommands.hset(id, "content", contentBase64);
syncCommands.hset(id, "contentType", contentType);
syncCommands.expire(id, 3600); // TTL 1小时 (自动过期)
// ... 原响应
}
// doGet: 下载/详情 (get检查存在, TTL自动处理过期)
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String id = req.getPathInfo().substring(1);
if (!syncCommands.exists(id)) { // 不存在或已过期 (TTL删除)
resp.setStatus(404);
return;
}
String description = syncCommands.hget(id, "description");
String contentBase64 = syncCommands.hget(id, "content");
byte[] content = Base64.getDecoder().decode(contentBase64);
String contentType = syncCommands.hget(id, "contentType");
// ... 原下载逻辑, 使用content
// 单次下载: 下载后删除 (或decr计数)
syncCommands.del(id); // 删除键 (只允许一次)
}
}
4. 测试
启动Redis服务器。上传文件,检查Redis CLI(redis-cli): keys * 查看键,TTL id 查看剩余时间。下载后键消失(404);超时后自动删除。
Redis方案更优雅(TTL零延迟),推荐生产使用。H2适合复杂查询。如果需要更多细节或完整代码,请告知!