简介:Java Web开发中,Servlet与JSP是理解请求处理流程的基石,而Tomcat作为Servlet容器,负责将HTTP请求映射到Java类,是贯穿前后端交互的核心组件。SQL Server作为企业级关系型数据库,其表结构设计与T-SQL方言(如OFFSET FETCH分页、IDENTITY自增)常与MySQL存在差异,熟悉这些细节能显著提升工程落地效率。以电子书下载系统为例,它完整覆盖了用户注册登录、图书分页检索、文件上传下载、后台权限管理等经典场景,是课程设计、毕业设计乃至Java开发岗位面试的高频素材。本文基于Servlet+JSP+Tomcat+SQL Server技术栈,从数据库设计、JDBC连接、Filter权限控制到Tomcat部署与常见坑排查,提供一套可直接复用的实现方案,帮助读者从“能跑”到“能用”,并理解完整Web请求链路。 很多人看到“基于Java+JSP+Tomcat+SQL Server的电子书下载系统”这类标题,第一反应往往是“太老了”“都什么年代了还在用JSP”。但我做毕设辅导和课程设计带练这几年下来,发现这套技术栈恰恰是需求量最大、最容易被问细节、也最值得彻底吃透的一类项目。原因很简单:它是太多高校软件工程、计算机科学专业教学体系里的主干内容,也是你面试Java开发岗位时,能快速讲清楚“请求到底是怎么被处理的”的最好素材。
这篇文章我就用这个“电子书下载系统”作为完整案例,从技术选型逻辑、数据库设计、前后台功能拆解、核心代码实现,到Tomcat+SQL Server的部署配置、常见致命坑的排查链路,一次性聊透。内容会尽量说人话,所有代码片段和SQL都是可以直接抄走用的级别。
1. 为什么这个毕设项目值得做:JSP+Servlet技术栈的生存法则
先别急着吐槽技术老旧。任何一个能持续被用作课程设计和毕业设计的项目,一定不是因为“新”,而是因为它的知识密度刚好匹配教学评估体系。这个电子书下载系统,前后台功能完整、涉及文件上传下载、权限区分、分页搜索、数据库关联查询,几乎把Web开发的基础环节全过了一遍。
1.1 技术栈选型的真实动机
我见过很多人上来就问:为什么不直接用Spring Boot?不是不能,而是要看你所在学校/学院的验收标准。多数课程设计和本科毕设的评分点里,“是否能完整阐述请求在Servlet/Filter中的流转过程”“是否能手写JDBC连接并解释驱动加载机制”是可以加分的,而Spring Boot把这一切都封装掉了。至于JSP,它的本质是在HTML里嵌Java代码(或通过EL/JSTL简化),用来理解“动态页面渲染”这个概念,成本最低。
这套系统的技术栈组合是:
- Java 8+:核心业务逻辑、Servlet、JavaBean
- JSP + JSTL/EL:视图层,渲染图书列表、详情页、后台管理页
- Tomcat 8.5/9.0:Servlet容器,负责请求接收与响应
- SQL Server 2012/2014/2019:数据持久化
- JDBC:Java连接SQL Server的标准方式,不需要额外ORM框架
1.2 用Tomcat做服务器的关键原因
Tomcat本质上是一个Servlet容器,它实现了Servlet规范,能识别注解(@WebServlet)和web.xml中的配置,把HTTP请求映射到对应的Java类上。选它而不是Jetty,核心原因是教学资源丰富、默认端口8080已被所有教程覆盖、配置方式直观。后面我会专门讲Tomcat的目录结构、如何修改端口、如何避免常见的JSP编译失败问题。
1.3 这个项目适合谁,能做到什么程度
如果你是正在做课程设计/毕业设计的学生,或者想用一套代码快速复现完整Web全流程的初学者,这个项目基本是量身定做。它能覆盖:
- 用户模块:注册、登录、Session管理、权限控制
- 图书模块:列表分页展示、按分类筛选、模糊搜索、详情页
- 下载模块:文件上传(后台)、文件下载(前台)、下载次数记录
- 后台管理:图书增删改、分类管理、用户管理、订单/下载记录统计
这套东西做完,你不但能答上“JSP和Servlet有什么区别”,还能自己动手演示“从浏览器地址栏输入URL到页面渲染,中间到底发生了什么”。
2. 系统功能模块拆解:前台下载与后台管理的完整闭环
电子书下载系统看起来简单,但模块划分如果做得清晰,代码维护起来会非常舒服。实测下来,我觉得把所有功能分成“前台用户端”和“后台管理端”两个层面来设计,既符合常规业务习惯,也方便后续扩展。
2.1 前台用户端的核心功能
前台主要服务的是注册用户,游客可以浏览,但下载必须登录,这样才有用户管理和积分/记录的意义。
注册与登录:
- 注册字段建议包含用户名、密码、确认密码、邮箱。不要存明文密码,用
MD5或SHA-256加盐处理。 - 登录后把用户信息放入
HttpSession,通过session.setAttribute("loginUser", user)实现用户态保持。
- 注册字段建议包含用户名、密码、确认密码、邮箱。不要存明文密码,用
图书浏览与检索:
- 首页展示推荐图书、最新上架、热门下载三个维度的列表。
- 分类导航:按编程、文学、历史、经济等分类筛选。
- 搜索功能:按书名、作者、ISBN模糊搜索,使用
LIKE '%keyword%'。
图书详情与下载:
- 详情页显示封面图、简介、文件大小、格式、下载次数。
- 下载按钮触发
DownloadServlet,鉴权通过后把文件流写回浏览器。 - 每成功下载一次,
download_count字段自增1,用于排行榜。
个人中心:
- 查看我的下载历史、修改密码。
2.2 后台管理端的功能构成
后台是给管理员用的,建议直接通过判断Session中的用户角色来决定能否访问/admin/下的页面。
- 管理员登录:独立于用户表,单独建
admin表,或者给用户表加is_admin字段。 - 图书管理:
- 新增图书:上传封面、填写书名、作者、ISBN、分类、简介、上传电子书文件(PDF/EPUB/TXT等)。
- 编辑/删除图书:删除时注意把服务器上的物理文件一并清理,否则会残留垃圾文件。
- 分类管理:增加、修改、删除分类,删除前检查该分类下是否还有图书。
- 下载记录管理:查看所有用户的下载记录,支持按时间/图书/用户筛选。
- 基础统计:在一个Dashboard页面里,用SQL聚合查询统计总图书数、总用户数、总下载次数、最热图书Top10。
2.3 一套代码两种角色的权限控制实现
我用过滤器+Session的方式实现后台拦截,比在每个Servlet里手写判断要优雅得多:
@WebFilter("/admin/*") public class AdminAuthFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; HttpSession session = request.getSession(false); Object admin = (session != null) ? session.getAttribute("admin") : null; if (admin == null) { response.sendRedirect(request.getContextPath() + "/admin/login.jsp"); return; } chain.doFilter(req, resp); } }这个小Filter解决了一个重要问题:确保所有/admin/*资源都必须登录后才能访问。你们可以试试不带这个过滤器直接访问后台页面的后果——直接被人绕过登录,后台裸奔。
3. SQL Server数据库设计:电子书系统的表结构与关系
数据库设计是这套系统的地基。SQL Server和MySQL在语法细节上有区别,我会把核心表结构和关键SQL语句都列出来,方便直接跑。
3.1 核心表结构设计
我建议至少设计以下6张表:
| 表名 | 用途 | 关键字段 |
|---|---|---|
users | 前台用户 | id, username, password, email, create_time |
admin | 管理员 | id, username, password, create_time |
category | 图书分类 | id, name, sort_order |
book | 电子书信息 | id, title, author, isbn, category_id, cover_url, file_url, file_size, format_type, download_count, intro, create_time |
download_record | 下载记录 | id, book_id, user_id, download_time |
notice | 公告 | id, title, content, create_time |
我在做这个项目时踩过一个坑:一开始把下载记录表字段设计成记录所有信息,结果统计时发现数据重复严重。后来改成只存book_id和user_id的关联表,通过JOIN查询去拿图书名和用户名,逻辑清爽很多。
3.2 SQL Server下的建表SQL实例
这里给出book表的SQL Server建表语句,注意SQL Server支持IDENTITY(1,1)自增,和MySQL的AUTO_INCREMENT写法不同:
CREATE TABLE book ( id INT IDENTITY(1,1) PRIMARY KEY, title NVARCHAR(200) NOT NULL, author NVARCHAR(100), isbn NVARCHAR(50), category_id INT NULL, cover_url NVARCHAR(255), file_url NVARCHAR(255) NOT NULL, file_size INT DEFAULT 0, format_type NVARCHAR(20) DEFAULT 'pdf', download_count INT DEFAULT 0, intro NVARCHAR(MAX), create_time DATETIME DEFAULT GETDATE(), CONSTRAINT fk_book_category FOREIGN KEY (category_id) REFERENCES category(id) );注意两个点:第一,NVARCHAR而不是VARCHAR,是因为SQL Server下NVARCHAR能正确处理中文和Unicode;第二,外键约束fk_book_category建议保留,虽然有人嫌它影响插入效率,但对数据完整性是好事。
3.3 高频查询SQL的写法
分页查询(SQL Server 2012+版本支持OFFSET...FETCH,比老式ROW_NUMBER()好用得多):
SELECT * FROM book ORDER BY create_time DESC OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY;统计下载次数排行Top10:
SELECT TOP 10 title, download_count FROM book ORDER BY download_count DESC;按分类统计图书数量:
SELECT c.name, COUNT(b.id) AS book_count FROM category c LEFT JOIN book b ON c.id = b.category_id GROUP BY c.name;模糊搜索:
SELECT * FROM book WHERE title LIKE N'%' + ? + N'%' OR author LIKE N'%' + ? + N'%';3.4 设计时要考虑到的问题
一是封面图和电子书文件不要直接存数据库,只存文件路径URL,这样数据库体积可控。二是download_record表一定要索引(book_id, user_id),不然下载量大了以后JOIN查询会明显变慢。三是善用SQL Server的GETDATE()做默认时间,比Java代码里取时间再插值更可靠。
4. 核心功能代码实现:上传、下载、分页这些关键代码怎么写
所有功能里,文件上传和文件下载是最容易出问题的两个环节,也是答辩时老师最喜欢追问的地方。我分开说重点。
4.1 文件上传的实现思路
后台管理员上传电子书时,会同时提交图书信息和文件。这里需要用到commons-fileupload和commons-io两个库。核心代码逻辑:
// 设置上传大小限制 DiskFileItemFactory factory = new DiskFileItemFactory(); ServletFileUpload upload = new ServletFileUpload(factory); upload.setFileSizeMax(50 * 1024 * 1024); // 单文件最大50MB upload.setSizeMax(100 * 1024 * 1024); // 总请求最大100MB List<FileItem> items = upload.parseRequest(request); for (FileItem item : items) { if (item.isFormField()) { // 处理普通表单字段,如书名、作者、ISBN String fieldName = item.getFieldName(); String value = item.getString("UTF-8"); } else { // 处理文件字段 String fileName = new File(item.getName()).getName(); File file = new File(uploadDir, fileName); item.write(file); } }上传路径建议动态获取,不要硬编码绝对路径:
String uploadDir = getServletContext().getRealPath("/") + "uploads" + File.separator + "books"; File dir = new File(uploadDir); if (!dir.exists()) dir.mkdirs();4.2 文件下载的完整响应逻辑
下载功能的关键在于两点:设置HTTP响应头和用流把文件写回。下面给出一个可复用的DownloadServlet:
@WebServlet("/download") public class DownloadServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 鉴权:未登录直接跳转 HttpSession session = request.getSession(false); if (session == null || session.getAttribute("loginUser") == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } // 2. 根据bookId查询文件路径 int bookId = Integer.parseInt(request.getParameter("bookId")); BookDao dao = new BookDao(); Book book = dao.findById(bookId); File file = new File(book.getFileUrl()); // 3. 设置响应头 response.setContentType("application/octet-stream"); response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(file.getName(), "UTF-8")); response.setContentLengthLong(file.length()); // 4. 写文件流 try (InputStream is = new FileInputStream(file); OutputStream os = response.getOutputStream()) { byte[] buffer = new byte[8192]; int bytesRead; while ((bytesRead = is.read(buffer)) != -1) { os.write(buffer, 0, bytesRead); } } // 5. 下载次数+1并插入记录 dao.incrementDownloadCount(bookId); new DownloadRecordDao().insert(bookId, ((User) session.getAttribute("loginUser")).getId()); } }这段代码里最容易被忽略的地方是**URLEncoder.encode(file.getName(), "UTF-8")**。如果不加这一行,中文文件名的下载会乱码,尤其IE/Edge浏览器会直接把文件名解析成乱码字符串。
4.3 分页功能的封装技巧
分页是图书列表页的刚需。我的做法是封装一个PageBean<T>:
public class PageBean<T> { private List<T> list; // 当前页数据 private int pageNum; // 当前页码 private int pageSize; // 每页数量 private int totalCount; // 总记录数 private int totalPages; // 总页数 public PageBean(int pageNum, int pageSize, int totalCount, List<T> list) { this.pageNum = pageNum; this.pageSize = pageSize; this.totalCount = totalCount; this.list = list; this.totalPages = (int) Math.ceil((double) totalCount / pageSize); } }BookDao里的分页查询方法,动态拼接搜索条件:
public PageBean<Book> findPage(int pageNum, int pageSize, Integer categoryId, String keyword) { StringBuilder sql = new StringBuilder("SELECT * FROM book WHERE 1=1 "); List<Object> params = new ArrayList<>(); if (categoryId != null) { sql.append("AND category_id = ? "); params.add(categoryId); } if (keyword != null && !keyword.trim().isEmpty()) { sql.append("AND (title LIKE ? OR author LIKE ?) "); params.add("%" + keyword + "%"); params.add("%" + keyword + "%"); } sql.append("ORDER BY create_time DESC OFFSET ? ROWS FETCH NEXT ? ROWS ONLY"); params.add((pageNum - 1) * pageSize); params.add(pageSize); return queryPage(sql.toString(), params); }写分页时有个关键点:SQL Server的OFFSET子句不能单独使用,必须跟FETCH NEXT搭配,而FETCH NEXT又要求ORDER BY存在。这个顺序写错会直接报语法错误。
4.4 JDBC连接工具类:如何稳定连接SQL Server
JDBC连SQL Server要比连MySQL多一些“坑”,最典型的四个:
- 驱动类名是
com.microsoft.sqlserver.jdbc.SQLServerDriver,不要写错。 - 连接URL格式是
jdbc:sqlserver://localhost:1433;DatabaseName=ebook_db,分号分隔,不是/。 - SQL Server默认可能禁用TCP/IP协议,要在“SQL Server配置管理器”里启用。
- 需要下载
mssql-jdbc-*.jar(例如mssql-jdbc-9.4.1.jre8.jar),放到WEB-INF/lib下。
一个可复用的工具类:
public class DBUtil { private static final String DRIVER = "com.microsoft.sqlserver.jdbc.SQLServerDriver"; private static final String URL = "jdbc:sqlserver://localhost:1433;DatabaseName=ebook_db;encrypt=false"; private static final String USER = "sa"; private static final String PASSWORD = "123456"; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }5. Tomcat+SQL Server部署:从零到能跑通的完整配置
许多人在IDE里点一下运行就能看到页面,但一旦要部署到独立Tomcat、换一台机器,就各种问题。这一节我把配置细节全部铺开。
5.1 开发环境与生产环境的目录差异
在Eclipse/IDEA里,项目部署到Tomcat的路径是自动管理的;但如果你拿到一个已经打包好的WAR包(比如标题里那个zip解压出来的项目),你需要手动把它放到Tomcat的webapps目录下,然后启动Tomcat。
Tomcat目录重点关注:
| 目录/文件 | 作用 |
|---|---|
bin/startup.bat/shutdown.bat | Windows下启动/停止 |
conf/server.xml | 端口、Host配置 |
conf/tomcat-users.xml | 管理员账号配置 |
webapps/ | 放置WAR包或解压后的项目目录 |
lib/ | 全局共享Jar包(比如JDBC驱动) |
logs/ | 日志输出,报错必查 |
5.2 修改端口与上下文路径
默认端口是8080。如果你本机已经占用了8080,或者想直接通过http://localhost:8080/ebook访问,修改conf/server.xml里的Connector节点:
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />上下文路径(Context Path)取决于你WAR包的名字。如果WAR包叫ebook.war,部署后访问路径就是http://localhost:8080/ebook/。如果想让项目跑在根路径http://localhost:8080/,可以把WAR包改名为ROOT.war,或者解压后把内容放到webapps/ROOT目录,删掉原来的ROOT目录。
5.3 JDBC驱动的两难放置位置
这是个非常经典的坑:驱动Jar包到底放WEB-INF/lib还是Tomcat的lib目录?
两种方案都行,但有个坑要特别说:如果WEB-INF/lib里放了旧版本驱动(比如mssql-jdbc-6.x),而Tomcatlib里放了新版本,容易出现“加载类冲突”。我自己的习惯是:只在WEB-INF/lib里放驱动,保持项目自包含,换Tomcat环境时也能运行。如果是多项目共用Tomcat,或者项目里已经没有WEB-INF/lib了,再放到Tomcat的lib目录。
5.4 SQL Server端需要检查的配置
连接SQL Server报“与网络相关”的错误时,90%的根因是SQL Server没开启TCP/IP或端口不对,而不是代码问题。排查顺序:
- 打开“SQL Server配置管理器”。
- 找到“SQL Server网络配置”,启用“TCP/IP”协议。
- 右键“TCP/IP” -> “属性” -> “IP地址”页签,确认
IPAll的TCP端口是1433。 - 重启SQL Server服务。
- 确认防火墙放行1433端口。
如果是在本机测试,连127.0.0.1即可,注意连接串里不要写localhost时有问题就换成127.0.0.1,两种写法在SQL Server上解析有时不太一样。
5.5 Maven/Gradle依赖管理与非Maven项目的取舍
这个项目的源码包通常不是Maven工程,而是传统的Web目录结构(src+WebContent)。如果你拿到的是这种结构,在IDEA里导入时要注意:
- 设置“Project Structure” -> “Facets” -> “Web”的Web资源目录为
WebContent或webapp。 - 添加Tomcat Server作为Application Server。
- 确保
WEB-INF/lib里有jstl.jar和standard.jar(JSTL需要),以及mssql-jdbc驱动的Jar包。
如果不想用Maven,最简单的方式就是手动管理Jar包;如果项目已自带pom.xml,用Maven仓库拉依赖会更省事。考试和答辩时,最好能说清楚“没有用Spring是因为想体现Servlet和JSP的原生原理”,会更有说服力。
6. 实战中的坑:JDBC连接失败、文件下载乱码等问题的排查链路
这一节我把平时带学员时遇到频率最高的几个问题整理成“排查链路”,而不是直接给结论。这样你遇到同类问题时,能按图索骥,而不是每次百度答案。
6.1 连接SQL Server报错:建立连接时出现与网络相关的错误
完整报错往往是:
java.sql.SQLException: 在与 SQL Server 建立连接时出现与网络相关的或特定于实例的错误。未找到或无法访问服务器。排查链路:
- 先做基础连通性测试:打开cmd,输入
telnet 127.0.0.1 1433。如果提示无法打开,说明SQL Server的TCP/IP端口没监听或防火墙拦截。 - 打开“SQL Server配置管理器” -> “SQL Server网络配置” -> 启用“TCP/IP”。
- 确认SQL Server服务状态是“正在运行”。右键重启服务。
- 再测试
telnet 127.0.0.1 1433,如果有黑窗口且不报错,说明端口通了。 - 如果端口通但仍然报错,检查连接URL里是否写了正确的数据库名。
DatabaseName=ebook_db如果不存在,也会报“无法访问服务器”的误导性错误。 - 检查驱动版本是否兼容JDK版本。如果你是JDK 17还用旧版
mssql-jdbc-6.x,大概率报类加载错误,换成mssql-jdbc-9.4.1.jre8.jar或更新版本即可。
这个排查链路是我实测几十次后总结的,顺序一定不要乱。先网络、再配置、最后代码。
6.2 文件下载时中文文件名乱码
报错表现是:浏览器保存文件时,文件名变成????.pdf或一串%E7%94%B5%E5%AD%90%E4%B9%A6.pdf的百分号编码。
原因分析:Content-Disposition头里的filename,不同浏览器对编码的解析标准不同。最通用的写法是filename*=UTF-8''加URL编码,但有些老浏览器不支持。兼容写法是在代码里同时给出filename和filename*两个参数:
String encodedFileName = URLEncoder.encode(file.getName(), "UTF-8").replaceAll("\\+", "%20"); response.setHeader("Content-Disposition", "attachment;filename=\"" + encodedFileName + "\";filename*=UTF-8''" + encodedFileName);这里有个细节:URLEncoder.encode默认会把空格转成+,但在URL参数里+表示空格,在Content-Disposition里不会识别为空格,所以必须手动把+替换成%20。这个问题如果不亲自踩一遍,理解不会这么深。
6.3 IDEA中JSP无法编译:incremental annotation processing is disabled
新版本IDEA在编译JSP时,偶尔会警告incremental annotation processing is disabled,原因通常是对应的javac版本过高或Lombok相关注解处理器冲突。处理方式:
- 打开
Settings -> Build, Execution, Deployment -> Compiler,取消勾选“Build project automatically”造成的增量编译问题。 - 在
pom.xml中显式指定maven-compiler-plugin的版本和source/target为1.8。 - 如果还不行,检查
Project Structure -> Project里的Language Level是否比实际JDK版本高。
6.4 JSP页面里函数点击无法跳转
有些学员在JSP页面里用<a href="javascript:void(0)" onclick="showXXX()">,但点击后没反应。排查链路:
- 打开浏览器F12控制台,看有没有JS报错。前端JS变量名和Java后台的
request.setAttribute里的key搞混,是初学者最常见的坑。 - 检查函数名是否重复定义。JSP引入的公共JS文件和页面内联JS里如果有同名函数,后定义的会覆盖先定义的。
- 确认函数调用传参是否是字符串。比如
onclick="detail(bookId)",如果bookId是Java变量但没加${},页面上会直接输出bookId这个字符串,自然找不到变量。
6.5 Tomcat启动报OutOfMemoryError
运行一段时间后,Tomcat弹出java.lang.OutOfMemoryError: insufficient memory或者PermGen space,通常是本地Tomcat启动时分配的JVM堆内存太小。在Tomcat的bin/catalina.bat(Windows)或catalina.sh(Linux)里增加:
set CATALINA_OPTS=-Xms256m -Xmx1024m -XX:MaxMetaspaceSize=512m如果项目本身很轻量,256~512MB已经足够。这个配置在高并发下载场景尤其重要,因为文件流传输时会占用堆外内存和直接内存。
6.6 连接SQL Server报“查询失败,返回错误为”的几种可能
这类报错在SQL Server驱动下经常是因为SQL语法与方言不兼容:
- 用了MySQL的
LIMIT ? OFFSET ?,SQL Server不识别,应该用OFFSET ? ROWS FETCH NEXT ? ROWS ONLY。 GROUP BY后的非聚合列没有全部包含在GROUP BY里,SQL Server在这方面的校验比MySQL严格得多。- 字符串拼接用了
+号没有加N前缀,遇到中文参数时在SQL Server里容易变成乱码,建议参数化查询。
我记得有次一位同学把MySQL的auto_increment直接搬到SQL Server里用,结果表死活建不出来。SQL Server的自增不是写在字段类型定义里,而是IDENTITY(1,1),这是方言差异,值得单独写一行加粗。
7. 从“能跑”到“能用”:我建议你增加的几个高分功能
如果说前面都是把系统做出来,那这一节更像是在“做完力”基础上帮你拔高分。很多课程设计和毕设的分数差异,都体现在这些细节上。
7.1 给下载功能加上每日次数限制
不加限制,会有用户批量下载。最简单的实现方式:在download_record表里查询当前用户当天已下载次数,超过阈值(比如每天10本)就拒绝下载:
SELECT COUNT(*) FROM download_record WHERE user_id = ? AND CONVERT(date, download_time) = CONVERT(date, GETDATE());7.2 登录时增加“记住我”功能
用Cookie实现,把用户名加密后保存到Cookie里,7天内有效。加密可以用简单的Base64加个盐,或者用AES。
7.3 导入Excel批量添加图书
用Apache POI读取Excel,一行数据就是一条图书记录。这个功能在答辩时非常讨巧,因为“批量导入”展示了工程化思维。
7.4 把头像切到本地存储或对象存储
如果你觉得把封面图放本机磁盘不够“专业”,可以在application.properties或config.properties里配置OSS的AK/SK,把上传的文件直接传到OSS,数据库只存URL。这样一个简单的配置替换,就能展示你对云存储的理解。
8. 源码使用前的检查清单与二次开发建议
你手里如果有这份“源码+文档”压缩包,一定不要解压后直接就跑,要先按清单检查环境,否则很容易被一些细节问题卡住半天。
8.1 拿到源码后必做的5件事
- 检查JDK版本:项目如果是Eclipse工程,大概率是JDK 8。本地环境如果是JDK 17,编译时可能出现
source/target 1.8不支持的报错,需要调整Project Structure。 - 检查数据库脚本:解压包里通常有个
.sql文件。用SQL Server Management Studio执行前,先确认目标数据库名和脚本里的是否一致。 - 检查
WEB-INF/lib下的Jar包:是否包含mssql-jdbc、jstl、standard、commons-fileupload、commons-io。少一个都会在运行时报ClassNotFound。 - 修改数据库连接配置:在
DBUtil.java或db.properties里,把账号密码改成你自己的SQL Server账号。 - 初始化管理员账号:确认脚本里是否有一条
INSERT INTO admin VALUES ('admin', MD5('123456'))。如果没有,需要手动插一条,或者写个InitAdminServlet来创建。
8.2 SQL Server与MySQL不兼容点的速查表
| 功能 | MySQL写法 | SQL Server写法 |
|---|---|---|
| 字符串拼接 | CONCAT(a, b) | a + b |
| 分页 | LIMIT ?, ? | OFFSET ? ROWS FETCH NEXT ? ROWS ONLY |
| 自增 | AUTO_INCREMENT | IDENTITY(1,1) |
| 当前时间 | NOW() | GETDATE() |
| 判断空 | IFNULL(a, 0) | ISNULL(a, 0) |
| 限制返回行数 | LIMIT 10 | SELECT TOP 10 ... |
| 字符串长度 | LENGTH() | LEN() |
| 正则匹配 | REGEXP | 不原生支持,用LIKE+通配符 |
这张表格直接背下来,可以避免很多“SQL看起来一样但报错”的问题。
8.3 二次开发时的扩展方向建议
如果是用来交毕设,不建议只停留在原封不动跑通,加个自定义功能会让答辩覆盖点更多:
- 新增“在线阅读”功能:TXT文件直接转纯文本显示,PDF可用PDF.js插件嵌入页面。
- 增加“积分系统”:注册送积分,下载消耗积分,普通用户和管理员的积分策略分开,这样用户表需要加
points字段。 - 增加“评论系统”:在
book详情页下加评论列表,需要新建comment表,含book_id、user_id、content、create_time。 - 增加“数据导出”:管理员后台可以一键导出下载记录为Excel,这也是企业里非常常见的需求。
我个人在实际项目带练中有一个比较深的体会:电子书下载系统的核心难点,不在于技术多高深,而在于“把一连串简单的事情做扎实”。Servlet怎么接收参数、文件流怎么不内存溢出、SQL怎么在方言差异里正确执行、部署时端口协议怎么配——这些单独拎出来都是基础题,但拼在一起就成了一个完整项目的骨架。只要按我上面的思路把每个环节都踩一遍,面试官或答辩老师问你“这个下载接口遇到并发怎么办”“数据库索引怎么加”“Tomcat启动失败怎么排查”,你都能接得上话。
如果你打算把这个项目运行起来,建议按这样的顺序来:先建库跑脚本,再改连接配置,再部署到Tomcat,最后跑通下载流程。等主流程通了,再考虑加功能、换界面。碰到问题也别慌,把控制台报错复制下来搜关键词,大概率还是上面那几类。真遇到跑不通,多半是环境变量、驱动包、协议配置这三件套里有一样没对齐,一项项查就行。
本文还有配套的精品资源,点击获取