当前位置: 首页 > news >正文

Jakarta EE 课程 --- 微型资料投递与分发(Mini Drop-off Box)

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: 在IntelliJ IDEA中使用Maven Archetype创建项目

  1. 打开IntelliJ IDEA,点击“File > New > Project”。
  2. 选择“Maven Archetype”。
  3. 配置:
    • Name:HttpExperiment。
    • Location:选择保存目录。
    • Archetype:点击“Add Archetype...”,填写:
      • GroupId:org.apache.maven.archetypes
      • ArtifactId:maven-archetype-webapp
      • Version:1.4(稳定版)。
    • 点击“OK”,然后“Next”。
  4. 项目属性:
    • GroupId:com.example
    • ArtifactId:httpexperiment
    • Version:1.0-SNAPSHOT
  5. 点击“Create”。IntelliJ生成项目,包括pom.xml和src/main/webapp/index.jsp。

步骤2: 更新pom.xml到Jakarta EE并添加依赖

  1. 双击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>
  1. 右键pom.xml > Maven > Reload project(下载依赖)。

步骤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>

步骤4: 创建Servlet类(DropsServlet.java)

src/main/java/com/example下New > Java Class创建DropsServlet.java。

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} 显示详情/下载@Overrideprotected 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 Foundreturn;}// 检查条件请求 (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 处理上传@Overrideprotected 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) {  // >5MBresp.setStatus(413);  // 413 Payload Too Largereturn;}String contentType = filePart.getContentType();if (contentType == null || (!contentType.startsWith("text/") && !contentType.startsWith("image/"))) {  // 示例: 只支持text/imageresp.setStatus(415);  // 415 Unsupported Media Typereturn;}// 读取文件内容到字节数组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("<", "&lt;").replace(">", "&gt;");// 生成唯一ID并存储String id = UUID.randomUUID().toString().substring(0, 8);  // 短UUIDfileStore.put(id, new FileData(description, content, contentType));// 根据Accept头响应String accept = req.getHeader("Accept");if (accept != null && accept.contains("application/json")) {resp.setStatus(201);  // 201 Createdresp.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);  // 重定向到详情}}
}

步骤5: 创建JSP文件

  • src/main/webapp下删除默认index.jsp,New > JSP创建drops.jsp(上传表单和列表)。

    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)。

步骤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)。

步骤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 id="xxx"><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)。

设计方案

  • 步骤
    1. 添加JAXB依赖到pom.xml(用于XML序列化)。
    2. 修改FileData类添加JAXB注解。
    3. 在doPost中检查Accept,如果包含application/xml,返回XML响应(201 Created + Location头)。
    4. 测试:用curl -H "Accept: application/xml"发送POST,验证XML输出。
  • 潜在扩展:支持更多格式(如YAML),或用内容协商库(如Spring的,但这里纯Jakarta EE)。

代码示例

  1. 更新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。
  2. 修改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); })// ... 构造函数等
    }
    
  3. 修改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生成XMLObjectMapper mapper = new ObjectMapper();  // 等, 等待, 实际用JAXBjakarta.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 {// 原重定向逻辑// ...
    }
    
  4. 测试
    • 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原则。

代码示例

  1. 修改FileData类

    private static class FileData {// ... 原有字段long expireTime;int downloadCount = 0;FileData(String desc, byte[] cont, String type) {// ... 原有expireTime = System.currentTimeMillis() + 3600000;  // 1小时过期}
    }
    
  2. 在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();
    }
    
  3. 修改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 Gonereturn;
    }
    if (file.downloadCount >= 1) {resp.setStatus(404);  // 已下载一次return;
    }// ... 原下载逻辑
    // 下载后
    file.downloadCount = 1;  // 标记已下载
    
  4. 测试
    • 上传文件,获取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 {@Idprivate 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;@Overridepublic 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");}@Overridepublic void destroy() {super.destroy();scheduler.shutdown();emf.close();}// doPost: 上传 (设置expireTime)@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {// ... 原上传逻辑, 获取filePart, description, content, contentTypeString 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)@Overrideprotected 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);  // Gonereturn;}// ... 原下载逻辑, 使用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),安装后运行redis-server.exe(默认端口6379)。

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;@Overridepublic void init() throws ServletException {super.init();redisClient = RedisClient.create("redis://localhost:6379");  // Redis URLconnection = redisClient.connect();syncCommands = connection.sync();}@Overridepublic void destroy() {super.destroy();connection.close();redisClient.shutdown();}// doPost: 上传 (用hset存储, setex设置TTL)@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {// ... 原上传逻辑, 获取description, content (Base64编码存储), contentTypeString 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自动处理过期)@Overrideprotected 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适合复杂查询。如果需要更多细节或完整代码,请告知!


文章转载自:

http://oCJQvopV.mmjqk.cn
http://pFEoyptQ.mmjqk.cn
http://GEyOWMvW.mmjqk.cn
http://IDIsYq09.mmjqk.cn
http://eP5bJFud.mmjqk.cn
http://67J2wY7a.mmjqk.cn
http://QIEVtWoC.mmjqk.cn
http://NgE5OfeF.mmjqk.cn
http://Or1HFUyk.mmjqk.cn
http://GxQmpiiu.mmjqk.cn
http://Uzbd0lC2.mmjqk.cn
http://RqNV9s3q.mmjqk.cn
http://NhwKzuqf.mmjqk.cn
http://3D6cHhCl.mmjqk.cn
http://Do2Yj4Kg.mmjqk.cn
http://RLMNo80q.mmjqk.cn
http://vyIQiar9.mmjqk.cn
http://9mb6tZbE.mmjqk.cn
http://Uv0AjeJ1.mmjqk.cn
http://XAExfaFW.mmjqk.cn
http://UXEMkxpe.mmjqk.cn
http://dyApBSj4.mmjqk.cn
http://3j4JWgek.mmjqk.cn
http://gF2sSouC.mmjqk.cn
http://QRmeTXA6.mmjqk.cn
http://Ke875k2P.mmjqk.cn
http://Honq7yzJ.mmjqk.cn
http://Oa5oLeup.mmjqk.cn
http://AKTSAPIW.mmjqk.cn
http://xL7jcv8n.mmjqk.cn
http://www.dtcms.com/a/376867.html

相关文章:

  • 【船类】监控录像下船舶类别检测识别数据集:近7k图像,6类,yolo标注
  • 《UE5_C++多人TPS完整教程》学习笔记51 ——《P52 使用我们的瞄准偏移(Using Our Aim Offsets)》
  • 腾讯云远程桌面连接不上?5步排查法解决RDP连接失败
  • ffplay播放pcm
  • 计算机毕业设计 基于Hadoop的B站数据分析可视化系统的设计与实现 Python 大数据毕业设计 Hadoop毕业设计选题【附源码+文档报告+安装调试】
  • 【Halcon 】Halcon 裁剪尺寸的像素陷阱全解析:为什么要 -0.5,为什么要 -1,而圆却不用?
  • 机器视觉质检数据融合PLM:产品缺陷根因分析新范式
  • 【超详细图文教程】2025年最新Win10 系统安装 MySQL 教程
  • 医疗行业面临的网络安全挑战及应对策略
  • JVM CMS垃圾回收器深度解析
  • 鸿蒙Next ArkWeb进程解析:多进程架构如何提升Web体验
  • Credo发布专为低功耗、高带宽与超低时延的AI网络打造的Bluebird 1.6T光DSP芯片
  • Shell 循环语句与函数全解析
  • Zookeeper核心知识全解:节点类型、集群架构与选举机制
  • Android 项目中 Gradle 配置实战:多渠道打包、签名配置、版本管理
  • 新手向:实现验证码程序
  • 【小程序】微信小程序隐私协议
  • LeetCode 刷题【71. 简化路径】
  • 【LeetCode 每日一题】1493. 删掉一个元素以后全为 1 的最长子数组——(解法一)预处理
  • Java代理模式详解
  • 【论文阅读】MEDDINOV3:如何调整视觉基础模型用于医学图像分割?
  • 超声波探伤的所用到的频段?
  • 关于ping不通,如何排查?
  • const allImages = { ...leftCategoryImages, ...rightCategoryImages }; 是是什么用法
  • 论文阅读:arxiv 2023 Large Language Models are Not Stable Recommender Systems
  • Transformer系列 | Pytorch复现Transformer
  • 神经网络常见层速查表
  • 算法练习——55.跳跃游戏
  • linux驱动开发
  • 今日分享 二分算法及多语言实现