简介:这是一套围绕大学图书馆业务设计的Java Web管理系统,采用JSP、Spring、JDBC与Servlet技术组合,适合Java Web学习者、课程设计学生或需要搭建后台管理系统的开发者参考。项目清晰划分学生端与管理端:学生端覆盖图书查询、借阅预约、个人借阅记录查看和图书挂失;管理端实现学生信息管理、图书维护、借还书操作,并提供超级管理员与普通管理员两级权限,完整呈现了用户权限控制的应用方式。资源以RAR压缩包形式提供,大小约2.8MB,便于下载解压、导入IDE查看源码和二次开发。目前已获得127人学习,对于理解传统Java Web分层架构、JDBC数据库操作以及Spring与Servlet的整合具有实际参考价值。 慕仁大学图书馆管理系统这套东西,说实话在Java Web领域算是很经典的组合了。我当年在学校做课程设计的时候就折腾过类似的架构,JSP+Spring+JDBC+Servlet这套技术栈,放在今天看虽然有点“复古”,但它恰恰是理解Java Web底层原理最好的教材。这篇文章我就以这套图书管理系统为线索,把整个项目从头到尾拆开聊一聊,包括架构选型、核心模块设计、实操过程中的关键代码和踩坑记录,希望对正在做类似项目的同学有帮助。
1. 项目整体设计与技术选型思路
先说结论:这套系统的技术栈选型非常“教科书”,适合做毕业设计、课程设计,也适合刚入门Java Web的开发者拿来练手。它的核心价值不在于技术有多新,而在于每一层技术各司其职,能让你清清楚楚地看到一次HTTP请求从前端页面到数据库再返回前端展示的完整链路。
1.1 为什么是JSP+Servlet+Spring+JDBC这个组合
很多刚接触Java Web的同学会问:现在都Spring Boot满天飞了,为什么还要学这套老技术栈?我个人的观点是,Spring Boot太“傻瓜化”了,很多东西被自动配置藏了起来,你根本不知道底层发生了什么。而JSP+Servlet+Spring+JDBC这套组合,每一步都是显式的,你能看到请求怎么进Servlet,怎么被Spring容器管理,怎么通过JDBC操作数据库,这对建立完整的Web开发认知特别重要。
具体到慕仁大学图书馆管理系统这个项目,选这套技术栈还有几个现实原因:
- 学校课程体系通常以JSP/Servlet为起点,这套系统正好衔接课程内容。
- JSP在页面展示上确实方便,尤其是图书馆管理这种需要大量表单和列表页面的场景。
- 不引入MyBatis或Hibernate,直接用JDBC,反而是好事——你能把SQL牢牢握在自己手里,对数据库操作的理解更深刻。
1.2 系统的核心需求拆解
做项目最忌讳的就是一上来就写代码。我拿到“慕仁大学图书馆管理系统”这个题目后,第一步是先拆解业务需求。图书馆管理系统归根结底就三类核心角色和一组支撑功能。
第一类是普通学生用户,他们要能检索图书、查看个人借阅记录、在线预约借书、查看图书详情。第二类是图书馆管理员,他们要负责图书信息的增删改查、处理借还书操作、管理学生账号。第三类可以理解为系统超级管理员,负责整体数据统计和管理员账号维护。
支撑功能则包括:用户登录认证(区分角色)、图书分类管理、借阅超期自动计算罚款等。这些功能拆完后,再去设计数据库表结构就顺理成章了。
1.3 数据库表结构的预先设计
我在动手写任何代码前,先设计了五张核心表:
- t_user(用户表):包含用户ID、用户名、密码、角色(student/admin)、学号、院系、手机号等字段。
- t_book(图书表):图书ID、ISBN、书名、作者、出版社、分类ID、库存总量、当前可借数量、存放位置。
- t_category(分类表):分类ID、分类名称、父分类ID。
- t_borrow(借阅表):借阅ID、用户ID、图书ID、借书时间、应还时间、实际归还时间、状态(借阅中/已归还/超期)。
- t_reserve(预约表):预约ID、用户ID、图书ID、预约时间、状态(等待中/已通知/已取消)。
这五张表的关系一定要理清:用户和图书是多对多关系,通过借阅表关联;图书和分类是多对一关系;预约表是借阅的补充机制,用于处理热门图书已被借光的情况。表结构设计好了,后面的DAO层代码写起来就只是体力活了。
2. Spring容器与JDBC持久层核心解析
这套系统里,Spring主要扮演两个角色:IOC容器管理和事务管理。JDBC则负责最底层的数据库操作。这两块是项目的“地基”,地基不牢,上面全白搭。
2.1 Spring IOC在这套系统中的应用
传统方式下,Service层要使用DAO层对象,得自己new一个出来,硬编码在代码里,后期想换实现类得改源码。Spring IOC的做法是把对象的创建和依赖关系交给容器管理,在XML里配置Bean,让容器负责注入。
我当时用XML方式配置了数据源和各个DAO、Service的Bean:
<!-- 数据源配置 --> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/library_db?useSSL=false&characterEncoding=utf8"/> <property name="username" value="root"/> <property name="password" value="123456"/> </bean> <!-- JdbcTemplate配置 --> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <property name="dataSource" ref="dataSource"/> </bean> <!-- DAO层配置 --> <bean id="bookDao" class="com.muren.dao.BookDao"> <property name="jdbcTemplate" ref="jdbcTemplate"/> </bean> <!-- Service层配置 --> <bean id="bookService" class="com.muren.service.BookService"> <property name="bookDao" ref="bookDao"/> </bean>这种XML配置看起来很啰嗦,但它把对象之间的依赖关系展现得一清二楚。第6行的连接池URL里我特意加上了useSSL=false&characterEncoding=utf8参数,这一点非常关键,少了它你大概率会遇到中文乱码或者SSL连接报错,后面排查起来极其崩溃。
2.2 Servlet在整个请求链路中的位置
很多初学者搞不清Servlet和JSP的关系。简单说,Servlet是Java服务端的处理程序,负责接收HTTP请求、调用业务逻辑、返回响应;JSP本质上是一个被容器编译成Servlet的模板文件,负责展示动态页面数据。
在这套系统里,我使用了Servlet 3.0的注解方式配置,不再需要在web.xml里写一长串映射:
@WebServlet("/book/list") public class BookListServlet extends HttpServlet { private BookService bookService; @Override public void init() throws ServletException { // 从Spring容器中获取Service ApplicationContext context = WebApplicationContextUtils .getWebApplicationContext(getServletContext()); bookService = context.getBean(BookService.class); } @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String keyword = req.getParameter("keyword"); List<Book> books = bookService.searchBooks(keyword); req.setAttribute("bookList", books); req.getRequestDispatcher("/WEB-INF/jsp/book_list.jsp").forward(req, resp); } }这段代码展示了一个完整的请求处理流程:第4行通过@WebServlet注解把URL请求映射到这个Servlet;第8到10行从Spring容器获取Service实例,这种方式保证了Servlet和Spring容器之间的关联;随后的doGet方法接收参数、调用Service层处理业务、把结果放到request域中、最后转发到JSP页面渲染。
这里有个细节要强调:Servlet从Spring容器拿Bean时,必须用WebApplicationContextUtils.getWebApplicationContext()获取WebApplicationContext,直接new ClassPathXmlApplicationContext重新加载配置是错误示范,会造成容器重复创建和数据源重复初始化,大量浪费资源。
2.3 JDBC操作与SQL参数处理
DAO层我选择了Spring的JdbcTemplate来简化JDBC操作。JdbcTemplate帮我们处理了Connection获取、Statement创建、ResultSet遍历和异常转换等模板代码,让我们只需要关注SQL本身:
@Repository public class BookDao { private JdbcTemplate jdbcTemplate; public List<Book> findByKeyword(String keyword) { String sql = "SELECT * FROM t_book WHERE book_name LIKE ? OR author LIKE ?"; return jdbcTemplate.query(sql, new Object[]{"%" + keyword + "%", "%" + keyword + "%"}, (rs, rowNum) -> { Book book = new Book(); book.setId(rs.getInt("id")); book.setBookName(rs.getString("book_name")); book.setAuthor(rs.getString("author")); book.setPublisher(rs.getString("publisher")); book.setTotal(rs.getInt("total")); book.setAvailable(rs.getInt("available")); return book; }); } public int updateAvailable(int bookId, int delta) { String sql = "UPDATE t_book SET available = available + ? WHERE id = ? AND available + ? >= 0"; return jdbcTemplate.update(sql, delta, bookId, delta); } }这段代码有两点值得说道:第6行和第8行使用?占位符,避免SQL注入风险,这是从第一天写JDBC就要养成的习惯,千万别图省事去拼接字符串;第14行的库存更新SQL加了一个AND available + ? >= 0条件,这是为了防止库存扣成负数,用数据库层面的条件保证业务数据的正确性。
updateAvailable的return值也很重要——它返回受影响的行数。如果返回0,说明没有更新成功,通常意味着库存不足,Service层就可以据此抛出异常或提示用户。这种先判断再更新的事务控制方式,在高并发借书场景下很实用,能避免库存被重复抢占。
3. 核心功能模块的完整实现
系统里最核心的功能就是借书和还书。这两个流程涉及多张表的联动更新,最能体现系统设计的好坏。
3.1 借书流程的实现细节
借书并非简单地往借阅表插一条记录就完事,它其实是一个事务性的操作,需要处理多层逻辑:
@Service @Transactional public class BorrowService { @Autowired private BorrowDao borrowDao; @Autowired private BookDao bookDao; @Autowired private UserDao userDao; public boolean borrowBook(int userId, int bookId) { // 校验读者是否存在 User user = userDao.findById(userId); if (user == null) { throw new IllegalArgumentException("用户不存在"); } // 检查是否有未归还的超期图书 if (borrowDao.countOverdue(userId) > 0) { throw new RuntimeException("存在超期未还图书,请先归还"); } // 扣减库存,返回影响行数判断是否成功 int rows = bookDao.updateAvailable(bookId, -1); if (rows == 0) { throw new RuntimeException("图书库存不足"); } // 插入借阅记录,设置应还时间为30天后 Date borrowTime = new Date(); Date dueTime = DateUtil.addDays(borrowTime, 30); int result = borrowDao.insert(userId, bookId, borrowTime, dueTime); if (result != 1) { throw new RuntimeException("借阅记录创建失败"); } return true; } }类上的@Transactional注解是这套系统中Spring事务管理的核心。一旦方法内任意一步抛出异常,Spring会回滚整个事务,库存扣减和借阅记录插入要么都成功,要么都失败,不会出现“库存扣了但借阅记录没建”这种数据不一致问题。
借阅流程中还要加一个判断:如果该用户存在超期未还的图书,应该禁止继续借书。这是很多同学容易漏掉的业务细节,但在真实图书馆管理里,超期不还是影响图书周转率的最大因素,所以必须加限制。
3.2 还书流程与超期罚款计算
还书流程和借书相反,要做三件事:
public boolean returnBook(int borrowId) { BorrowRecord record = borrowDao.findById(borrowId); if (record == null || record.getStatus() != 0) { throw new RuntimeException("借阅记录不存在或已归还"); } Date now = new Date(); // 计算是否超期,超期每天罚款0.1元 if (now.after(record.getDueTime())) { long days = (now.getTime() - record.getDueTime().getTime()) / (1000 * 60 * 60 * 24); double fine = days * 0.1; // 记录罚款金额到借阅记录中 borrowDao.updateFine(borrowId, fine); } // 更新借阅状态和归还时间 borrowDao.updateReturnTime(borrowId, now); // 恢复库存 bookDao.updateAvailable(record.getBookId(), 1); return true; }这里的超期判断逻辑比较基本,用的是毫秒差除以一天的时间戳。有一个容易忽视的Bug:如果now.getTime() - record.getDueTime().getTime()是负数(即还没到应还时间),整除后会是负数,所以我在now.after(record.getDueTime())这里先做了时间判断,确保只有真正超期才进入计算分支,避免出现倒扣罚款的问题。
罚款金额我按天计算并保留两位小数,可以直接存小数。每天0.1元这个标准只是示例,实际项目中罚款标准通常由图书馆规章制度决定,把标准单独抽成一个常量或配置项会更灵活。
3.3 登录认证与JSP页面的Session管理
登录模块是整个系统的门面,几乎所有功能都要求用户先登录。我实现了一个简单的登录Servlet,验证通过后把用户信息放入Session:
@WebServlet("/login") public class LoginServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username = req.getParameter("username"); String password = req.getParameter("password"); User user = userService.login(username, password); if (user != null) { HttpSession session = req.getSession(); session.setAttribute("loginUser", user); if ("admin".equals(user.getRole())) { resp.sendRedirect(req.getContextPath() + "/admin/dashboard"); } else { resp.sendRedirect(req.getContextPath() + "/book/list"); } } else { req.setAttribute("errorMsg", "用户名或密码错误"); req.getRequestDispatcher("/login.jsp").forward(req, resp); } } }第6行到第8行说明了Servlet如何获取请求参数;第9行登录成功后将用户对象放置在Session中,后续所有页面都能通过${sessionScope.loginUser.username}获取当前用户信息。第11行到第13行的角色判断和重定向很关键——管理员和学生看到的主页面完全不同,这需要在JSP页面里根据Session中的角色做差异化展示。
JSP页面中我用了JSTL标签和EL表达式来控制显示逻辑,比如导航栏这样写:
<c:choose> <c:when test="${sessionScope.loginUser.role == 'admin'}"> <li><a href="admin/book_manage.jsp">图书管理</a></li> <li><a href="admin/borrow_manage.jsp">借阅管理</a></li> </c:when> <c:otherwise> <li><a href="book/list">图书检索</a></li> <li><a href="my/borrow">我的借阅</a></li> </c:otherwise> </c:choose>EL表达式直接读取Session域中的属性值,JSTL标签根据角色输出不同的菜单,这就是JSP作为视图层最典型的用法。如果角色判断逻辑写成了== 'admin'但存进去的是字符串对象,比较的时候要注意类型匹配,否则非管理员用户也可能看到管理菜单,这个权限漏洞比功能缺陷更严重。
4. JSP页面渲染与Servlet跳转实战
很多人在JSP和Servlet跳转上容易踩坑,搞不清楚forward和redirect的区别。在这套系统中,我使用了一些安全且合理的页面流转方式。
4.1 图书列表页面的数据展示
图书列表页是我写的第一个完整JSP页面,包含条件检索、分页和状态展示。Servlet端处理分页参数,JSP端只负责循环展示。核心逻辑如下:
int pageNum = 1; int pageSize = 10; String pageNumStr = req.getParameter("pageNum"); if (pageNumStr != null && !pageNumStr.isEmpty()) { pageNum = Integer.parseInt(pageNumStr); } int total = bookService.countByKeyword(keyword); int totalPages = (total + pageSize - 1) / pageSize; List<Book> books = bookService.findPage(keyword, (pageNum - 1) * pageSize, pageSize);这里计算总页数用(total + pageSize - 1) / pageSize而不是total / pageSize,是为了处理“总数不能整除页大小”的情况,比如总共25条数据、每页10条,这个公式得到的totalPages是3而不是2,避免了最后一页数据丢失的问题。
JSP页面用JSTL的forEach标签循环输出表格行,配合fmt:formatDate格式化日期:
<table class="table table-hover"> <thead> <tr> <th>书名</th><th>作者</th><th>出版社</th> <th>总库存</th><th>可借数量</th><th>操作</th> </tr> </thead> <tbody> <c:forEach items="${bookList}" var="book"> <tr> <td>${book.bookName}</td> <td>${book.author}</td> <td>${book.publisher}</td> <td>${book.total}</td> <td> <c:choose> <c:when test="${book.available > 0}"> <span class="text-success">${book.available}</span> </c:when> <c:otherwise> <span class="text-danger">已借完</span> </c:otherwise> </c:choose> </td> <td> <c:if test="${book.available > 0}"> <a href="borrow?action=borrow&bookId=${book.id}">借阅</a> </c:if> </td> </tr> </c:forEach> </tbody> </table>如果没有JSTL和EL表达式,这些代码全部要用Java脚本片段在JSP里写,页面会变得极其混乱。这也是热词里“如果在jsp上写java代码的风险”这个话题的来源——JSP中嵌入大量Java代码,虽然运行没问题,但可维护性极差,而且页面设计师根本没法改这种模板。这套系统坚持用EL+JSTL就是好习惯的体现。
4.2 Forward与Redirect的选用原则
这是我在团队评审时被追问最多的问题。简单总结我的选择逻辑:
- Forward是服务器端转发,URL地址栏不变,可以携带request属性,适合把数据带往JSP渲染。
- Redirect是客户端重定向,URL变更,无法直接携带request属性,适合登录成功后跳转或表单提交后的防重复提交。
在借阅操作完成后,我用的就是Redirect:
resp.sendRedirect(req.getContextPath() + "/my/borrow");这样做的原因是:借阅操作触发了数据修改,如果使用Forward停留在当前页面,用户在刷新时会再次提交请求,导致重复借阅。Redirect之后,刷新的是新的URL请求,不会重复借书。这是在Web开发中很常见但很容易忽略的一个细节,尤其是使用JSP+Servlet的同学要特别注意。
4.3 JSP页面中的几个常见隐患
写JSP时我遇到过几个坑,这里单独列出来供参考。
第一是pageEncoding和contentType的编码设置。JSP第一行必须写<%@ page contentType="text/html;charset=UTF-8" language="java" %>,同时页面文件本身保存为UTF-8编码,两者不一致会出现中文乱码。当年我第一次写JSP时没注意这个,结果页面上所有中文都变成问号,排查了半天才发现是编码问题。
第二是JSP自动生成的Java代码会改变表单提交方式,所以表单中必须明确写method="post",如果漏写默认为GET,URL会带着参数,既不安全也容易超出URL长度限制。
第三是尽量减少<%= %>表达式的使用,改用EL表达式来获取作用域中的值。EL表达式为null时页面显示空白而不是抛出异常,这在渲染页面时会友好很多。
5. 常见问题排查与踩坑实录
这套系统的开发周期不算长,但踩的坑一个都没少。我把最有代表性的几个问题整理出来,供大家参考。
5.1 数据库中文乱码终极排查
这个问题几乎每个Java Web项目都会遇到。我当时严格按照以下顺序排查:
- 第一步:检查数据库表字符集,执行
SHOW CREATE TABLE t_book;,查看CHARSET是否为utf8或utf8mb4。 - 第二步:检查JDBC连接URL是否包含
characterEncoding=utf8。 - 第三步:检查Servlet是否设置
req.setCharacterEncoding("UTF-8"),而且必须在读取参数之前设置。 - 第四步:检查JSP页面是否设置了
pageEncoding="UTF-8"。 - 第五步:检查MySQL连接器版本,老版本驱动对utf8mb4支持不佳,建议升级到8.x版本。
如果以上五步都检查过还是乱码,大概率是IDE的文件编码问题。默认GBK编码的项目在提交到Git后会变成乱码历史,所以建项目第一步就应该把IDE默认编码设为UTF-8,这个习惯比任何技术修复都重要。
5.2 Spring容器注入为空的问题
我写过一版代码,Servlet中直接@Autowired注入Service,结果启动后一直报空指针。原因在于Servlet是由Tomcat容器管理的,不在Spring容器管辖范围内。Spring只能注入那些由它创建的Bean,而Servlet由Servlet容器创建,两者是不同的容器体系。
正确做法是:
- 在Servlet的init方法中,用
WebApplicationContextUtils从ServletContext获取Spring容器。 - 或者让Servlet实现
SpringBeanAutowiringSupport,这样@Autowired才会生效。 - 或者干脆把Servlet也注册为Spring的Bean,通过
ServletRegistrationBean方式注册。
我在实际项目中选择了第一种方案,通过ContextLoaderListener监听器把Spring容器放入ServletContext,然后在Servlet init方法中获取,代码简洁且没有任何性能问题。
5.3 JdbcTemplate批量操作的正确姿势
处理批量插入时,我一开始用for循环逐条执行jdbcTemplate.update,数据量一上来性能惨不忍睹。后来改用batchUpdate方法,效率提升明显:
public void batchInsertBorrowRecords(List<BorrowRecord> records) { String sql = "INSERT INTO t_borrow (user_id, book_id, borrow_time, due_time, status) VALUES (?, ?, ?, ?, 0)"; jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { @Override public void setValues(PreparedStatement ps, int i) throws SQLException { BorrowRecord record = records.get(i); ps.setInt(1, record.getUserId()); ps.setInt(2, record.getBookId()); ps.setDate(3, new java.sql.Date(record.getBorrowTime().getTime())); ps.setDate(4, new java.sql.Date(record.getDueTime().getTime())); } @Override public int getBatchSize() { return records.size(); } }); }使用batchUpdate时,getBatchSize()返回值要与records.size()一致,如果数据量很大(比如超过10万条),建议分批处理,每批500条左右,避免一次性提交占用过多内存。setValues方法中的索引从1开始,我们当时因为下标写错导致整个批处理失败,排查了很久才定位到问题。
java.util.Date转java.sql.Date时需要注意,如果还需要时间部分,直接用new java.sql.Timestamp(record.getBorrowTime().getTime())更合适,避免丢失时分秒。
5.4 常见问题速查表
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 页面中文乱码 | 编码设置不一致 | 统一为UTF-8:JSP页面、Servlet、JDBC URL、数据库表字符集 |
| Spring注入为null | Servlet不受Spring管理 | 用WebApplicationContextUtils获取容器 |
| 数据库连接失败 | 驱动版本不匹配或URL错误 | 检查驱动jar包版本与URL格式,MySQL 8.x注意时区参数 |
| 图书库存变负数 | 缺少库存校验 | SQL加条件available + ? >= 0,配合事务回滚 |
| 刷新页面重复借书 | 表单重复提交 | 操作完成后Redirect跳转,而不是Forward |
| SQL注入风险 | 拼接SQL字符串 | 一律使用PreparedStatement占位符 |
5.5 我踩过的一个低级但影响巨大的Bug
借书功能上线测试时,我用两个账号同时借同一本库存只剩一本书的图书,结果两笔都成功了,库存变成了-1。排查发现原因是我在Service层没有加@Transactional注解,扣减库存和插入借阅记录之间出现了并发竞争,两个请求同时读到旧库存,各自执行了扣减。
解决方案是加事务注解并在数据库层做条件更新。@Transactional默认隔离级别是可重复读,两个并发事务想同时更新同一行,数据库行锁会保证只有一个事务能成功。我在第2章写的那个updateAvailable方法中加上available + ? >= 0条件,再配合事务,基本上保证了并发场景下的数据正确性。
这个教训让我明白一个道理:在开发环境里测不出并发问题,不代表生产环境没有,数据库操作必须从第一天就考虑并发安全。
6. 项目扩展与后续优化方向
当前这套系统已经能支撑一个中小型图书馆的日常管理,但离生产级别还有不少距离。如果希望进一步优化或把这个项目作为简历亮点,可以在以下几个方向继续发力。
6.1 引入连接池提升数据库性能
目前代码中直接用的DriverManagerDataSource,每次获取连接都会创建新的物理连接。在实际部署中,更合理的方案是换成Druid或HikariCP连接池:
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <property name="url" value="jdbc:mysql://localhost:3306/library_db?useSSL=false&characterEncoding=utf8"/> <property name="username" value="root"/> <property name="password" value="123456"/> <property name="initialSize" value="5"/> <property name="maxActive" value="50"/> <property name="minIdle" value="5"/> <property name="maxWait" value="60000"/> </bean>连接池把数据库连接复用起来,避免频繁创建和销毁连接的开销,在高并发场景下性能提升非常显著。如果你想让项目更有亮点,把普通数据源替换为连接池是很值得做的优化项。
6.2 前端页面与交互体验升级
当前的JSP页面还停留在服务端渲染的层次。如果要优化用户体验,可以在前端引入Vue或React,后端保留Servlet提供JSON接口。这种前后端分离改造涉及整个架构调整,适合有大把时间的情况下做。
如果只想小步快跑,也可以先在JSP页面引入jQuery和Ajax,把局部刷新做起来。比如图书检索页面的异步搜索、预约状态的轮询更新等,都能显著提升交互体验,而且技术改动不大。
6.3 引入缓存优化热点数据
图书馆系统中的热门图书检索、图书排行榜、公告信息等属于高频读取、低频修改的数据,非常适合引入缓存。可以自己实现一个简单的Map缓存,也可以接入Redis。考虑到项目技术栈比较传统,引入Redis需要搭建额外服务,如果只是学习用途,用一个ConcurrentHashMap的短时缓存就够了。
如果项目中引入了Redis,借阅超期计算还可以借助Redis的过期机制实现自动更新,不过这会增加系统的复杂度,看需求决定是否值得做。
6.4 引入Maven进行依赖管理
当时我用的是手工拷贝jar包的方式管理依赖,项目结构里放了一堆jar文件,不仅体积大,版本还容易混乱。如果从头开发这个项目,强烈建议从第一天就使用Maven或Gradle管理依赖,把JSP、Servlet、Spring、JdbcTemplate、MySQL驱动等所有依赖都写到pom.xml中,一键构建即可运行,无论对自己还是对后来接手项目的人都友好得多。
写在最后的几点体会
慕仁大学图书馆管理系统这套JSP+Spring+JDBC+Servlet的项目,做完一遍之后,我对Java Web开发的理解有了质的提升。它能帮助你把从前端页面到后端处理再到数据库操作这条完整链路彻底打通,这些底层知识在后来的Spring Boot项目中依然适用,只是被框架藏得更深了。
我想给正在做类似项目的同学几个真心建议。第一,不要嫌弃技术栈老,先把底层原理吃透,后面学新框架会轻松很多。第二,不要只写功能代码,多做并发压测和异常场景测试,很多问题只在特定条件才会暴露。第三,写提交信息时要有意义,这是整个项目里最被忽略却最值得坚持的工程习惯。
最后分享一个调试小技巧:在使用JdbcTemplate时,如果SQL执行结果不符合预期,先在Navicat或命令行中把SQL手动执行一遍,看是不是SQL本身写错了。很多时候问题不在Java代码,而在SQL语句本身。把这条排查路径变成肌肉记忆,能帮你省下大量不必要的调试时间。
本文还有配套的精品资源,点击获取