1. 项目全景与需求拆解
1.1 为什么把“综合实验”当作一次真正的前后端贯通练习
大概每个计算机专业的学生都绕不开“综合实验”这门课,但多数人做这个项目时都是抱着“交差”的心态——课程表上写着“课程设计”,验收时老师看看界面能跑、增删改查没大毛病,就给个分数完事。我当年也是这样糊弄过来的,直到后来真正进入一线做业务系统,才发现当年那个被当成“作业”的综合实验,其实已经把Web开发里最核心的链路都串了一遍:客户端发起请求、服务端接收处理、数据库读写、结果回显到页面。这条链路无论换什么框架、什么语言,本质都不变。
我建议所有正在做综合实验的读者,把这次作业当成一次“模拟入职”来对待。不是老师要什么就给什么,而是主动去想:如果这是一套要给图书馆管理员真正使用的系统,管理员会怎么操作?图书数据从哪里来?借阅记录会不会越来越多导致查询变慢?这些问题想清楚了,哪怕最终交付的还是一份课程设计,你做出来的东西也会比同龄人高出一个量级——更重要的是,这套思考方式会跟着你走进真正的职场。
今天分享的这个综合实验项目,我选了一个非常传统但极其经典的场景:图书管理系统。选它的原因是图书管理涉及完整的业务闭环:图书入库、读者注册、借书、还书、超期计算、统计查询。麻雀虽小,但五脏俱全,而且每个人对图书馆的业务都不陌生,不用额外解释行业背景,可以集中精力把技术细节讲透。
阅读这篇文章之前,希望你至少具备以下任意一条基础:会一点Java基础语法、会用MySQL进行简单的增删改查、或者哪怕只是跟着教程跑通过一个Servlet项目。超出这个范围的部分我会尽量用通俗的方式解释清楚,即使你目前还在入门阶段,也可以把这篇文章当作一份“手把手踩坑实录”来读。
1.2 需求分析阶段最容易踩的坑
很多同学动手写代码之前不做需求分析,上来就建表,结果写到一半发现字段不够、逻辑冲突、页面不知道该跳转到哪里。这个习惯非常危险,需求分析不是走过场,而是帮你把模糊的“做一个图书管理系统”变成清晰的“需要哪些页面、哪些按钮、哪些数据”。
我当时做需求分析用了一招:把自己当成图书馆的兼职管理员,脑海里完整走一遍一天的工作流程。早上开门,有读者进来要借书,我需要先确认读者有没有逾期未还的书,有的话要提醒他先还书;然后问他要借哪本书,查询库存是否充足;如果库存为0,要告诉他这本书已被借走,或者帮他预约。读者还书时,我要检查书籍有没有损坏、是否超期,超期要计算罚款金额。管理员还要定期盘点库存、登记新书、下架旧书。这一套流程走下来,系统的功能模块就自然而然地浮出水面了。
基于这个思路,我把系统划分为四个核心模块:图书管理、读者管理、借阅管理和统计分析。图书管理负责图书信息的录入、修改、删除和查询;读者管理负责读者档案的建立和维护;借阅管理处理借书/还书/续借/挂失等核心业务;统计分析则提供图书借阅排行、读者借阅排行、库存预警等辅助功能。
这里要说一个很多课程设计会忽略的点:权限区分。至少要把系统用户分为管理员和普通读者两类角色,不同角色登录后看到的功能菜单不一样,操作的权限也不一样。如果所有用户登录后都能删书、改库存,那这个系统在实际业务中是没有人敢用的。综合实验虽然不需要做到像真实系统那么严谨,但一定要有这个意识。
注意:需求分析阶段多花一天时间,代码阶段能少改三天的bug。别嫌麻烦,这是整个项目性价比最高的一步。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心技术选型与设计思路
2.1 技术栈选择的底层逻辑
技术选型是综合实验里另一个容易“翻车”的点。很多同学喜欢一上来就上Spring Boot + MyBatis Plus + Vue全家桶,理由是人人都用、教程多。这个想法没错,但要看你自身的基础和实验的时间要求。如果距离提交只剩两周,你连Spring的IOC容器都还没搞明白,贸然上全家桶只会让你在配置环境的时间比写代码的时间还长。
我做这个项目选了Java Web最经典的组合:Servlet + JSP + MySQL + Tomcat。选择这套组合有三个理由。第一,这套技术栈足够“轻”——不需要复杂的配置,核心代码量适中,适合在有限的时间内从零到一完整地跑通一遍;第二,它能让你看到Web开发最底层的东西——HTTP请求是怎么被接收的、参数是怎么解析的、页面是怎么渲染的,这些底层原理在Spring里被封装得很深,早期看不到反而容易懵;第三,学校综合实验的验收环境差异化很大,有的机器上Java版本五花八门,用Servlet这套最稳。
如果你已经能把SSM或Spring Boot跑得比较熟了,当然可以用更趁手的工具。但我始终认为,综合实验的核心目标不是炫技,而是把一条链路完整地走通。用一个你完全能够驾驭的技术栈,把业务逻辑做扎实,远比用一堆高深框架最后连上线都上不了要强得多。
前端部分,我采用的是JSP + Bootstrap的组合。Bootstrap能帮你快速做出不算难看的管理界面,而且它对前端基础要求很低,只需要会套用现成的CSS类名就行。不建议在这个阶段花费大量时间研究Vue、React这些框架,那是下一阶段的事情,现在的首要任务是保证业务逻辑能跑通、页面能用。
2.2 数据库表设计:五个表还是三个表?
表设计是综合实验里最能体现“内功”的地方。我见过不少同学把整个系统做成一张大表,字段堆了三四十个,耦合严重,稍微改一个需求就要重建表结构。正确的做法是按业务模块拆分,让每个表只负责一类实体的存储。
这个图书管理系统我设计了四张表:读者表、图书表、借阅表和系统用户表。为什么不是三张也不是五张?原因是借阅关系是读者与图书之间的多对多联系,必须用一张独立的表来存储。如果没有这张中间表,你无法回答“张三过去一年一共借过哪些书”这类核心业务问题。
我列出核心表结构的概览,具体字段在实操部分会详细展示:
- 系统用户表(user): id, username, password, role, create_time
- 读者表(reader): id, name, phone, email, status, create_time
- 图书表(book): id, isbn, name, author, publisher, category, stock, total, create_time
- 借阅表(borrow): id, reader_id, book_id, borrow_time, due_time, return_time, status, fine
关于外键,我想多说一句。很多教材强调用物理外键保证数据完整性,但在实际项目里,物理外键会导致后期数据维护非常痛苦——删除一本书时外键约束会拦住你,批量导入数据时外键检查会拖慢速度。我更推荐“逻辑外键”,即两张表之间保持引用关系,但不建立物理约束,所有关联校验都在代码层完成。综合实验的数据量不大,物理外键的负面影响不明显,但养成逻辑外键的设计习惯,对你未来的工作更有帮助。
另一个容易被忽视的细节是索引。给借阅表的reader_id、book_id分别加上普通索引,图书表给name、isbn加上索引,这些操作在数据量小的时候感觉不到差别,但一旦数据量上来,没有索引的查询全表扫描会非常慢。哪怕实验数据只有几十条,也应该从第一天就养成创建索引的习惯。
3. 核心编码实现与实操过程
3.1 搭建项目骨架:从空目录到可运行系统
项目骨架这一节,我按实操顺序来讲,每一步做完你都能看到可视化的成果,不会出现“代码写了一堆但不知道对不对”的迷茫感。
第一步,建立项目的目录结构。我习惯用一个Maven项目来组织代码,哪怕你用的是最原始的Eclipse Dynamic Web Project,也建议把项目结构向Maven的标准目录看齐:src/main/java放Java源码,src/main/resources放配置文件,src/main/webapp放JSP页面、CSS、JS和静态资源。
第二步,配置web.xml。如果你用的不是Servlet 3.0以上的容器,web.xml就是项目的“总开关”,负责配置Servlet映射、过滤器和监听器。我的项目里至少需要三样东西:一个字符编码过滤器、一个登录权限过滤器、以及所有Servlet的URL映射。Tomcat 8以后的默认编码是UTF-8,但请求参数传递时如果不设置字符编码,中文依然会出现乱码。
第三步,编写数据库连接工具类。我早期写课程设计时,每个Servlet里都写一遍Class.forName、DriverManager.getConnection,代码冗余不说,改一次数据库密码要改十几个文件。正确做法是写一个DBUtil工具类,数据库连接信息放在jdbc.properties配置文件里,用static代码块完成驱动加载。这样后期即使要切换数据库,也只需修改配置文件。
java复制public class DBUtil {
private static String url;
private static String username;
private static String password;
static {
try {
Properties props = new Properties();
props.load(DBUtil.class.getClassLoader().getResourceAsStream("jdbc.properties"));
Class.forName(props.getProperty("jdbc.driver"));
url = props.getProperty("jdbc.url");
username = props.getProperty("jdbc.username");
password = props.getProperty("jdbc.password");
} catch (Exception e) {
e.printStackTrace();
}
}
public static Connection getConnection() throws SQLException {
return DriverManager.getConnection(url, username, password);
}
}
这里要特别强调一个细节:很多同学在static代码块里漏了Class.forName这一行。早期JDBC版本必须显式加载驱动类,MySQL Connector/J 8.0以后虽然支持SPI自动注册,但为了兼容性和安全性,保留这行代码是个不会错的选择。关于连接管理,我的建议是最简单的方式就开始,能跑通后再考虑连接池,避免一上来引入太多概念把自己绕晕。
3.2 登录模块:过滤器、会话管理与密码安全
登录模块是整个系统的第一道门,也是综合实验里老师必看的部分,所以我会把登录模块的每个环节都讲透。
登录页面的表单提交到LoginServlet之后,Servlet从请求中拿到username和password,去数据库比对。这里要特别注意SQL注入问题。最简单粗暴的错误写法是用字符串拼接的方式:
sql复制SELECT * FROM user WHERE username = 'admin' AND password = '123456'
试想如果用户在密码框里输入 ' OR '1'='1,拼出来的SQL就变成了:
sql复制SELECT * FROM user WHERE username = 'admin' AND password = '' OR '1'='1'
这个条件永远为真,攻击者不需要密码就能获得管理员权限。防止SQL注入的正确做法是使用PreparedStatement预编译:
java复制String sql = "SELECT * FROM user WHERE username = ? AND password = ?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, username);
ps.setString(2, password);
ResultSet rs = ps.executeQuery();
PreparedStatement会把参数当作普通字符串处理,而不是当作SQL语句的一部分,这样即使输入了恶意字符串也不会对原有语句造成影响。所以从第一行代码开始就养成用PreparedStatement的习惯,这个习惯越早越好。
登录成功之后,一定要在Session中保存当前用户的标识信息(例如用户ID、用户名、角色),后续每个需要权限判断的请求都通过Session来判断用户是否已登录。Session的配置在Tomcat的web.xml中可以设置超时时间,我建议设置为30分钟,太短会影响使用体验,太长则增加安全风险。
密码存储的问题,我多说一句。很多综合实验里密码都是明文存储的,如果时间允许,建议至少用MD5加盐的方式存储密码。加盐的意思是给原始密码拼接一段随机字符串,再做哈希运算,这样两个用户即使密码相同,存储的密文也不一样。这个技术在现在的企业项目里已经是标配了,早点掌握在简历上也是一个加分项。
3.3 权限控制与过滤器链的巧妙应用
登录验证通过后,如何在每个页面生效?如果每个Servlet里都写一遍“判断Session里有没有用户”,代码会非常冗余。这里用Filter(过滤器)来解决。
我写了一个AuthFilter,它拦截所有以/admin/开头的路径。用户每次访问受保护资源时,过滤器先检查Session是否存在有效的用户信息,不存在就重定向到登录页,存在则放行。如果还要区分管理员和普通读者,就在过滤器里校验角色,角色不符则返回提示页面。
java复制public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
HttpSession session = req.getSession(false);
// 判断Session中是否存在登录用户
User loginUser = (session != null) ? (User) session.getAttribute("loginUser") : null;
if (loginUser == null) {
resp.sendRedirect(req.getContextPath() + "/login.jsp");
return;
}
// 如果需要管理员权限,校验角色
String uri = req.getRequestURI();
if (uri.contains("/admin/") && !"ADMIN".equals(loginUser.getRole())) {
resp.sendError(HttpServletResponse.SC_FORBIDDEN);
return;
}
chain.doFilter(request, response);
}
这个设计的优点在于,业务Servlet本身不需要关心用户是否登录的问题,只需要专注于处理业务逻辑,职责分离非常清晰。我实际试下来,用过滤器统一做权限管理,比在每个Servlet里都写判断代码要省心得多,而且后期如果要加“记住我”、“登录IP限制”等功能,只要在过滤器链上增加新的一环即可。
还有一个容易被忽略的问题是登录失败后的提示。很多同学喜欢用alert弹窗,体验不好;我建议在登录页放一个隐藏的错误提示区,登录失败时重定向回登录页并带上错误参数,页面读取参数后展示错误信息,用户体验立刻提升一个档次。
3.4 图书管理模块:分页查询与查询条件组合
图书管理模块是综合实验的核心内容之一,也是老师重点考察的CRUD能力。图书管理页面必须有:搜索功能、分页列表、新增/编辑弹窗、删除确认。我先讲分页,因为分页是最容易踩坑的地方。
数据库分页查询的SQL使用LIMIT语句,格式是LIMIT offset, row_count,其中offset等于(currentPage - 1) * pageSize,row_count就是每页的记录数。这部分逻辑可以封装成一个PageUtil工具类,接收当前页码和每页条数,返回分页后的数据和总页数。
java复制public PageResult<Book> listBooks(String keyword, String category, int pageNum, int pageSize) {
StringBuilder sql = new StringBuilder("SELECT * FROM book WHERE 1=1");
List<Object> params = new ArrayList<>();
// 动态拼接条件,注意用参数占位符,不要直接拼字符串
if (keyword != null && !keyword.trim().isEmpty()) {
sql.append(" AND (name LIKE ? OR author LIKE ? OR isbn LIKE ?)");
String likePattern = "%" + keyword.trim() + "%";
params.add(likePattern);
params.add(likePattern);
params.add(likePattern);
}
if (category != null && !category.isEmpty()) {
sql.append(" AND category = ?");
params.add(category);
}
// 先查询总数
String countSql = sql.toString().replace("*", "COUNT(*)");
// 再查分页数据
sql.append(" ORDER BY id DESC LIMIT ?, ?");
params.add((pageNum - 1) * pageSize);
params.add(pageSize);
}
这段代码里有个细节值得注意:查询总数的SQL不是再写一遍完整逻辑,而是在原来SQL基础上把*替换成COUNT(*),这样能保持查询条件完全一致。很多同学分页时总数和列表的查询条件写成了两份代码,结果改了列表的条件忘了改总数的,分页数据就永远对不上。
图书的新增和编辑功能,可以用同一个Servlet来处理。请求参数里带bookId就走更新逻辑,不带就走新增逻辑,这种方法在业务不复杂时非常高效。要注意的是,图书的库存字段stock必须小于等于总册数total,新增和编辑时都要做校验,这个校验逻辑可以写在Servlet里,但更规范的做法是同时在前端和后端各校验一次。前端的校验能及时反馈用户,后端的校验才是真正的安全防线,两者缺一不可。
3.5 借阅管理:事务、并发与超期计算
借阅模块是整个系统业务逻辑最复杂的地方,也是最容易出问题的地方。借书的大致流程是:读者身份验证→图书库存检查→扣减库存→新增借阅记录。还书的流程是:检查是否有超期→计算罚款金额(如有)→更新借阅记录状态→回补库存。
我先说借书的并发问题。什么叫并发?就是两个管理员同时操作,或者两个读者同时在线借同一本书。如果系统不做控制,库存只有1本,A和B同时点击“借书”按钮,两个请求都检查到了库存≥1,都执行了扣减操作,库存就变成了-1。数据库层面的解决方案是在扣减库存时加上库存充足的条件:
java复制UPDATE book SET stock = stock - 1 WHERE id = ? AND stock > 0
这句SQL把“检查库存”和“扣减库存”合并成一个原子操作,数据库会保证这条UPDATE语句的串行执行,即使并发请求同时到达,也只有一个请求能把stock成功减到0,另一个请求更新行数为0(表示库存不足),代码层据此判断借书失败,完美解决超卖问题。
借书和还书涉及多个SQL操作,比如借书给读者时既要更新图书表的库存,又要插入借阅记录表。这两个操作必须保证同时成功或同时失败,否则就会出现“库存扣了但借阅记录没生成”的数据不一致问题。Java中可以通过Connection来控制事务,核心代码段如下:
java复制Connection conn = null;
try {
conn = DBUtil.getConnection();
conn.setAutoCommit(false); // 关闭自动提交
// 执行图书库存扣减
// 执行借阅记录插入
conn.commit(); // 全部成功则提交
} catch (SQLException e) {
if (conn != null) {
conn.rollback(); // 有异常则回滚
}
throw e;
} finally {
if (conn != null) {
conn.setAutoCommit(true);
conn.close();
}
}
很多刚学的人不理解事务的重要性,觉得“这不就多两行代码的事吗”。我可以负责任地说,凡是不做事务处理的借阅模块,早晚会在数据量变大时暴露脏数据问题。综合实验虽然数据量小,但养成这个编码习惯会让你在进入企业项目后少挨很多骂。
还书时的超期计算,我使用的规则是:系统设置一个最大借阅天数,默认30天;计算应还日期 = 借书日期 + 30天;如果实际还书日期晚于应还日期,按照每天0.1元计算罚款,罚款金额取min(逾期天数, 0)后再乘以日罚款金额。
java复制long daysLate = (returnDate.getTime() - dueDate.getTime()) / (24 * 60 * 60 * 1000);
if (daysLate > 0) {
double fine = daysLate * DAILY_FINE;
}
这里有个小坑:日期计算时如果用(returnDate - dueDate) / 86400000直接算天数,遇到夏令时、时区问题时会偏差。稳妥的方法是使用java.time包里的LocalDate和ChronoUnit.DAYS.between(),现代的API处理日期更干净,不会踩老API的坑。
3.6 读者管理:一张表搞定用户与角色
读者管理的核心需求是“能查、能加、能改、能删”。数据库设计时,我把系统用户表(有登录账号密码的人)和读者表(有借书行为的人)分开存储。为什么分开?系统的管理员也需要借书,管理员本身也应当是一个读者,如果这两类角色混在一张表里,字段职责会混淆。
读者表的status字段表示读者状态:正常、挂失、停用。状态为“停用”的读者不能借书,这需要在借书Servlet里做判断。挂失功能虽然不复杂,但我建议做上,因为它是综合实验答辩时老师特别喜欢问的一个延伸点:“如果读者的借书证丢了怎么办?”有挂失功能,说明你确实思考过实际业务场景,这比代码写得漂亮更能给老师留下好印象。
新增读者时,需要注意手机号和邮箱的格式校验。手机号用正则匹配11位数字,邮箱用简单的^\w+@\w+\.\w+$就可以。这些校验逻辑可以放前端用JavaScript做,但后端同样要抄一遍,原理还是那句话:前端校验是用户体验,后端校验才是安全保证。
3.7 统计分析模块:用SQL说话
统计分析功能是整个系统的“彩蛋”。虽然课程设计的验收标准不一定要求统计图表,但加上这个模块,系统完成度会有肉眼可见的提升。我做了一个“借阅排行榜”,展示TOP10的被借次数最多的图书;做了一个“读者借阅排行”,展示借书最活跃的读者;还做了“库存预警”,列出库存量低于设定阈值的图书。
统计模块的核心其实是SQL,页面展示反而是最简单的。比如借阅排行榜的核心SQL:
sql复制SELECT b.id, b.name, b.author, COUNT(br.id) AS borrow_count
FROM book b
LEFT JOIN borrow br ON b.id = br.book_id
GROUP BY b.id, b.name, b.author
ORDER BY borrow_count DESC
LIMIT 10
这里用LEFT JOIN而不是INNER JOIN很关键。LEFT JOIN能把没有被借过的图书也查出来(借阅次数为0),INNER JOIN则会过滤掉这些图书。排行榜里没有借阅次数的图书,不应该被排除,它应该排在最后面,而不是不显示。
提示:数据可视化不是必须的,但如果你有时间,可以用ECharts画一个简单的柱状图,效果会非常加分。ECharts的使用非常简单,引入JS文件、配置一个option对象、调用setOption方法即可完成图表渲染,一小时就能学会。
4. 测试排查与经验心得
4.1 功能测试清单:用表格自查
综合实验最尴尬的场景是答辩演示时才第一次完整走流程,结果当场翻车。为了避免这种局面,我强烈建议提前把所有功能按照一份清单完整测一遍。下面把我用的测试清单分享出来:
| 测试模块 | 测试卡片 | 预期结果 |
|---|---|---|
| 登录 | 正确账号密码登录 | 跳转到首页 |
| 登录 | 错误密码登录 | 提示“用户名或密码错误” |
| 登录 | 未登录直接访问后台URL | 重定向到登录页 |
| 图书管理 | 新增图书后列表出现 | 数据正常展示 |
| 图书管理 | 搜索书名关键词 | 只显示匹配结果 |
| 图书管理 | 删除有借阅记录的图书 | 要么阻止,要么删除借阅记录 |
| 借阅管理 | 借库存为0的书 | 提示库存不足 |
| 借阅管理 | 同一本书连续借两次 | 第二次提示可借数量不足 |
| 还书管理 | 超期还书 | 自动计算并展示罚款金额 |
| 读者管理 | 新增手机号格式错误的读者 | 前端拦截并提示 |
| 统计分析 | 查看借阅排行榜 | 图表正常展示TOP10数据 |
测试时建议换一个思路:不要光测“正确路径”,更要测“错误操作”。比如故意输入非法字符、故意点击保存两次、故意在浏览器里直接输入后台URL。很多系统演示时翻车都是因为只测了正常路径,没测异常路径。
4.2 数据库连接池配置:从总断连到稳定运行
如果你把项目部署到服务器上长期运行,或者一口气测试很多页面,可能会遇到一个典型问题:Tomcat运行一会儿之后,突然报Connection is closed或Communications link failure。我当时排查了很久,最后发现根因是数据库连接没有被正确关闭,或者连接空闲时间太长被MySQL服务端断开了。
最彻底的解决方案是引入数据库连接池。连接池的思路相当于建立一批数据库连接常驻内存,需要用的时候从池里取一个,用完再归还,而不是每一次操作都重新建立连接。连接池有多种选择,比如DBCP、C3P0、Druid,我推荐Druid,因为它在监控和防SQL注入方面有中文文档,适合初学者使用。
Druid连接池的核心配置大概如下:
properties复制jdbc.driverClassName=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/library?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
jdbc.username=root
jdbc.password=123456
jdbc.initialSize=5
jdbc.maxActive=20
jdbc.minIdle=5
jdbc.maxWait=60000
jdbc.validationQuery=SELECT 1
jdbc.testWhileIdle=true
jdbc.timeBetweenEvictionRunsMillis=60000
配置文件里几个关键参数说明一下:initialSize表示启动时初始化的连接数,maxActive是最大连接数,maxWait是获取连接时的最大等待毫秒数,超过这个时间还拿不到连接就抛异常。validationQuery存在的意义是让连接池定期用SELECT 1检查连接是否健康,如果连接已经被MySQL断开,连接池会自动把它移除并新建一个连接,这样就能解决“跑一段时间后突然连接失败”的问题。
4.3 中文乱码的根源:编码链路上每一个环节都要保持UTF-8
中文乱码几乎是每个综合实验都会遇到的问题。解决乱码不能只靠一处的设置,而是要从浏览器到服务器、再到数据库整条链路都保持一致。我把需要检查的地方列成一个检查链,任何一个环节出问题都会出现乱码。
第一环:数据库表编码。建库时必须指定字符集:CREATE DATABASE library DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci。如果建库时忘了指定,后面可以执行ALTER DATABASE library CHARACTER SET utf8mb4来修正。第二环:JDBC连接URL中要带characterEncoding=utf8参数。第三环:JSP页面顶部要声明<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>。第四环:Servlet接收POST请求参数前,一定要执行request.setCharacterEncoding("UTF-8"),这个操作必须在读取第一个参数之前完成,否则不会生效。
如果你用的是Tomcat 8或更高版本,GET请求的编码默认已经是UTF-8了,可以不用额外处理。但POST请求的编码就必须要靠第一行代码来保证。我见过很多同学把所有地方都设置了一遍,结果忘了request.setCharacterEncoding("UTF-8")这一行,花了半天时间排查乱码问题。把这个检查链记住,以后再遇到乱码问题十分钟内就能定位。
4.4 答辩演示时的三个加分技巧
综合实验最终还是要过答辩这一关。我这里分享几个实践中验证过的小技巧,能让你的演示效果远超过同级同学。
第一,演示之前把系统恢复到干净状态。删掉测试时产生的脏数据,把库存调整成合理的数值,准备几个名称有辨识度的演示数据。如果打开页面满屏都是“测试1”、“测试2”,给老师的观感会差很多。
第二,演示时一定要准备一条“错误路径”的展示。比如故意输入一个错误的密码,展示系统给出友好提示;或者故意借一本库存为0的书,展示系统的库存校验。演示错误路径并不会让老师觉得你能力不行,反而证明你确实思考过异常情况的处理。
第三,提前准备好一个“亮点清单”。比如你在哪个模块用了PreparedStatement防SQL注入、在哪里用了事务保证数据一致性、在哪里用了连接池优化性能。答辩时老师问“你这个项目有什么亮点”,你直接把这个清单流利地说出来,老师对项目的好感度会大幅提升。这比临时临场想答案要从容得多。
5. 项目扩展方向与常见陷阱补全
5.1 三个值得加入的扩展功能
综合实验做完之后,如果还有时间或者想挑战一下自己,有以下几个扩展方向,选哪个都不会错。第一个是图书预约功能。当图书库存为0时,读者可以提交预约,等有人还书时系统自动通知预约者。预约功能会涉及预约状态的管理和通知机制,业务复杂度增加得恰到好处,是一个很好的进阶练习。
第二个是Redis缓存热点数据。把图书的查询结果缓存到Redis中,减少对数据库的直接访问。对于一个图书管理系统来说,图书信息是读多写少的数据,非常适合做缓存。这也能让你提前接触到企业中“缓存与数据库一致性”的问题——图书信息更新时如何让缓存失效,数据更新了缓存没刷新的情况怎么处理。
第三个是仪表盘图表可视化。把统计分析模块从简单的表格升级为ECharts图表,增加借阅趋势折线图、分类占比饼图等。叠加一个Filter来统计接口响应时间,做成一个简单的性能监控面板,既能锻炼接口分析能力,又能在答辩中显得项目维度更高。
5.2 必须绕开的五个陷阱
第一个陷阱是删除功能无脑做物理删除。图书表里如果有借阅记录关联,直接执行DELETE会破坏数据完整性。实际业务中更常见的做法是逻辑删除,即增加一个del_flag字段,删除操作只是把该字段置为1,查询时统一加上WHERE del_flag = 0。这个设计能保留所有历史数据,也是企业开发中的标配。
第二个陷阱是不做时间字段的默认值处理。新建图书或者读者时,create_time字段如果在INSERT语句里没有赋值,数据库会报错或者写入NULL。建议在表结构设计时直接让create_time默认值为CURRENT_TIMESTAMP,避免每次插入都要手动设置日期。
第三个陷阱是JSP页面里写Java代码逻辑。如果JSP页面里塞满了<% %>和<%= %,页面会极其难维护。Java代码应该全部封装在Servlet和Service层,JSP里只保留HTML标签和JSTL标签库的渲染逻辑。这不仅是为了代码整洁,也是面试官问“MVC模式”时你能不能讲明白的关键。
第四个陷阱是忽略防御式编程。所有从前端传来的参数都默认是不可信的。对方可能传一个负数页码给你,可能传一个超长的字符串给你,甚至可能直接构造一个请求绕过前端校验。后端必须对参数进行二次校验和异常捕获,防止服务器直接抛出500错误页。
第五个陷阱是项目收尾时不写README。README看起来和工作无关,但如果项目里有清晰的部署说明(环境要求、数据库初始化脚本、默认账号密码、功能清单),不仅方便老师复现你的项目,也是在养成一个职业习惯。我建议README至少包含以下四部分:项目简介、技术栈、运行步骤、账号信息。
5.3 这份综合实验的经验如何迁移到真实项目
做一个综合实验最大的价值,从来不只是那一个“图书管理系统”本身,而是你在这个过程里建立起的工程化思维。从需求分析时把业务走一遍,到数据库设计时把实体关系理清,到编码时用PreparedStatement防注入、用事务保护数据一致性,再到测试时列清单逐项核对——这些方法论在你未来做任何系统时都会复用。
等到你真正开始接触Spring Boot和微服务架构时,你会发现底层的很多概念在这里就已经接触过:Servlet对应Spring MVC的Controller、连接池对应数据源配置、Filter对应Interceptor、JSP对应模板引擎。当初在综合实验里踩的每个坑,都会成为你理解新框架的“锚点”。所以不要小看这门课,它看起来只是一门“大作业”,实际上是把你从“会写几行语法”推向“能完成一个完整需求闭环”的第一道练习场。
我自己后来带新人时经常说:我不在乎你用了多么时髦的技术栈,我只在乎你在遇到问题时有没有一套排查思路,在写代码时有没有想清楚这段代码运行之后会影响哪些数据。如果一个新人能在我面前把他做的一个课程设计的表结构讲清楚、说明白为什么这么设计,我就敢把真实的业务模块交给他。这套标准,同样适用于正在读这篇文章的你。
