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

内容分享2小时前发布
0 0 0

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)


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} 显示详情/下载
    @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("<", "&lt;").replace(">", "&gt;");
 
        // 生成唯一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文件


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

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),安装后运行
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;
 
    @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适合复杂查询。如果需要更多细节或完整代码,请告知!

© 版权声明

相关文章

暂无评论

none
暂无评论...