ARTICLE DETAIL

资讯详情

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

JavaWeb电子书下载系统实战:JSP+Tomcat+SQL Server全流程

JavaWeb电子书下载系统实战:JSP+Tomcat+SQL Server全流程 简介这份资源是面向Java Web初学者与课程设计者的电子书下载系统完整实现方案基于Java、JSP与Tomcat技术栈后端采用SQLServer数据库适合作为毕业设计、课程作业或自学练手项目。压缩包约91.98MB内含项目全套源码与完整文档源码经过测试校正可百分百成功运行帮助读者快速理解电子书下载系统的整体架构与业务逻辑。项目围绕电子书浏览、下载、分类管理等核心模块展开配套文档对系统设计思路与实现细节进行了说明便于对照源码梳理开发流程。目前已有334人学习下载说明该方案在同类课程设计中具有一定参考价值。读者可从中获取可运行的工程代码、数据库脚本与设计文档用于二次开发、功能扩展或答辩准备尤其适合需要快速搭建Java Web项目原型的同学参考借鉴。1. 电子书下载系统从 JSP 页面到 SQL Server 落库一套能跑通的 JavaWeb 老架构很多人第一次接触 JavaWeb 就是从一个「电子书下载系统」开始的用户注册登录、图书列表分页、按分类筛选、点下载扣积分、后台管理上传。这套东西听起来像课程设计但它把 JSP、Servlet、Tomcat、SQL Server 这几样东西的核心用法全串了一遍。你如果正在搜「javajspTomcat 电子书下载系统」大概率是要交一份能演示、能讲清原理、还能自己改需求的作业或练手项目。它适合刚学完 Java 基础、想搞明白「浏览器请求到底怎么变成数据库里一行记录」的人。下面我按真实搭建顺序把环境、建库、页面、下载逻辑和踩坑点讲清楚你照着做能跑起来也能看懂每一层在干什么。2. 环境与选型为什么这套组合至今还在用2.1 Tomcat 和 JSP 的分工要拎清Tomcat 是一个 Servlet 容器它负责把浏览器发来的 HTTP 请求翻译成 Java 对象再交给你写的 Servlet 或 JSP 去处理。JSP 本质上是「写起来像 HTML 的 Servlet」——第一次被访问时Tomcat 会把它编译成一个 Java 类之后就走编译好的字节码。所以你在 JSP 里写的% %脚本片段最终都会变成_jspService()方法里的语句。电子书下载系统里JSP 主要干两件事展示页面图书列表、详情、登录注册表单和接收表单参数。真正的业务逻辑比如「扣积分」「写下载记录」建议放在 Servlet 或单独的 Java 类里JSP 只负责渲染。这样后面改需求时不会一锅粥。Tomcat 的版本选择上如果你用 JDK 8配 Tomcat 8.5 或 9 都行JDK 11 以上就选 Tomcat 9 或 10。注意 Tomcat 10 开始把javax.servlet改成了jakarta.servlet老代码直接搬过去会报 ClassNotFound这是新手最容易翻车的地方。2.2 SQL Server 建库三张核心表就够电子书下载系统的数据模型不复杂核心就三张表用户表、图书表、下载记录表。下面这段 SQL 可以直接在 SQL Server Management Studio 里执行建库建表一次到位。-- 创建数据库 CREATE DATABASE EbookDB; GO USE EbookDB; GO -- 用户表存登录信息和积分 CREATE TABLE users ( id INT IDENTITY(1,1) PRIMARY KEY, username NVARCHAR(50) NOT NULL UNIQUE, password NVARCHAR(100) NOT NULL, points INT DEFAULT 10, -- 注册送10积分 create_time DATETIME DEFAULT GETDATE() ); -- 图书表电子书基本信息 CREATE TABLE books ( id INT IDENTITY(1,1) PRIMARY KEY, title NVARCHAR(200) NOT NULL, author NVARCHAR(100), category NVARCHAR(50), file_path NVARCHAR(500), -- 服务器上文件相对路径 cover_img NVARCHAR(500), download_count INT DEFAULT 0, upload_time DATETIME DEFAULT GETDATE() ); -- 下载记录表谁下了哪本书用于扣积分和防重复 CREATE TABLE download_log ( id INT IDENTITY(1,1) PRIMARY KEY, user_id INT NOT NULL, book_id INT NOT NULL, download_time DATETIME DEFAULT GETDATE(), FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (book_id) REFERENCES books(id) );建表时几个参数要留意NVARCHAR而不是VARCHAR因为书名和用户名可能有中文IDENTITY(1,1)让主键自增插入时不用管 idGETDATE()是 SQL Server 的当前时间函数和 MySQL 的NOW()不一样。积分字段给个默认值 10这样新用户注册完就能直接下载一本演示时不会卡住。2.3 JDBC 连接串与驱动放置Java 连 SQL Server 用的是微软官方 JDBC 驱动常见做法是把mssql-jdbc-*.jar丢进WEB-INF/lib目录。连接串写法如下// 数据库连接工具类片段 public class DBUtil { private static final String URL jdbc:sqlserver://localhost:1433;databaseNameEbookDB;encryptfalse; private static final String USER sa; private static final String PWD 你的密码; public static Connection getConn() throws Exception { Class.forName(com.microsoft.sqlserver.jdbc.SQLServerDriver); return DriverManager.getConnection(URL, USER, PWD); } }encryptfalse这一句在本地开发时建议加上否则新版驱动会强制加密连接报证书错误。databaseName后面跟库名端口默认 1433。如果你在 SQL Server 配置管理器里改过端口这里要同步改。Class.forName那行是加载驱动虽然 JDBC 4.0 以后可以省略但老项目里保留着更稳。3. 从页面到数据库注册登录与图书列表的实现3.1 注册页面的表单提交与后端接收注册页面register.jsp就是一个普通 HTML 表单关键是action指向 Servletmethod用 post。!-- register.jsp 核心部分 -- form actionRegisterServlet methodpost 用户名input typetext nameusername requiredbr 密码input typepassword namepassword requiredbr input typesubmit value注册 /form后端RegisterServlet继承HttpServlet在doPost里取参数、写库。// RegisterServlet.java 核心逻辑 protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); // 必须放在取参数之前 String username req.getParameter(username); String password req.getParameter(password); String sql INSERT INTO users(username, password) VALUES(?, ?); try (Connection conn DBUtil.getConn(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); ps.executeUpdate(); resp.sendRedirect(login.jsp); } catch (Exception e) { e.printStackTrace(); resp.getWriter().write(注册失败 e.getMessage()); } }req.setCharacterEncoding(UTF-8)这行必须写在getParameter之前否则中文用户名会变乱码。用PreparedStatement而不是拼字符串是为了防 SQL 注入——这是血泪经验别图省事用Statement拼接。注册成功后重定向到登录页避免刷新时重复提交。3.2 图书列表分页SQL Server 的 OFFSET FETCH 写法分页是电子书下载系统的标配。SQL Server 2012 以后支持OFFSET ... FETCH比老式的ROW_NUMBER()写法清爽很多。-- 每页 10 条查第 2 页跳过前 10 条 SELECT id, title, author, category, download_count FROM books ORDER BY id OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY;注意OFFSET FETCH必须配合ORDER BY使用没有排序会直接报错。页码从 0 开始算的话偏移量就是page * pageSize。在 Java 里拼这个 SQL 时OFFSET和FETCH后面的数字不能直接用?占位符部分驱动不支持常见做法是用String.format拼进去但前提是这两个值来自程序计算而非用户输入避免注入。列表页bookList.jsp用 JSTL 的c:forEach遍历结果集渲染表格每行放一个「下载」链接指向DownloadServlet?bookIdxxx。3.3 下载扣积分事务要包住两条写操作下载逻辑是这个系统里唯一需要事务的地方扣用户积分和写下载记录必须同时成功或同时失败。// DownloadServlet.java 核心事务逻辑 protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int bookId Integer.parseInt(req.getParameter(bookId)); Integer userId (Integer) req.getSession().getAttribute(userId); if (userId null) { resp.sendRedirect(login.jsp); return; } Connection conn null; try { conn DBUtil.getConn(); conn.setAutoCommit(false); // 开启事务 // 1. 扣积分积分不足则不更新 String deduct UPDATE users SET points points - 1 WHERE id ? AND points 1; PreparedStatement ps1 conn.prepareStatement(deduct); ps1.setInt(1, userId); int rows ps1.executeUpdate(); if (rows 0) { conn.rollback(); resp.getWriter().write(积分不足); return; } // 2. 写下载记录 String log INSERT INTO download_log(user_id, book_id) VALUES(?, ?); PreparedStatement ps2 conn.prepareStatement(log); ps2.setInt(1, userId); ps2.setInt(2, bookId); ps2.executeUpdate(); // 3. 下载次数加一 String inc UPDATE books SET download_count download_count 1 WHERE id ?; PreparedStatement ps3 conn.prepareStatement(inc); ps3.setInt(1, bookId); ps3.executeUpdate(); conn.commit(); // 事务成功后再输出文件流 resp.sendRedirect(DownloadFileServlet?bookId bookId); } catch (Exception e) { if (conn ! null) conn.rollback(); e.printStackTrace(); } finally { if (conn ! null) { conn.setAutoCommit(true); conn.close(); } } }setAutoCommit(false)是开启事务的关键三条 SQL 全部执行完才commit()。扣积分那条 SQL 带了AND points 1这样积分不够时executeUpdate返回 0直接回滚不用先查再判断少一次数据库往返。文件流输出单独放在另一个 Servlet 里避免事务没提交就把文件推给浏览器。4. 文件上传与下载路径、编码和流处理4.1 上传电子书文件到服务器目录上传用commons-fileupload或 Servlet 3.0 自带的Part接口。这里用Part写法少引一个 jar。// UploadServlet.java 核心上传逻辑 protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); Part filePart req.getPart(ebookFile); // 表单里 file 控件的 name String fileName filePart.getSubmittedFileName(); // 存到 webapp 外的固定目录避免重新部署被清掉 String saveDir D:/ebook_files/; File dir new File(saveDir); if (!dir.exists()) dir.mkdirs(); filePart.write(saveDir fileName); // 把相对路径写进数据库 String sql INSERT INTO books(title, author, category, file_path) VALUES(?,?,?,?); try (Connection conn DBUtil.getConn(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, req.getParameter(title)); ps.setString(2, req.getParameter(author)); ps.setString(3, req.getParameter(category)); ps.setString(4, fileName); ps.executeUpdate(); } catch (Exception e) { e.printStackTrace(); } resp.sendRedirect(bookList.jsp); }getSubmittedFileName()拿到的是浏览器传来的原始文件名可能带路径不同浏览器行为不一致稳妥做法是用Paths.get(fileName).getFileName().toString()再取一次。存储目录放在 webapp 外面是因为 Tomcat 重新部署时会把 webapp 目录清空你上传的文件会跟着没这是很多人演示前才发现文件全丢的原因。4.2 下载时设置响应头与流拷贝下载 Servlet 要做三件事从数据库查文件路径、设置响应头告诉浏览器这是附件、把文件流拷到响应输出流。// DownloadFileServlet.java 核心下载逻辑 protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int bookId Integer.parseInt(req.getParameter(bookId)); String filePath null; String title null; String sql SELECT title, file_path FROM books WHERE id ?; try (Connection conn DBUtil.getConn(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, bookId); ResultSet rs ps.executeQuery(); if (rs.next()) { title rs.getString(title); filePath rs.getString(file_path); } } catch (Exception e) { e.printStackTrace(); } if (filePath null) { resp.getWriter().write(文件不存在); return; } File file new File(D:/ebook_files/ filePath); if (!file.exists()) { resp.getWriter().write(文件已丢失); return; } // 文件名编码防止中文乱码 String encodedName URLEncoder.encode(title .pdf, UTF-8) .replace(, %20); resp.setContentType(application/octet-stream); resp.setHeader(Content-Disposition, attachment; filename*UTF-8 encodedName); resp.setContentLengthLong(file.length()); try (InputStream in new FileInputStream(file); OutputStream out resp.getOutputStream()) { byte[] buf new byte[4096]; int len; while ((len in.read(buf)) ! -1) { out.write(buf, 0, len); } } }Content-Disposition里的filename*是 RFC 5987 的写法专门解决中文文件名乱码。老写法filename遇到中文会变问号或乱码。流拷贝用 4KB 缓冲区别一次读整个文件到内存大文件会把 Tomcat 内存撑爆。setContentLengthLong提前告诉浏览器文件大小下载进度条才能正常显示。5. 避坑与排查这套老架构最容易翻车的地方5.1 中文乱码三个位置都要设 UTF-8现象注册时输入中文用户名数据库里存进去变成???或者页面显示乱码。原因乱码可能出现在三个环节——请求体编码、响应编码、数据库排序规则。只改一处不够。解决请求端req.setCharacterEncoding(UTF-8)放在取参数前响应端resp.setContentType(text/html;charsetUTF-8)数据库建库时排序规则选Chinese_PRC_CI_AS字段用NVARCHAR。JSP 页面顶部加% page contentTypetext/html;charsetUTF-8 %。四处都对齐乱码基本消失。5.2 Tomcat 启动报 8080 端口被占用现象启动 Tomcat 时控制台报Address already in use: JVM_Bind服务起不来。原因8080 端口被别的程序占了常见的是另一个 Tomcat 实例没关干净或者某些开发工具自带的服务器。解决Windows 下netstat -ano | findstr 8080找到占用进程的 PID再taskkill /PID xxx /F干掉。或者改conf/server.xml里的Connector port8080为 8081。改端口后访问地址也要跟着改。5.3 SQL Server 连接报「无法找到数据库引擎启动句柄」现象JDBC 连接时报错提示无法建立连接或者 SQL Server 配置管理器里服务启动失败。原因SQL Server 服务没启动或者 TCP/IP 协议没启用。默认安装后 TCP/IP 是禁用的只开了共享内存Java 通过 TCP 连不上。解决打开 SQL Server 配置管理器找到「SQL Server 网络配置」下的协议把 TCP/IP 启用重启 SQL Server 服务。同时确认 1433 端口在防火墙里放行。混合验证模式要开启否则sa账号登不上。5.4 上传文件后重新部署就丢了现象上传的电子书文件在演示时好好的重启 Tomcat 或重新部署 war 包后下载全部报「文件已丢失」。原因文件存到了 webapp 目录下重新部署时 Tomcat 会清空整个应用目录。解决存储路径设在 webapp 外面比如D:/ebook_files/数据库里只存文件名。下载时用绝对路径拼接。这个坑几乎每个第一次做文件上传的人都会踩。5.5 分页查询 OFFSET 报语法错误现象执行分页 SQL 时报Incorrect syntax near OFFSET。原因SQL Server 2008 及更早版本不支持OFFSET FETCH这个语法是 2012 才引入的。或者ORDER BY没写。解决确认数据库版本是 2012 以上。如果必须兼容 2008改用ROW_NUMBER() OVER (ORDER BY id)子查询。另外检查ORDER BY是否存在OFFSET不能单独用。6. 进阶技巧把下载记录变成「猜你喜欢」的推荐位系统跑通之后最值得加的一个功能是基于下载记录的简单推荐。不用上协同过滤那套就用 SQL 的聚合查询在图书详情页下方显示「下载了这本书的人还下载了」。-- 查与当前书被同一批用户下载过的其他书按共同下载人数排序 SELECT b.id, b.title, COUNT(*) AS co_count FROM download_log d1 JOIN download_log d2 ON d1.user_id d2.user_id JOIN books b ON d2.book_id b.id WHERE d1.book_id ? -- 当前书 id AND d2.book_id ! ? -- 排除自己 GROUP BY b.id, b.title ORDER BY co_count DESC OFFSET 0 ROWS FETCH NEXT 5 ROWS ONLY;这条 SQL 的逻辑是先找出下载过当前书的所有用户再看这些用户还下载了哪些别的书按共同下载人数降序取前 5 本。d1和d2是同一张下载记录表的两次自连接d1锁定当前书d2找同用户的其他记录。参数两个都传当前书 id。在bookDetail.jsp里加一个查询把结果渲染成小列表点击跳转到对应书的详情页。这个功能代码量不到 30 行但演示时效果很好面试时也能讲出「基于物品的协同过滤雏形」这个点。验证推荐是否生效可以手动往download_log里插几条测试数据让同一个用户下载书 A 和书 B然后访问书 A 的详情页看下方是否出现书 B。如果没出现先检查 SQL 里的!有没有写成SQL Server 两种都认但别写成NOT 再确认GROUP BY的字段和SELECT里非聚合字段是否一致。我自己的习惯是每加一个查询功能先在 SSMS 里把 SQL 跑通、看到正确结果再往 Java 里搬。这样出问题时能立刻判断是 SQL 写错了还是 Java 传参错了省掉来回翻日志的时间。这套电子书下载系统虽然技术栈老但把 JSP、Servlet、JDBC、事务、文件流这几样东西串一遍后面换 Spring Boot 时你会知道框架帮你省了哪些事。希望帮到你。本文还有配套的精品资源点击获取
返回列表