简介:这套基于SSM+MySQL的图书管理系统设计与实现源码包,定位为JavaWeb课程设计/期末大作业的满分参考方案,面向需要快速完成图书管理类项目的在校生与开发者。内含完整Java源码、JSP页面、SQL数据库脚本及实验报告,代码注释清晰,配合前端静态资源(CSS/JS/图片等)与配置文件,简单部署即可运行,功能覆盖图书借还、分类管理、读者管理等常见模块。包内共664个文件,压缩包约38.28MB,以Java源码(93个)、JSP页面(51个)、图片素材(124个PNG、106个GIF等)为主体,还包含CSS样式、SQL脚本和项目说明文档,结构直观便于对照学习。当前已有357人学习与下载。借助这套完整方案,读者可以迅速理解SSM整合原理与页面交互逻辑,节省从零搭建的时间;实验报告也为答辩或文档撰写提供了参考,是冲刺高分、巩固JavaWeb技能的实用资源。
1. 图书管理系统大作业,为什么都选 SSM+MySQL 这套组合
"图书管理系统"大概是高校 Java Web 课程里出现频率最高的一类大作业,而 SSM(Spring + SpringMVC + MyBatis)+ MySQL 又是其中最主流的组合。市面上的"满分大作业"源码包,十有八九是这套技术栈:SSM 撑起后端骨架,MySQL 负责数据底座,源码、数据库脚本和实验报告三件套各司其职。
这套组合被反复选用的逻辑很清楚:分层清晰,Controller、Service、Mapper 各管一段,写进实验报告就是现成的架构图;Spring 的依赖注入配合 MyBatis 的半自动 SQL,代码量保持在课程设计该有的体量——比 Spring Boot 全家桶更能展示"设计过程",比纯 JDBC 又体面得多。
这篇文章适合正在做或准备做图书管理系统大作业的读者。无论你是打算从头自己写,还是拿到一份参考源码想彻底搞懂它怎么跑起来、每个文件干什么,沿着下面几个章节都能把项目吃透,并把答辩时最常踩的坑提前填平。
2. 数据库设计先行:四张核心表的字段、外键与初始化 SQL
拿到一套图书管理系统源码,我先看的往往不是 Java 文件,而是 SQL 脚本。表结构决定了一个课程设计的业务上限——字段设计得好,后面写代码、画 ER 图、答追问都顺;字段设计得含糊,Controller 写得再漂亮,演示时也经不起深挖。下面这套表结构是这类大作业的标准方案,建议先照这个思路把库建出来,再谈代码。
2.1 功能模块与角色划分:先定边界再建表
动手建表之前,把功能清单列出来,划清两个角色的边界。管理员要做的是:维护图书(增删改查、上下架)、维护分类、管理读者(冻结与解冻)、查看借阅记录和逾期罚款统计。读者要做的是:检索图书、借书、还书、查看个人借阅历史和当前在借列表。
我一般建议把"借书-还书-逾期罚款"这条完整链路做扎实,其他功能点到为止。课程设计的评分逻辑通常是"有一个完整闭环,再有一个能展开讲的亮点",而不是"菜单铺了一大堆,却没有一个功能能讲清楚"。借书时校验库存并扣减、还书时自动计算逾期天数与罚款、管理端能看到热门图书排行,这三个点任意一个都够在答辩时讲上五分钟。
角色设计方面,很多同学喜欢拆 admin 和 reader 两张用户表。我不推荐这么干:这个项目里两类用户字段差异其实只有一两个,管理员不需要学号,读者不需要管理权限,用一个 role 字段(0 管理员 / 1 读者)完全够用。合并之后登录只查一张表,Session 里存 userId 和 role 就能做权限控制,联表查询少一半,实验报告里画 E-R 图也更省事。
2.2 从用户到借阅记录:tb_user、tb_book、tb_borrow 的关键字段设计
四张核心表分别是:tb_user(用户)、tb_category(分类)、tb_book(图书)、tb_borrow(借阅记录)。外表看起来简单,但有几个字段要特别想清楚。
tb_user 的 password 字段长度给到 VARCHAR(64),不是拍脑袋。MD5 摘要固定 32 位十六进制,VARCHAR(32) 刚好够,但一旦你想加盐或者升级成 SHA-256,32 就卡脖子了,64 留了余量。status 字段做冻结功能,演示时可以现场把某个读者账号冻结,再登录给他看"账号已被冻结"的提示,这个细节很多同学忽略,但它是"管理端有完整语义"的直接证据。
tb_book 里最容易混的是 stock 和 borrow_count。stock 是"当前可借库存",每次借书减 1、还书加 1;borrow_count 是"累计借出次数",只增不减,用来做热门图书统计。两个字段表达两个完全不同的业务概念,别合并成一个,否则你还书时都不知道该加还是该减。
tb_borrow 的状态字段建议用 1-借出中、2-已归还、3-逾期未还。逾期状态不要靠定时任务去"改",而是在还书那一刻实时判断:还书时间晚于应还时间,就按逾期天数计算罚款并置为 3。这个设计答辩时讲出来很加分,因为它说明你想清楚了"这个数据到底在什么时机被修改"。
2.3 初始化 SQL 的三个落地细节:外键、索引与测试数据
完整的建库脚本如下,MySQL 5.7 和 8.0 都能直接跑。建库时指定 utf8mb4 是硬要求,否则后面中文乱码会让你查找一整晚。
-- 建库:字符集统一 utf8mb4,乱码问题从这里就掐断 CREATE DATABASE IF NOT EXISTS book_manager DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE book_manager; -- 用户表:管理员和读者共用一张表,用 role 区分 CREATE TABLE tb_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录名', password VARCHAR(64) NOT NULL COMMENT '密码,建议 MD5 后存储', real_name VARCHAR(50) NOT NULL COMMENT '真实姓名', student_no VARCHAR(20) DEFAULT NULL COMMENT '学号,读者专用', phone VARCHAR(20) DEFAULT NULL, role TINYINT NOT NULL DEFAULT 1 COMMENT '0-管理员,1-读者', status TINYINT NOT NULL DEFAULT 1 COMMENT '1-正常,0-冻结', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_role (role) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; -- 分类表 CREATE TABLE tb_category ( id INT PRIMARY KEY AUTO_INCREMENT, category_name VARCHAR(50) NOT NULL, description VARCHAR(200) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='图书分类表'; -- 图书表 CREATE TABLE tb_book ( id INT PRIMARY KEY AUTO_INCREMENT, book_name VARCHAR(100) NOT NULL, isbn VARCHAR(20) NOT NULL, author VARCHAR(50) DEFAULT NULL, publisher VARCHAR(100) DEFAULT NULL, category_id INT DEFAULT NULL, price DECIMAL(8,2) DEFAULT NULL, stock INT NOT NULL DEFAULT 0 COMMENT '当前可借库存', borrow_count INT NOT NULL DEFAULT 0 COMMENT '累计借出次数', location VARCHAR(100) DEFAULT NULL COMMENT '馆藏位置', status TINYINT NOT NULL DEFAULT 1 COMMENT '1-可借,0-下架', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_category (category_id), KEY idx_isbn (isbn), CONSTRAINT fk_book_category FOREIGN KEY (category_id) REFERENCES tb_category(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='图书表'; -- 借阅记录表 CREATE TABLE tb_borrow ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, book_id INT NOT NULL, borrow_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '借出时间', due_time DATETIME NOT NULL COMMENT '应还时间', return_time DATETIME DEFAULT NULL COMMENT '实际归还时间', status TINYINT NOT NULL DEFAULT 1 COMMENT '1-借出中,2-已归还,3-逾期未还', fine DECIMAL(8,2) NOT NULL DEFAULT 0 COMMENT '逾期罚款金额', KEY idx_user (user_id), KEY idx_book (book_id), KEY idx_status (status), CONSTRAINT fk_borrow_user FOREIGN KEY (user_id) REFERENCES tb_user(id), CONSTRAINT fk_borrow_book FOREIGN KEY (book_id) REFERENCES tb_book(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='借阅记录表'; -- 初始数据:一个管理员 + 两个读者 INSERT INTO tb_user (username, password, real_name, student_no, phone, role) VALUES ('admin', '21232f297a57a5a743894a0e4a801fc3', '系统管理员', NULL, NULL, 0), ('stu001', 'e10adc3949ba59abbe56e057f20f883e', '张三', '2023001', '13800000001', 1), ('stu002', 'e10adc3949ba59abbe56e057f20f883e', '李四', '2023002', '13800000002', 1); -- 注:e10adc3949ba59abbe56e057f20f883e 是 123456 的 MD5,admin 那串是 admin123 的 MD5 INSERT INTO tb_category (category_name, description) VALUES ('计算机', '编程、算法、数据库相关'), ('文学', '小说、散文、诗歌'), ('历史', '中外历史著作');细节一:外键约束。删除一本图书前,必须先确认 tb_borrow 里没有这本书的未归还记录,否则外键直接报错。演示现场"删不掉"时,第一反应应该是查借阅表而不是怀疑 SQL 写错。如果不想被这个约束打扰,可以把两张外键改成语义上的逻辑关联,但我建议保留,因为实验报告里写"通过外键保证数据完整性"是一句实打实的话。
细节二:索引。isbn 用普通索引就够了,不要设唯一索引——同一个 ISBN 的书可能有多本副本,只是 id 不同。外键字段(category_id、user_id、book_id)和经常作为查询条件的 status 都值得加索引,这几条是借阅列表页和图书列表页查询频率最高的列。
细节三:测试数据。至少要准备一个管理员、两个读者、十本以上覆盖三个分类的图书,再手工插入几条借阅记录,其中故意留一条"应还时间已经过期、但还没还"的记录。这样演示逾期罚款时不需要现场等三十天,直接还这一本就能看到罚款金额跳出来,演示节奏会从容很多。
3. 三层框架搭建与核心代码:从配置文件到借书流程跑通
数据库就位后,下一步是把 SSM 三层搭起来。这一章不列全部源码,只把工程骨架和一条借书链路讲透——借书这条路通了,其余增删改查就是同一套写法的复制粘贴,不会有本质区别。
3.1 Maven 依赖与两个核心配置文件:版本组合和 Bean 装配
先给一组经过验证的版本搭配:Spring 5.2.x、MyBatis 3.5.x、mybatis-spring 2.0.x、MySQL Connector/J 5.1.49(配 MySQL 5.7)或 8.0.x(配 MySQL 8)、Druid 1.2.x。这套组合在 JDK 8 下编译运行没有任何问题,也是绝大多数参考源码的默认配置。版本锁定这件事千万别大意,Spring 5 和 Spring 4 的包名差异、mybatis-spring 2.x 对 Spring 5 的强依赖,任何一个错位都会让你在 Tomcat 启动阶段就翻车。
<properties> <spring.version>5.2.22.RELEASE</spring.version> <mybatis.version>3.5.6</mybatis.version> <mybatis-spring.version>2.0.6</mybatis-spring.version> <druid.version>1.2.8</druid.version> </properties> <dependencies> <!-- Spring MVC 与事务 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <!-- MyBatis 与 Spring 整合 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>${mybatis.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>${mybatis-spring.version}</version> </dependency> <!-- MySQL 驱动:MySQL 5.7 用 5.1.49,MySQL 8 换成 8.0.33 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.49</version> </dependency> <!-- 连接池 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>${druid.version}</version> </dependency> <!-- JSP 标准标签库 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> </dependencies>依赖只是前提,真正决定框架能不能转起来的是两个 XML 配置。applicationContext.xml 是 Spring 的根容器,负责数据源、SqlSessionFactory、Mapper 扫描和事务;spring-mvc.xml 是 Web 层容器,只管 Controller 和视图解析。两个容器职责分开是 SSM 的经典约定,别图省事全塞进一个文件。
<!-- applicationContext.xml:根容器,管 Service、Mapper、数据源、事务 --> <context:component-scan base-package="com.example.bms"> <!-- Controller 交给 spring-mvc.xml 管,这里要排除掉 --> <context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/> </context:component-scan> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="url" value="jdbc:mysql://localhost:3306/book_manager?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai"/> <property name="username" value="root"/> <property name="password" value="root"/> <property name="initialSize" value="5"/> <property name="maxActive" value="50"/> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.example.bms.mapper"/> </bean> <tx:annotation-driven/>这里有几个参数必须说清楚。url 里的useUnicode=true&characterEncoding=utf8是中文不乱码的前提,两个参数要成对出现;serverTimezone=Asia/Shanghai在 MySQL 8 驱动下是必填项,缺了它日期会整体偏移八小时;useSSL=false是为了关掉本地开发的 SSL 握手告警。MapperScannerConfigurer 的 basePackage 写的是 Mapper 接口所在的包,扫描到接口后会自动生成代理对象,你才能在 Service 里直接@Autowired一个接口进去。
<!-- spring-mvc.xml:Web 容器,管 Controller 和视图 --> <context:component-scan base-package="com.example.bms.controller"/> <mvc:annotation-driven/> <mvc:default-servlet-handler/> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/views/"/> <property name="suffix" value=".jsp"/> </bean>mvc:annotation-driven这一行必不可少,它负责注册 HandlerMapping 和 HandlerAdapter,同时帮你把 JSON 转换、类型转换等默认组件配好。少了它,Controller 里的@RequestMapping根本不会被识别,页面 404 都算轻的。InternalResourceViewResolver 的 prefix 指向/WEB-INF/views/,意味着所有 JSP 都要放在这个目录下,Controller 返回逻辑视图名后由它拼出真实路径。
3.2 借书业务的完整链路:Controller 到 Mapper 的关键代码
借书是整张业务表里最能体现"设计感"的操作,因为它在同一个事务里干了三件事:校验图书状态、扣减库存、插入借阅记录。把这三件事的先后顺序和并发安全性想明白,答辩就赢了一半。先看 Controller:
@Controller @RequestMapping("/borrow") public class BorrowController { @Autowired private BorrowService borrowService; @RequestMapping("/add") public String borrow(Integer bookId, HttpSession session) { // 登录拦截器已经保证 session 里有 userId Integer userId = (Integer) session.getAttribute("userId"); try { borrowService.borrowBook(userId, bookId); } catch (RuntimeException e) { // 业务异常直接带到列表页提示,不回跳转混淆页面 return "redirect:/book/list?error=" + e.getMessage(); } return "redirect:/borrow/list"; } }Controller 里不写任何业务判断,它的职责只是取参数、调用 Service、决定跳转。用户名从 Session 里拿而不是让页面传,这是防止越权的第一道防线——如果 borrow 接口接收一个 userId 参数,那读者就可以随便替别人借书了。Service 接口的实现才是核心:
@Service @Transactional public class BorrowServiceImpl implements BorrowService { @Autowired private BookMapper bookMapper; @Autowired private BorrowMapper borrowMapper; @Override public void borrowBook(Integer userId, Integer bookId) { // 先查图书状态,下架的或者不存在的直接拒绝 Book book = bookMapper.selectById(bookId); if (book == null || book.getStatus() != 1) { throw new RuntimeException("图书不存在或已下架"); } // 关键点:用条件 UPDATE 代替"先查库存再扣减",从根上避免并发超借 int rows = bookMapper.decreaseStock(bookId); if (rows == 0) { throw new RuntimeException("库存不足,借书失败"); } // 生成借阅记录,默认借期 30 天 Borrow borrow = new Borrow(); borrow.setUserId(userId); borrow.setBookId(bookId); borrow.setBorrowTime(new Date()); Calendar cal = Calendar.getInstance(); cal.add(Calendar.DAY_OF_MONTH, 30); borrow.setDueTime(cal.getTime()); borrow.setStatus(1); borrowMapper.insert(borrow); } }decreaseStock返回影响行数,这是整个方法里最值得在答辩时展开的点。两个读者同时点借书,如果先select再update,两人都能查到 stock=1,最后库存会变成 -1;而把条件写进 UPDATE 的 WHERE 子里,数据库行锁保证只有一个请求能成功,另一个影响行数为 0。MyBatis 的对应 SQL 如下:
<update id="decreaseStock"> UPDATE tb_book SET stock = stock - 1, borrow_count = borrow_count + 1 WHERE id = #{bookId} AND stock > 0 </update>@Transactional保证 insert 和 update 同生共死:如果 insert 失败抛异常,前面的扣库存也会回滚。但要注意,只有 RuntimeException 才会触发 Spring 默认的回滚策略,如果代码里把异常 catch 住吞掉了,事务就形同虚设。
3.3 登录拦截器:演示时被追问的高频点
没有登录拦截器的图书系统,等于把管理员的增删改查接口裸奔在公网上。课程设计阶段很多同学只在前端用session判断,后端的 Controller 没有任何保护,答辩老师登录一个普通读者账号,直接访问/book/delete?id=1,系统照删不误——这是最容易翻车的场景。标准做法是加一个 HandlerInterceptor:
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); // 登录成功后把用户对象放进了 session,这里只做存在性判断 Object loginUser = session.getAttribute("loginUser"); if (loginUser == null) { response.sendRedirect(request.getContextPath() + "/login"); return false; } return true; } }然后在 spring-mvc.xml 里注册,并显式排除登录相关的路径和静态资源:
<mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/**"/> <mvc:exclude-mapping path="/login"/> <mvc:exclude-mapping path="/login.jsp"/> <mvc:exclude-mapping path="/static/**"/> <bean class="com.example.bms.interceptor.LoginInterceptor"/> </mvc:interceptor> </mvc:interceptors>/login和静态资源必须排除,否则登录页面的 CSS 加载不出来、登录请求自己也被拦截,直接死循环。如果你还细分了管理员和读者权限,可以再写一个 role 判断:loginUser.role == 0才放行/admin/**路径。这个拦截器加上之后,实验报告里的"系统安全设计"章节就有着落了。
4. 连接、事务、分页与日期:四个必调参数和页面联调细节
代码能编译只是第一步。很多源码包导入后,你第一眼看到的不是功能页面,而是一串参数报错。这一章把演示前必须检查的四个参数点讲清楚,它们几乎决定了你的系统是"能跑"还是"能演示"。
4.1 MySQL 连接串与 Druid 连接池的必调参数
连接串是排查乱码、日期偏移、连不上库的第一现场。把这几个参数背下来,遇到问题先对照一遍:
| 连接串参数 | 作用 | 必填场景 |
|---|---|---|
| useUnicode=true&characterEncoding=utf8 | 驱动与数据库之间以 UTF-8 传输 | 只要表里有中文就必填 |
| useSSL=false | 关闭 SSL 握手 | 本地开发必填,否则日志全是警告 |
| serverTimezone=Asia/Shanghai | 指定驱动使用的服务器时区 | MySQL 8 驱动必填,否则日期偏差 8 小时 |
| allowPublicKeyRetrieval=true | 允许获取公钥用于密码认证 | MySQL 8 配合 caching_sha2_password 时可能用到 |
Druid 连接池的参数里,我建议至少设置这四个,其他用默认值就够课程设计用:
| 参数 | 建议值 | 说明 |
|---|---|---|
| initialSize | 5 | 启动时建立 5 个连接,页面首访不卡 |
| maxActive | 50 | 最大活跃连接数,课程设计 50 足够 |
| maxWait | 10000 | 拿不到连接时最多等 10 秒,避免页面无限转圈 |
| validationQuery | SELECT 1 | 空闲连接检测,防止 MySQL 8 小时超时断开连接 |
maxWait 是很多人忽视的救命参数。没有它,当连接池被打满时,新请求会一直阻塞,Tomcat 线程被耗尽,页面表现为"假死"——刷新没用,重启才行。设成 10 秒后,超时直接抛异常,你能在日志里看到明确错误而不是一无所知。
提示:本地演示前把 url 里的密码改成你自己的 MySQL 密码,这是所有"数据库连不上"类问题的第一排查项。
4.2 事务与分页:课程设计里最容易被追问的两个点
事务上面已经提过,这里补两个容易栽的细节。第一,@Transactional只能加在 public 方法上,加到 private 方法上 Spring 的代理根本看不见,静默失效。第二,Spring 默认只在 RuntimeException 时回滚,如果你在 catch 里把异常吞掉再返回一个正常值,事务同样白配。养成习惯:业务方法里不要 catch,让异常往上层抛,由 Controller 统一处理提示。
分页是借阅记录列表和图书列表的标配。课程设计阶段有两种做法:自己拼LIMIT,或者用 PageHelper 插件。我更推荐 PageHelper,因为它把"总条数统计"和"当前页数据"一次搞定,少写不少样板代码。在 SqlSessionFactory 里注册插件:
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> <property name="plugins"> <array> <bean class="com.github.pagehelper.PageInterceptor"> <property name="properties"> <value> helperDialect=mysql reasonable=true </value> </property> </bean> </array> </property> </bean>Service 里使用时有个经典坑:PageHelper.startPage()必须紧跟它的第一条 MyBatis 查询,中间不能插入其他查询语句,否则分页会被应用到错误的 SQL 上。常见写法是:
PageHelper.startPage(pageNum, pageSize); List<Book> list = bookMapper.selectByCondition(bookName, categoryId); PageInfo<Book> pageInfo = new PageInfo<>(list);reasonable=true的意思是页码越界时自动修正:你请求第 99 页但总共只有 5 页,它会把页码钳到第 5 页而不是返回空列表。这个细节在演示时很实用,因为你不知道台下老师会翻到哪一页。
4.3 JSP 页面的日期格式化和状态字段显示
后端把Date对象塞进 request 交给 JSP,页面直接输出会得到一长串英文格式,比如Thu Sep 01 00:00:00 CST 2024,既不美观也不像中文系统。统一用 JSTL 的 fmt 标签处理:
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%-- 借阅时间 --%> <fmt:formatDate value="${borrow.borrowTime}" pattern="yyyy-MM-dd HH:mm:ss"/> <%-- 状态字段:用 c:choose 转成中文,别在 Java 里拼字符串 --%> <c:choose> <c:when test="${borrow.status == 1}">借出中</c:when> <c:when test="${borrow.status == 2}">已归还</c:when> <c:when test="${borrow.status == 3}">逾期未还</c:when> </c:choose>状态字段的显示要集中处理,别散落在各个 JSP 里各写一套。一个页面用 c:choose,另一个页面直接输出数字 1,答辩老师很容易看出你的代码是拼凑的。罚款金额也是同理,${borrow.fine}直接输出可能不带两位小数,用 fmt:formatNumber 统一成0.00格式。
5. 避坑指南:SSM 图书系统从启动到演示的 5 个常见问题
下面这几条是这类项目里出现频率最高的问题,每条我都按实际排查顺序写,现象对得上就直接跳到解决步骤。
5.1 依赖版本冲突导致 Tomcat 启动即失败
现象:Tomcat 启动时报java.lang.NoClassDefFoundError或BeanCreationException: Error creating bean with name 'sqlSessionFactory',控制台日志指向org.springframework.beans.factory,但代码本身看起来没有任何问题。
原因:最常见的有两类。一是 Spring 与 mybatis-spring 版本跨度太大,mybatis-spring 2.x 要求 Spring 5.x,如果你用的是 Spring 4.x,启动时会出现 Bean 创建异常;二是 IDEA 的 Artifacts 配置问题——pom 里明明有依赖,但 Tomcat 部署的 war 包里没有把依赖打进WEB-INF/lib,运行时自然找不到类。
解决:版本按本章 3.1 节的组合锁定,统一用properties管理版本号;然后在 IDEA 的 Project Structure → Artifacts 里确认 Output Layout 包含lib目录并勾选所有依赖。改完后 Clean 再 Rebuild,别直接 Redeploy,旧的 lib 会残留。
5.2 MySQL 8 驱动改名与日期偏移
现象:配置文件和 pom 都没动,启动后报ClassNotFoundException: com.mysql.jdbc.Driver;或者系统能跑,但所有日期字段都比真实时间晚了 8 小时。
原因:MySQL Connector/J 从 8.0 开始把驱动类从com.mysql.jdbc.Driver改成了com.mysql.cj.jdbc.Driver,同时强制要求连接串里带serverTimezone。你的源码包如果是在 MySQL 5.7 时代写的,拿到 MySQL 8 环境必然踩这两个问题。
解决:二选一。如果继续用 MySQL 5.7,驱动保持5.1.49不动;如果本地装的是 MySQL 8,把 driver-class 改成com.mysql.cj.jdbc.Driver,并在 url 末尾加上&serverTimezone=Asia/Shanghai。我一般建议跟随本地 MySQL 版本改驱动,而不是给本地降级数据库。
5.3 JSP 页面 EL 表达式原样输出或取不到数据
现象:页面上直接显示${book.bookName}这样的字符串,或者表达式没报错但显示空白。更隐蔽的是:列表页其他数据都正常,只有某一列取不到值。
原因:前者通常是 web.xml 版本太低,EL 默认被禁用,或者 web.xml 里显式写了<el-ignored>true</el-ignored>;后者九成是 request attribute 的名字没对上——Controller 里放进去的是bookList,页面却写${books},两边差一个字母。
解决:确认 web.xml 用的是 Servlet 3.0 以上版本头(看<web-app>标签的 version 属性),并检查 attribute 名逐字比对。还要注意:return "redirect:/book/list"是重定向,request 域的 attribute 会全部丢失;return "/book/list"是转发,attribute 才保留。这个区别在调试时很容易让人怀疑人生。
5.4 中文乱码的三处源头
现象:数据库里存进去的中文变成问号,或者从数据库查出来在页面上显示乱码。有时插入正常、查询乱码,有时反过来。
原因:乱码从来不是一个点的问题,是链路里三处不一致。数据库/表字符集不是 utf8mb4;连接串缺characterEncoding=utf8;JSP 页面pageEncoding不是 UTF-8。三处只要有一处不同,就会在某一环节翻车。
解决:按顺序检查。SQL 脚本建库时指定DEFAULT CHARACTER SET utf8mb4;url 里带useUnicode=true&characterEncoding=utf8;JSP 头部写<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>。三处统一后,把已经乱码的表 drop 重建一次再验证。
5.5 @Transactional 回滚不生效
现象:借书时插入借阅记录成功,但库存扣减失败抛了异常,前台的 insert 却没有回滚,页面上出现"有借阅记录但库存没减"的数据不一致。
原因:三层原因按概率排——异常被 catch 住吞掉了;@Transactional加在了 private 方法上;或者根容器没配置tx:annotation-driven。第一个最常见,因为初学者习惯在 Service 里 try-catch 后 return 一个结果。
解决:业务方法里不要自己 catch,抛出去让 Controller 统一处理;@Transactional必须加在 public 方法上;检查 applicationContext.xml 里确实有<tx:annotation-driven/>,并且SqlSessionFactory的事务管理器已被 Spring 管理。改完重启,故意制造一次库存不足的借书,观察数据库里是否多出半截数据,没有就是回滚生效了。
6. 让大作业从"能跑"变成"能拿高分":实验报告与答辩的加分技巧
很多人的系统功能没问题,最后分数却不理想,问题出在实验报告和答辩表现。报告不是代码的堆砌,而是让老师快速确认"你确实理解这套系统"的说明书。
6.1 实验报告的结构:图表比代码更值钱
我建议按五个章节组织:需求分析(功能清单 + 用例描述)、数据库设计(E-R 图 + 表结构说明)、系统设计(分层架构图 + 模块职责)、核心实现(借书/还书流程的关键代码 + 时序说明)、系统测试(测试用例表 + 操作截图)。图表数量尽量大于代码数量,代码只贴关键流程的核心片段,别把全部源码复制进去。
数据库设计这一章,把第二章的建表脚本整理成表结构说明表格,每个字段写清楚含义,外键关系用文字描述。系统测试部分至少要列十行测试用例:正常登录、错误密码、冻结账号登录、借书成功、库存不足借书、还书逾期罚款、分页查询、模糊搜索。每行写好操作步骤、预期结果、实际结果,这就是"满分报告"的样子。
6.2 答辩现场最常被问的 4 个问题
第一个问题几乎必问:"为什么用 SSM 而不是 Spring Boot?"标准答案不是踩一个捧一个,而是说明 SSM 的分层让你能更清楚地展示 Controller、Service、Mapper 的职责边界,MyBatis 手写 SQL 便于你控制复杂的联表查询。第二个问题:"并发情况下两个读者同时借最后一本书怎么办?"把 3.2 节的条件 UPDATE 讲出来,再补一句"数据库行锁保证只有一个请求能成功扣减",这个回答可以直接让老师点头。
第三个问题:"逾期罚款是怎么算的?"要讲清楚计算时机——还书那一刻实时计算,用还书时间减去应还时间得到逾期天数,再乘每日罚款单价,而不是靠定时任务批量改状态。第四个问题:"这些索引为什么这么建?"回答案:isbn 用于图书检索、外键字段用于联表、status 用于状态过滤,查询频率最高所以加索引。
我自己带项目时的习惯是:拿到任何一份参考源码,第一件事永远是先把 SQL 脚本从头读一遍,再对着表结构看 Mapper 的 SQL。代码可以包装出复杂的假象,但数据库设计骗不了人。这个习惯帮我避免过太多次"答辩现场被问到底层设计时哑口无言"的尴尬,希望你也能用上。希望帮到你。
本文还有配套的精品资源,点击获取