ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

JSP+Servlet 网盘系统实战:文件上传下载与 MySQL 元数据管理

JSP+Servlet 网盘系统实战:文件上传下载与 MySQL 元数据管理 简介这是一套面向Java Web初学者与课程设计需求的网盘系统源码采用JSPServlet技术栈后端以MySQL存储数据适合作为毕业设计、课程实训或自学练手项目。压缩包共123个文件约2.28MB其中22个jsp页面承担前端展示与交互20个java源文件与20个class文件构成Servlet与DAO业务逻辑另有css、js及字体资源负责界面样式jar包与properties、xml配置支撑运行环境整体结构清晰、模块划分明确。项目已通过本地编译验证按文档配置环境后即可运行难度适中内容经助教审定覆盖用户管理、文件上传下载、关注关系等典型网盘功能。目前已有117人学习关注下载后可获得一套可直接运行的完整工程便于对照源码理解Servlet请求处理、过滤器与数据库访问的协作方式也适合在此基础上二次开发或作为答辩演示方案。1. JSPServlet 网盘系统为什么老技术栈反而更适合做文件管理入门如果你正在搜「JSPServlet 网盘系统 MySQL」大概率是三种人之一课程设计要交一个能跑的文件管理系统、想从零理解 HTTP 文件上传下载的底层链路、或者手头只有 Tomcat 环境不想引入 Spring 全家桶。这个组合放到今天不算新但它恰好把「文件存储 元数据管理 权限校验」三件事拆得足够干净没有框架帮你把 Servlet 生命周期、multipart 解析、JDBC 事务这些细节藏起来。网盘系统的核心不是界面多漂亮而是上传时文件流怎么落盘、下载时怎么防止路径穿越、列表分页时 MySQL 怎么建索引。这套技术栈能让你把这些环节全部手写一遍踩过的坑都是真实可迁移的。适合有 Java Web 基础、能配 Tomcat 和 MySQL、想做一个完整可演示项目的人。下面按「环境搭起来 → 上传下载跑通 → 数据库设计 → 避坑 → 进阶」的顺序讲每一步都给可抄的代码和参数。2. 环境与工程骨架Tomcat MySQL 的最小可运行组合2.1 为什么不用 Maven 也能跑但建议用JSPServlet 项目最常见的翻车点不是代码写错而是 jar 包没放对位置。Servlet API 由 Tomcat 提供不能打进 WEB-INF/lib否则会跟容器自带的版本冲突报ClassNotFoundException或NoSuchMethodError。MySQL 驱动、文件上传解析库commons-fileupload、commons-io则必须放进 WEB-INF/lib。用 Maven 的好处是依赖范围能写清楚servlet-api 用providedmysql-connector-java 和 commons-fileupload 用默认 compile。如果你坚持手动建 Dynamic Web Project那就记住一条Tomcat 的 lib 目录里已经有的你的 WEB-INF/lib 里不要再放。目录结构按标准来别自创WebContent/ index.jsp upload.jsp WEB-INF/ web.xml lib/ # mysql-connector-java-8.x.jar, commons-fileupload, commons-io classes/ # 编译后的 servlet static/ css/ js/2.2 数据库和连接参数怎么定MySQL 8.0 的驱动类名是com.mysql.cj.jdbc.Driver5.7 是com.mysql.jdbc.Driver写错直接连不上。连接 URL 必须带时区和字符集否则中文文件名会变问号// DBUtil.java —— 统一连接入口避免每个 Servlet 重复写 public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/netdisk?useUnicodetruecharacterEncodingutf8 serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue; private static final String USER root; private static final String PWD 你的密码; public static Connection getConn() throws SQLException { try { Class.forName(com.mysql.cj.jdbc.Driver); // 8.0 驱动 } catch (ClassNotFoundException e) { throw new SQLException(驱动未加载, e); } return DriverManager.getConnection(URL, USER, PWD); } }参数说明serverTimezone不写会在插入时间字段时抛The server time zone value is unrecognizeduseSSLfalse是本地开发省去证书配置生产环境要改成 true 并配 truststoreallowPublicKeyRetrievaltrue是 MySQL 8 默认 caching_sha2_password 认证方式下必须加的否则报Public Key Retrieval is not allowed。2.3 web.xml 里必须配的两件事Servlet 3.0 以后可以用注解WebServlet但文件上传的 multipart 配置在注解里写不如 web.xml 直观。建议在 web.xml 里声明欢迎页和错误页Servlet 用注解注册web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee version3.1 welcome-file-list welcome-fileindex.jsp/welcome-file /welcome-file-list error-page error-code404/error-code location/error/404.jsp/location /error-page /web-app配好之后启动 Tomcat访问http://localhost:8080/你的项目名/能看到 index.jsp 就说明骨架通了。这一步别急着写业务先把「Tomcat 能启动 MySQL 能连上」两个条件分别验证否则后面出问题你分不清是容器还是数据库。3. 上传与下载Servlet 处理文件流的完整链路3.1 上传commons-fileupload 的解析流程和参数网盘系统的上传不能直接用request.getParameter因为 multipart/form-data 的请求体是分段的Servlet 容器默认不解析。用 commons-fileupload 的标准流程是判断是否 multipart → 创建工厂 → 拿到 Item 列表 → 区分普通字段和文件字段 → 写盘。WebServlet(/upload) public class UploadServlet extends HttpServlet { // 上传根目录建议放在项目外避免重新部署被清空 private static final String UPLOAD_DIR D:/netdisk_data/; protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { if (!ServletFileUpload.isMultipartContent(req)) { resp.getWriter().write(表单必须设置 enctypemultipart/form-data); return; } DiskFileItemFactory factory new DiskFileItemFactory(); factory.setSizeThreshold(1024 * 1024); // 1MB 以内放内存 factory.setRepository(new File(System.getProperty(java.io.tmpdir))); ServletFileUpload upload new ServletFileUpload(factory); upload.setFileSizeMax(100L * 1024 * 1024); // 单文件 100MB upload.setSizeMax(500L * 1024 * 1024); // 整请求 500MB upload.setHeaderEncoding(UTF-8); // 解决中文文件名乱码 try { ListFileItem items upload.parseRequest(req); for (FileItem item : items) { if (item.isFormField()) { // 普通字段比如用户 id String uid item.getString(UTF-8); } else { String name new File(item.getName()).getName(); // 去掉路径 String saved UUID.randomUUID() _ name; // 防重名 File dest new File(UPLOAD_DIR, saved); item.write(dest); // 元数据入库见第 4 章 } } } catch (FileUploadBase.SizeLimitExceededException e) { resp.getWriter().write(文件超过限制); } catch (Exception e) { throw new ServletException(e); } } }关键参数setSizeThreshold决定多大文件开始写临时盘太小会频繁 IO太大吃内存setFileSizeMax和setSizeMax必须都设前者管单文件后者管整个请求只设一个会出现「单文件没超但总请求超了」的异常setHeaderEncoding(UTF-8)是中文文件名不乱码的关键很多人只设了 response 的编码却漏了这个。3.2 下载路径穿越防护和 Content-Disposition下载最容易出的安全事故是路径穿越。如果前端传fileName../../web.xml你直接拼路径就会把配置文件读出去。正确做法是数据库存的是逻辑文件名磁盘存的是 UUID 名下载时用 id 查库拿到真实路径绝不接受前端传路径。WebServlet(/download) public class DownloadServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String id req.getParameter(id); // 从数据库查 file_path 和 original_name而不是从请求里拿路径 FileMeta meta FileDao.findById(Integer.parseInt(id)); if (meta null) { resp.sendError(404); return; } File file new File(meta.getFilePath()); // 二次校验解析后的规范路径必须在允许目录内 if (!file.getCanonicalPath().startsWith(new File(D:/netdisk_data/).getCanonicalPath())) { resp.sendError(403); return; } resp.setContentType(application/octet-stream); String encoded URLEncoder.encode(meta.getOriginalName(), UTF-8) .replace(, %20); resp.setHeader(Content-Disposition, attachment; filename\ encoded \; filename*UTF-8 encoded); try (InputStream in new FileInputStream(file); OutputStream out resp.getOutputStream()) { byte[] buf new byte[8192]; int len; while ((len in.read(buf)) ! -1) out.write(buf, 0, len); } } }getCanonicalPath()会把..解析掉再做前缀比对这是防穿越最可靠的方式。Content-Disposition里同时给filename和filename*是为了兼容不同浏览器中文名必须 URL 编码否则下载下来是乱码。缓冲区 8192 是经验值太小频繁系统调用太大占内存8KB 在吞吐和内存之间比较平衡。4. MySQL 表设计与分页查询网盘系统的元数据怎么存4.1 三张表够用用户、文件、分享网盘系统的数据库不需要复杂但字段类型要选对。文件名用 varchar(255)大小用 bigintint 最大 2GB 不够时间用 datetime 并设默认值。文件表加索引是必须的否则文件一多列表页就慢。CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password CHAR(64) NOT NULL, -- SHA-256 十六进制 create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE file_meta ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, original_name VARCHAR(255) NOT NULL, stored_name VARCHAR(100) NOT NULL, -- UUID 名 file_path VARCHAR(500) NOT NULL, file_size BIGINT DEFAULT 0, upload_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_user_time (user_id, upload_time), -- 列表页按用户时间排序 KEY idx_name (original_name(50)) -- 按文件名搜索 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;idx_user_time是复合索引顺序不能反。列表页的查询是WHERE user_id? ORDER BY upload_time DESC复合索引按 user_id 先过滤再按时间排序能避免 filesort。如果只建 user_id 单列索引排序还是要额外排。idx_name用了前缀索引因为文件名很少整体匹配前缀 50 字符足够区分还能省索引空间。4.2 分页查询LIMIT 偏移量的坑分页最常见的写法是LIMIT offset, size但 offset 很大时 MySQL 会扫描并丢弃前面所有行越翻越慢。网盘系统文件量通常几千到几万LIMIT还能接受但要养成用「游标分页」的习惯-- 传统分页第 100 页时 offset9900要扫 9900 行 SELECT id, original_name, file_size, upload_time FROM file_meta WHERE user_id ? ORDER BY upload_time DESC LIMIT 9900, 100; -- 游标分页用上一页最后一条的时间做条件永远只扫 100 行 SELECT id, original_name, file_size, upload_time FROM file_meta WHERE user_id ? AND upload_time ? ORDER BY upload_time DESC LIMIT 100;游标分页的代价是不能跳页只能上一页下一页但网盘列表本来就不需要跳页。参数upload_time传上一页最后一条记录的时间如果同一秒有多条会漏所以实际项目里用(upload_time, id)组合做游标更稳。这个细节在文件批量上传时特别明显同一秒传十个文件只按时间分页会丢数据。4.3 事务上传时元数据和磁盘的一致性上传涉及两个动作文件写盘 元数据入库。如果入库失败但文件已经写了磁盘上就多一个孤儿文件。用事务包住数据库操作失败时在 catch 里删文件Connection conn null; try { conn DBUtil.getConn(); conn.setAutoCommit(false); FileDao.insert(conn, meta); // 入库 conn.commit(); } catch (SQLException e) { if (conn ! null) conn.rollback(); new File(destPath).delete(); // 回滚磁盘文件 throw e; } finally { if (conn ! null) conn.close(); }注意事务只管数据库磁盘文件不在事务范围内所以必须手动补偿。顺序上先写盘再入库因为写盘失败可以直接返回入库失败才需要删盘。反过来先入库再写盘写盘失败要删数据库记录逻辑一样但更容易漏。5. 避坑与排查JSPServlet 网盘系统最常见的 5 个翻车现场5.1 上传大文件报SizeLimitExceededException但限制明明设了现象设了setFileSizeMax(100MB)传 50MB 文件还是报超限。原因setSizeMax没设或设得比单文件限制还小commons-fileupload 会先检查整个请求大小。解决两个都设且sizeMax fileSizeMax同时检查 Tomcat 的maxPostSize和maxSwallowSize前者默认 2MB 会直接拦掉大请求。在 server.xml 的 Connector 上加maxPostSize524288000。5.2 中文文件名下载后变成乱码或下划线现象上传时文件名正常下载时变成____.pdf或乱码。原因Content-Disposition里的 filename 没有 URL 编码或者只用了filename没加filename*。解决按 3.2 的写法先URLEncoder.encode(name, UTF-8)再把替换成%20同时给两个 filename 参数。另外 Tomcat 8 以后 URI 默认 UTF-8但 GET 请求参数如果乱码要在 Connector 上加URIEncodingUTF-8。5.3 MySQL 连接报Public Key Retrieval is not allowed现象本地用 Navicat 能连Java 代码连就报这个。原因MySQL 8 默认认证插件是caching_sha2_passwordJDBC 需要公钥检索但默认禁止。解决连接 URL 加allowPublicKeyRetrievaltrueuseSSLfalse。如果生产环境必须用 SSL就改成useSSLtrue并配置 truststore不要为了省事一直关 SSL。5.4 列表页翻到后面越来越慢现象前几页秒开翻到几十页后要好几秒。原因LIMIT大偏移量导致全表扫描。解决改游标分页或者用子查询先查 id 再关联SELECT * FROM file_meta f JOIN (SELECT id FROM file_meta WHERE user_id? ORDER BY upload_time DESC LIMIT 9900,100) t ON f.idt.id。子查询走覆盖索引比直接LIMIT快但根治还是游标分页。5.5 重新部署后上传的文件全没了现象Tomcat 重新部署 war 包之前传的文件消失。原因上传目录设在了WebContent/upload或项目部署目录里重新部署会清空。解决上传根目录放在项目外比如D:/netdisk_data/数据库里存绝对路径。如果换服务器路径要能配置别硬编码在代码里抽到 properties 文件或环境变量。6. 进阶用 SHA-256 做秒传和文件去重网盘系统做到后面一定会遇到「同一个文件传多次」的问题。最实用的优化是秒传上传前先算文件的 SHA-256拿哈希去数据库查存在就直接在用户文件表里加一条引用不用再传文件流。这需要前端配合但即使前端不算后端在写盘后算一次也能做去重。// 计算文件 SHA-256用于秒传判断和去重 public static String sha256(File file) throws Exception { MessageDigest md MessageDigest.getInstance(SHA-256); try (InputStream in new FileInputStream(file)) { byte[] buf new byte[8192]; int len; while ((len in.read(buf)) ! -1) { md.update(buf, 0, len); // 流式更新避免大文件读进内存 } } StringBuilder sb new StringBuilder(); for (byte b : md.digest()) { sb.append(String.format(%02x, b)); } return sb.toString(); }表结构上把file_meta拆成两张file_hash存哈希和物理路径唯一索引user_file存用户和哈希的引用关系。上传时先查file_hash命中就只插user_file物理文件不重复写。删除时先删user_file再检查该哈希还有没有其他引用没有才删物理文件和file_hash记录。这个引用计数的逻辑一定要在事务里做否则并发删除会出现「文件被删了但还有用户引用」的情况。验证秒传是否生效最简单的办法是传同一个文件两次看D:/netdisk_data/下是不是只有一个物理文件数据库file_hash表是不是只有一条记录。如果每次传都生成新文件检查哈希计算是不是在写盘前做的、唯一索引有没有建。参数上SHA-256 比 MD5 慢但碰撞概率极低网盘场景用 SHA-256 更稳妥。如果文件特别大几个 GB计算哈希本身就要几秒可以在前端用File.slice分片算或者只算前 1MB 文件大小做快速判断命中后再算全量。这个取舍看你对秒传准确率的要求。我自己做这类项目最大的习惯是上传目录永远放在项目外数据库连接参数永远抽到配置文件路径穿越校验永远用getCanonicalPath而不是字符串contains。这三条帮我省了至少三次重新部署后数据丢失和两次安全扫描告警。希望帮到你。本文还有配套的精品资源点击获取
返回列表