ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JSP+Servlet网盘系统实战:从文件上传下载到分片秒传全链路

JSP+Servlet网盘系统实战:从文件上传下载到分片秒传全链路 简介这是一套面向Java Web初学者与课程设计需求的网盘系统源码采用JSPServlet技术栈后端结合MySQL数据库适合作为毕业设计、课程作业或Java Web入门练手项目。资源包共123个文件约2.28MB涵盖22个JSP页面、20个Java源文件与20个class文件另有CSS、JS、jar依赖及properties配置等结构上包含用户管理、文件上传下载、关注与权限过滤等模块便于理解MVC分层与请求处理流程。目前已有117人学习下载。源码经本地编译可运行按文档配置环境即可启动助教老师已审定内容难度适中。读者可借此掌握Servlet生命周期、过滤器与数据库交互等核心知识点并在此基础上进行功能扩展与二次开发遇到问题也可私信作者获得解答。1. 从零搭一套 JSPServlet 网盘系统为什么它仍是理解 JavaWeb 全链路的最佳练手项目很多人第一次接触 JavaWeb都是从「文件上传下载」这个场景入门的而 JSPServlet 网盘系统恰好把这条链路走全了前端表单提交、Servlet 接收、文件落盘、MySQL 记录元数据、JSP 渲染列表、再点下载把文件吐回去。它不依赖 Spring 全家桶没有自动装配的黑盒每一层都得自己写翻车了也看得见堆栈。这套东西适合两类人一是刚学完 Servlet 想找个完整案例练手的二是想搞清楚「一个文件从浏览器到磁盘再回到浏览器」到底经过哪些环节的。本文按「环境准备 → 数据库设计 → 上传下载核心 → 列表与权限 → 避坑 → 进阶」的顺序拆代码可直接抄参数会逐个说明。MySQL 用 8.0 版本JDK 用 8 或 11 都行Tomcat 用 9这套组合在 Windows 10 和 Linux 上都能跑通。2. 环境与数据库把 MySQL 8.0 和 Tomcat 的坑先填平2.1 JDK、Tomcat、MySQL 的版本搭配与安装顺序版本搭配这件事血泪经验就是别追新。JSPServlet 这套技术栈对 JDK 版本很敏感JDK 17 以上跑老版本 Tomcat 会出现模块访问被拒的报错新手很容易卡在这里。我一般推荐 JDK 8 或 JDK 11 配 Tomcat 9MySQL 用 8.0.x这个组合的兼容性最稳。安装顺序建议是 JDK → Tomcat → MySQL。JDK 装完配JAVA_HOME和PATH命令行敲java -version能出版本号就算过。Tomcat 解压后配CATALINA_HOME进bin目录执行startup.batWindows或startup.shLinux浏览器访问localhost:8080看到默认页就说明通了。MySQL 8.0 在 Windows 上用安装包Linux 上可以用 rpm 或 tar 包装完记得把bin目录加进PATH。# 验证三件套是否就位 java -version # 输出类似 java version 1.8.0_381 或 11.0.x echo %CATALINA_HOME% # Windows echo $CATALINA_HOME # Linux mysql --version # 输出 mysql Ver 8.0.xx for ...这三条命令是排错的第一道关卡。java -version报「不是内部或外部命令」说明PATH没配好CATALINA_HOME为空Tomcat 启动脚本会找不到bootstrap.jarmysql --version报错多半是bin目录没进环境变量。别小看这三步后面所有问题都建立在这三个能跑通的基础上。2.2 建库建表网盘系统需要哪几张表网盘系统的数据模型不复杂核心就两张表用户表和文件表。用户表存账号密码文件表存文件的元信息文件名、存储路径、大小、上传时间、所属用户。这里有个设计决策文件本身不存进数据库只存磁盘路径数据库里存的是「索引」。这么做的好处是数据库体积可控坏处是要自己保证磁盘和数据库的一致性。CREATE DATABASE netdisk DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE netdisk; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE t_file ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, original_name VARCHAR(255) NOT NULL, store_name VARCHAR(255) NOT NULL, file_size BIGINT DEFAULT 0, upload_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_user (user_id) ) ENGINEInnoDB;字符集必须用utf8mb4用utf8存中文文件名会出现乱码这是 MySQL 的老问题。store_name存的是磁盘上的实际文件名用 UUID 生成避免同名文件互相覆盖original_name存用户看到的原始名。file_size用BIGINTINT最大约 2GB大文件会溢出。idx_user索引是为了按用户查文件列表时走索引不然数据量上来后全表扫描会明显变慢。2.3 JDBC 连接 MySQL 8.0 的驱动与 URL 写法MySQL 8.0 的 JDBC 驱动类是com.mysql.cj.jdbc.Driver不再是 5.x 的com.mysql.jdbc.Driver。URL 也有变化必须带时区和 SSL 参数否则会报时区错误或 SSL 连接错误。public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/netdisk?useUnicodetruecharacterEncodingutf8 useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue; private static final String USER root; private static final String PWD your_password; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new RuntimeException(MySQL 驱动未找到, e); } } public static Connection getConn() throws SQLException { return DriverManager.getConnection(URL, USER, PWD); } }serverTimezoneAsia/Shanghai是必须的不写会报The server time zone value is unrecognized。useSSLfalse在本地开发时关掉 SSL省去证书配置的麻烦生产环境要开 SSL 并配证书。allowPublicKeyRetrievaltrue是 MySQL 8.0 默认加密插件caching_sha2_password要求的不加会报公钥检索失败。驱动 jar 包mysql-connector-java-8.0.x.jar要放进WEB-INF/lib目录不然 Tomcat 启动时找不到类。3. 上传与下载Servlet 处理文件流的完整链路3.1 用 Servlet 3.0 的 Part 接口接收上传文件Servlet 3.0 之后文件上传不用再依赖 Commons FileUpload 了直接用MultipartConfig注解加request.getPart()就能拿到文件流。这是最省事的做法少引一个依赖。WebServlet(/upload) MultipartConfig( fileSizeThreshold 1024 * 1024, // 1MB 以上写入磁盘 maxFileSize 1024 * 1024 * 500, // 单文件最大 500MB maxRequestSize 1024 * 1024 * 600 // 整个请求最大 600MB ) public class UploadServlet extends HttpServlet { private static final String STORE_DIR D:/netdisk_data/; Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); Part part req.getPart(file); if (part null || part.getSize() 0) { resp.getWriter().write(no file); return; } String originalName part.getSubmittedFileName(); String ext originalName.substring(originalName.lastIndexOf(.)); String storeName UUID.randomUUID().toString().replace(-, ) ext; File dir new File(STORE_DIR); if (!dir.exists()) dir.mkdirs(); File target new File(STORE_DIR storeName); try (InputStream in part.getInputStream(); OutputStream out new FileOutputStream(target)) { byte[] buf new byte[8192]; int len; while ((len in.read(buf)) ! -1) { out.write(buf, 0, len); } } // 写入数据库 User user (User) req.getSession().getAttribute(user); try (Connection conn DBUtil.getConn(); PreparedStatement ps conn.prepareStatement( INSERT INTO t_file(user_id, original_name, store_name, file_size) VALUES(?,?,?,?))) { ps.setInt(1, user.getId()); ps.setString(2, originalName); ps.setString(3, storeName); ps.setLong(4, part.getSize()); ps.executeUpdate(); } catch (SQLException e) { throw new ServletException(e); } resp.sendRedirect(list); } }fileSizeThreshold控制内存缓冲阈值小于这个值的文件先放内存超过就写临时文件。maxFileSize和maxRequestSize是硬限制超了会抛IllegalStateException前端要配合做大小校验。storeName用 UUID 加原扩展名保证磁盘上不重名。流拷贝用 8KB 缓冲区这是 IO 的常规做法太小频繁系统调用太大占内存。数据库写入放在文件落盘之后如果数据库写失败磁盘上会留孤儿文件后面避坑章节会讲怎么处理。3.2 下载Content-Disposition 与中文文件名编码下载的核心是设置响应头Content-Disposition告诉浏览器这是附件而不是内联展示。中文文件名要 URL 编码不然浏览器会乱码。WebServlet(/download) public class DownloadServlet extends HttpServlet { private static final String STORE_DIR D:/netdisk_data/; Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int fileId Integer.parseInt(req.getParameter(id)); User user (User) req.getSession().getAttribute(user); String originalName null; String storeName null; try (Connection conn DBUtil.getConn(); PreparedStatement ps conn.prepareStatement( SELECT original_name, store_name FROM t_file WHERE id? AND user_id?)) { ps.setInt(1, fileId); ps.setInt(2, user.getId()); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { originalName rs.getString(original_name); storeName rs.getString(store_name); } } } catch (SQLException e) { throw new ServletException(e); } if (storeName null) { resp.sendError(404); return; } File file new File(STORE_DIR storeName); resp.setContentType(application/octet-stream); String encoded URLEncoder.encode(originalName, UTF-8).replace(, %20); resp.setHeader(Content-Disposition, attachment; filename\ encoded \; filename*UTF-8 encoded); resp.setContentLengthLong(file.length()); 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); } } } }查询时带上user_id条件防止用户下载别人的文件这是权限校验的关键。URLEncoder.encode会把空格编成要替换回%20否则文件名里的空格会变成加号。filename*UTF-8是 RFC 5987 的写法现代浏览器优先认这个。setContentLengthLong用 long 版本大文件不会溢出。流式输出不要一次性读进内存500MB 的文件会把 JVM 撑爆。3.3 JSP 列表页把文件元数据渲染成可点击的表格列表页用 JSP 的 JSTL 标签遍历比在 JSP 里写 Java 代码干净。Servlet 负责查数据塞进 requestJSP 只负责展示。// ListServlet.java WebServlet(/list) public class ListServlet extends HttpServlet { Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { User user (User) req.getSession().getAttribute(user); ListFileItem list new ArrayList(); try (Connection conn DBUtil.getConn(); PreparedStatement ps conn.prepareStatement( SELECT id, original_name, file_size, upload_time FROM t_file WHERE user_id? ORDER BY upload_time DESC)) { ps.setInt(1, user.getId()); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { FileItem f new FileItem(); f.setId(rs.getInt(id)); f.setName(rs.getString(original_name)); f.setSize(rs.getLong(file_size)); f.setTime(rs.getTimestamp(upload_time)); list.add(f); } } } catch (SQLException e) { throw new ServletException(e); } req.setAttribute(files, list); req.getRequestDispatcher(/list.jsp).forward(req, resp); } }% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % table trth文件名/thth大小/thth上传时间/thth操作/th/tr c:forEach items${files} varf tr td${f.name}/td tdfmt:formatNumber value${f.size / 1024} pattern#0.0/ KB/td tdfmt:formatDate value${f.time} patternyyyy-MM-dd HH:mm//td tda hrefdownload?id${f.id}下载/a/td /tr /c:forEach /tableORDER BY upload_time DESC让最新上传的排前面符合网盘的使用习惯。JSTL 的fmt标签做数字和日期格式化比在 Java 里拼字符串灵活。${f.name}默认会做 HTML 转义防止文件名里的脚本注入这是 EL 表达式的安全特性。如果文件名含特殊字符导致页面错乱检查是不是用了% %而不是 EL。4. 避坑与排查JSPServlet 网盘系统最容易翻车的 5 个点4.1 上传大文件报「exceeded the maximum size」现象上传超过一定大小的文件页面报IllegalStateException: Request exceeds maxRequestSize或者直接 500。原因MultipartConfig的maxFileSize和maxRequestSize设小了或者根本没设用了默认值。Tomcat 对 multipart 请求有默认限制。解决把maxFileSize设成单文件上限maxRequestSize设成单文件上限的 1.2 倍左右留出表单其他字段的空间。同时前端也要做一次大小校验避免用户白等。如果还报错检查 Tomcat 的server.xml里Connector有没有maxPostSize限制默认是 2MB大文件上传要调大或设为 -1。4.2 中文文件名下载后变成乱码现象上传时文件名正常下载下来变成%E6%B5%8B%E8%AF%95.txt或者一堆问号。原因Content-Disposition头里的文件名没有正确编码或者用了filename但浏览器按 ISO-8859-1 解析。解决用URLEncoder.encode(name, UTF-8)编码同时提供filename*UTF-8的 RFC 5987 写法。两个都写上老浏览器认前者新浏览器认后者。注意URLEncoder把空格编成要替换成%20。4.3 MySQL 连接报「Public Key Retrieval is not allowed」现象JDBC 连接 MySQL 8.0 时报java.sql.SQLException: Public Key Retrieval is not allowed。原因MySQL 8.0 默认用caching_sha2_password认证插件JDBC 驱动需要检索公钥来加密密码但默认不允许。解决URL 里加allowPublicKeyRetrievaltrueuseSSLfalse。生产环境建议开 SSL 而不是关掉配置useSSLtrue并导入服务器证书。如果还不行检查 MySQL 用户是不是用mysql_native_password插件创建的可以用ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY pwd;改回来但不推荐安全性低。4.4 文件上传成功但数据库没记录现象磁盘上能看到文件但列表页不显示数据库t_file表是空的。原因文件落盘和数据库写入是两步操作中间抛异常了。常见的是user从 session 取出来是 null或者数据库连接失败。解决先检查 session 里有没有用户对象上传接口要做登录校验。数据库写入用 try-catch 包住失败时把已经落盘的文件删掉避免孤儿文件。更稳妥的做法是先写数据库再落盘但这样数据库记录可能指向不存在的文件两种方案各有取舍我一般选先落盘再写库失败时删文件。4.5 Tomcat 启动报「ClassNotFoundException: com.mysql.cj.jdbc.Driver」现象Tomcat 启动或第一次访问数据库时抛ClassNotFoundException。原因MySQL 驱动 jar 包没放进WEB-INF/lib或者放错了位置放到了 Tomcat 的lib目录但版本冲突。解决把mysql-connector-java-8.0.x.jar放到项目的WEB-INF/lib目录这是 Web 应用的标准类路径。如果项目用 Maven 管理加依赖时 scope 用compile而不是provided。检查 Tomcat 的lib目录有没有旧版本的驱动有的话删掉避免版本冲突。5. 进阶给网盘系统加上分片上传和秒传的验证思路基础版跑通后最值得加的进阶能力是分片上传和秒传。分片上传解决大文件断点续传问题秒传解决重复文件不重复存储的问题。这两个功能的核心都是「文件指纹」——用 MD5 或 SHA-256 对文件内容做哈希作为文件的唯一标识。秒传的验证逻辑是这样的前端在上传前先算文件的 MD5大文件可以用分片算再合并把 MD5 发给服务端查一下。如果t_file表里已经有这个 MD5 的记录直接返回「已存在」前端把文件标记为上传成功不用再传字节流。如果没有再走正常上传流程上传完成后把 MD5 存进数据库。// 秒传检查接口 WebServlet(/checkMd5) public class CheckMd5Servlet extends HttpServlet { Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String md5 req.getParameter(md5); resp.setContentType(application/json;charsetUTF-8); try (Connection conn DBUtil.getConn(); PreparedStatement ps conn.prepareStatement( SELECT id FROM t_file WHERE md5?)) { ps.setString(1, md5); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { resp.getWriter().write({\exists\:true,\id\: rs.getInt(id) }); } else { resp.getWriter().write({\exists\:false}); } } } catch (SQLException e) { throw new ServletException(e); } } }t_file表要加一列md5 VARCHAR(32)并建索引查询才快。前端算 MD5 用SparkMD5这类库分片读取文件算哈希避免一次性加载大文件到内存。分片上传则是把文件切成固定大小的块比如 5MB每块单独上传服务端按md5 chunkIndex存临时文件全部传完后合并。合并时按 chunkIndex 排序追加写入最后算整体 MD5 校验。验证秒传是否生效最简单的办法是上传同一个文件两次第二次看网络面板有没有实际传输字节。如果第二次请求体很小但返回成功说明秒传生效了。分片上传的验证是传一个 1GB 的文件中途断网再恢复看能不能从断点继续。这两个功能加起来代码量不小但理解了 MD5 作为文件指纹这个核心剩下的就是分片管理和状态记录的问题。我自己踩过的坑是MD5 算的时候用了FileInputStream一次性读整个文件500MB 的文件直接把内存打满。后来改成DigestInputStream配合 8KB 缓冲区流式读取内存占用就稳定了。还有一次是分片合并时没按索引排序合并出来的文件损坏排查了半天才发现是File.listFiles()返回顺序不保证。这些细节不写出来照着做的人大概率会再踩一遍。希望帮到你。本文还有配套的精品资源点击获取
返回列表