☰
SSM图书管理系统:从数据库设计到Tomcat部署避坑全指南
2026/10/2 18:47:25 网站建设 项目流程

简介:基于SSM框架实现的图书管理系统,是一套面向毕业设计、课程设计与期末大作业的Java实战项目资源,适合具备一定Java基础、想完整走通Spring、SpringMVC、MyBatis整合开发流程的学生。资源压缩包共311个文件、约62.87MB,内容涵盖Java源码与编译后的Class文件、JSP动态页面、Spring/MyBatis配置XML、SQL数据库脚本、依赖JAR包、部署说明文档、PPT演示文稿及界面截图素材;其中借阅管理、读者管理、图书检索、登录鉴权等核心模块分层清晰,Controller、Service、DAO结构完整,数据库脚本附带初始测试数据,代码注释较详细,配套文档可帮助快速完成部署运行。项目由个人手打完成并获得导师认可,已有321人学习使用,适合作为毕业设计、期末大作业或课程设计的高分参考,也可用于Java实训与面试项目复盘,整体兼具完整性与可扩展性。

1. SSM图书管理系统:从课程设计到真实落地的距离

很多人的 Java 课程设计选题是这个,面试突击也是从它开始:SSM 框架的图书管理系统,网上能找到的代码一抓一大把,但能让你在自己电脑上从空 MySQL 库跑出一条完整借书记录的,其实不多。这个项目表面上只有登录、图书增删改查、借书还书三个模块,但背后牵扯 Spring IoC 容器、SpringMVC 请求映射、MyBatis 参数绑定和声明式事务,恰好是 Java 后端岗位最常见的基础题范围。如果你正缺一个能写到简历上的实战项目,或者卡在项目构建、数据库初始化这些环境步骤上,它值得你投入一个周末。下文的表结构、配置和部署参数,都是照着可复现的标准给的。

2. 技术选型与数据库设计:SSM 与 MySQL 的搭配为什么最稳妥

2.1 SSM 三件套的分工:Spring 管对象、SpringMVC 管请求、MyBatis 管数据

SSM 是 Spring、SpringMVC、MyBatis 三个框架的组合,Spring 5.x 里已经能覆盖 JDK 8 到 JDK 17 的常用场景。Spring 的核心价值是 IoC 容器和 AOP:Controller、Service、Mapper 这些对象的创建和依赖注入都交给容器,事务、日志这类横切逻辑用 AOP 统一处理。SpringMVC 处理 HTTP 请求的流转,请求从 DispatcherServlet 进入,经过 HandlerMapping 找到对应 Controller 方法,返回时再由 ViewResolver 解析 JSP 或 JSON。MyBatis 负责 Java 对象和数据库表之间的映射,写 Mapper 接口加 XML 文件,就能把一条 SQL 的执行结果直接变成对象。

这三个框架拼在一起,正好覆盖一个 Java Web 项目的三层结构:Controller 层处理请求,Service 层写业务规则,Mapper 层做数据访问。对图书管理系统这种 CRUD 密集、事务边界清楚的项目,这种分层的好处是改业务时不用动数据库,换数据库时也不用动页面。很多 Java 面试题会问 IoC 是什么、AOP 解决什么问题、#{} 和 ${} 有什么区别,你把这个项目跑通后能顺手解释清楚——#{} 走的是 PreparedStatement 占位符,防 SQL 注入,${} 是字符串拼接,一般只用在表名或排序字段这种不能写占位符的位置。

动手之前先花三分钟确认本机环境,不然问题都堆到部署阶段一起爆发:

java -version mysql --version mvn -v

三条命令分别确认 JDK、MySQL、Maven 是否装了。JDK 版本建议 8 或 11,Maven 3.6+,MySQL 5.7 或 8.0 都行。如果你用的是 MySQL 8.0,后面连接配置会多几个参数,第 5 章会专门讲。确认命令有输出之后,再检查 Tomcat 是否解压到了纯英文路径的目录,目录带中文或者空格,后续启动时静默失败的几率很高。

2.2 三张核心表的设计:用户、图书、借阅记录如何拆分

图书管理系统的核心数据模型是三个实体:用户、图书、借阅记录。用户表存登录凭证和角色,图书表存书目信息和库存,借阅记录表把用户和图书关联起来。这种拆分方式不需要中间表,也没有多对多绕路,是最容易理解和扩展的库表结构。

借阅记录是项目的核心表,它既承担借书还书的业务状态,也是后续统计“谁借了什么书、什么时候到期”的唯一依据。一张借阅记录应该包含借书人 ID、图书 ID、借出时间、应还时间、实际归还时间和状态。状态字段最忌讳用字符串存中文,比如存“已借出”和“已归还”,中文在跨字符集迁移或排序时容易出幺蛾子,建议用 TINYINT 存数字枚举,0 表示借出,1 表示已还,2 表示逾期。后面查询和统计直接按数字过滤,页面需要中文文案时再转换。

至于图书表是否要单独拆一张分类表,取决于你接的需求。如果分类只是在列表页展示一下,book 表里放一个 category 字符串字段就够了;如果后续要按分类统计、做推荐,那才需要拆出 book_category 表。课程设计和两三个月的实习项目,真没必要一上来就建八张表。先跑通核心流程,再在第二个版本里做垂直拆分,这是最常见的演进路径。

3. 初始化数据库:建库脚本、测试数据与字段陷阱

3.1 建库、建表与插入测试数据的 SQL

先建库,再建表,最后插数据,顺序不要反。字符集统一用 utf8mb4,MySQL 8.0 的默认字符集已经是它,但 5.7 默认是 utf8mb3,存 emoji 或生僻字会丢数据,所以建库时最好显式指定。

CREATE DATABASE IF NOT EXISTS library_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE library_system; CREATE TABLE `user` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `username` VARCHAR(32) NOT NULL, `password` VARCHAR(64) NOT NULL, `real_name` VARCHAR(32) DEFAULT NULL, `role` TINYINT NOT NULL DEFAULT 1 COMMENT '0=管理员,1=普通用户', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `book` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `isbn` VARCHAR(20) NOT NULL, `title` VARCHAR(128) NOT NULL, `author` VARCHAR(64) DEFAULT NULL, `publisher` VARCHAR(64) DEFAULT NULL, `category` VARCHAR(32) DEFAULT NULL, `stock` INT NOT NULL DEFAULT 0 COMMENT '当前可借数量', `total_count` INT NOT NULL DEFAULT 0 COMMENT '入库总数', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY `uk_isbn` (`isbn`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `borrow_record` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `user_id` INT NOT NULL, `book_id` INT NOT NULL, `borrow_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `due_time` DATETIME DEFAULT NULL, `return_time` DATETIME DEFAULT NULL, `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0=借出,1=已还,2=逾期', KEY `idx_user` (`user_id`), KEY `idx_book` (`book_id`), CONSTRAINT `fk_borrow_user` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`), CONSTRAINT `fk_borrow_book` FOREIGN KEY (`book_id`) REFERENCES `book`(`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

建表时注意三件事。user 是 MySQL 的保留字,所以建表语句里要加反引号;book 表把 stock 和 total_count 分两个字段,stock 表示当前还能借的册数,total_count 表示这本书总共采购了多少本,借书减 stock,还书加 stock,total_count 保持不变,用来做馆藏统计;借阅记录表的外键约束一定要加,不然删除用户时可能留下孤儿数据。外键在业务量小的项目里完全够用,不用一开始就学互联网大厂那套去外键做法。

接着插入测试数据:

INSERT INTO `user` (`username`, `password`, `real_name`, `role`) VALUES ('admin', MD5('123456'), '系统管理员', 0), ('zhangsan', MD5('123456'), '张三', 1); INSERT INTO `book` (`isbn`, `title`, `author`, `publisher`, `category`, `stock`, `total_count`) VALUES ('9787111213826', 'Java编程思想', 'Bruce Eckel', '机械工业出版社', '编程', 3, 5), ('9787302170583', '深入理解Java虚拟机', '周志明', '清华大学出版社', '编程', 2, 3), ('9787115428028', 'MySQL必知必会', 'Ben Forta', '人民邮电出版社', '数据库', 5, 8);

密码字段用 MD5('123456') 而不是明文,是安全实践的一部分。虽然 MD5 在现代密码存储里已经不够看,但课程设计里配合登录验证足够,也无须引入额外加密组件。你要是有心气,往上升级时把密码改成 BCrypt,只需要改注册和登录两个 Service 方法。

3.2 字段设计上的三个坑:时间精度、枚举状态与唯一约束

时间字段是第一个坑。DATETIME 和 TIMESTAMP 都能存时间,但 DATETIME 的范围是 1000-01-01 到 9999-12-31,TIMESTAMP 只能存到 2038 年,而且 TIMESTAMP 会受 MySQL 时区影响。图书管理系统的借阅时间建议用 DATETIME,配合 DEFAULT CURRENT_TIMESTAMP 可以在插入时不手动赋值直接取系统时间。如果你的表已经建完了,可以用 ALTER TABLE 修复:

ALTER TABLE `borrow_record` MODIFY `borrow_time` DATETIME DEFAULT CURRENT_TIMESTAMP;

第二个坑是状态枚举。很多人喜欢用 VARCHAR 存“已借出”“已归还”,看着直观,但查询效率低、易写错、后期改文案还要 UPDATE 全表。我见过一个项目把状态从“已归还”改成“已还”,漏改了一条历史数据,统计报表直接崩了。TINYINT 存数字枚举,代码里定义常量或枚举类,页面层再做文案映射,是最稳妥的。

第三个坑是唯一约束。同一个用户名不能注册两次,同一本 ISBN 不应该在 book 表里出现多条,这些约束要在建表时就用 UNIQUE KEY 卡住,而不是在 Service 层里先 SELECT 再判断。你永远不知道并发请求会带来什么,数据库约束才是最后一道防线。真的出现重复数据后再清洗,代价是几倍起步。

4. 代码实现:从 Maven 依赖到登录、借书、还书的完整链路

4.1 工程结构与依赖清单

标准的 Maven Web 工程结构如下,按这个目录建,后面配置和部署都不会迷路:

library/ ├── pom.xml └── src/main/ ├── java/com/library/ │ ├── controller/ │ ├── service/ │ │ └── impl/ │ ├── dao/ │ ├── entity/ │ └── common/ ├── resources/ │ ├── mapper/ │ ├── spring/ │ │ ├── applicationContext.xml │ │ └── spring-mvc.xml │ └── db.properties └── webapp/ ├── WEB-INF/web.xml ├── static/ │ ├── css/ │ └── js/ └── views/

依赖清单是第一个容易翻车的地方,版本组合不对,编译能过但运行时各种 NoSuchMethodError。下面这套组合是我验证过能稳定配平 JDK 8 的:

<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.39</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.3.39</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.3.39</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.16</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.1.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.20</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>jstl</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>3.4.0</version> </plugin> </plugins> <finalName>library</finalName> </build>

spring-context 和 spring-webmvc 分别给了 IoC 容器和 MVC 能力,spring-jdbc 用来支撑事务管理器。mybatis-spring 是 MyBatis 和 Spring 整合的桥接包,缺少它,SqlSessionFactory 就无法交给 Spring 管理。Druid 是连接池,比直接用自带的池子好调优。javax.servlet-api 的 scope 是 provided,因为 Tomcat 自带 Servlet 容器,打 WAR 包时不该把它打进去。finalName 设为 library,部署后的访问路径就固定为 /library,省心。

4.2 核心配置:数据源、MyBatis 映射与 SpringMVC 扫描

数据库连接信息单独放一个 properties 文件,不要硬编码在 XML 里。这是血泪经验,我见过直接写在配置里的密码跟着代码一起提交到仓库的翻车现场。

jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/library_system?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true jdbc.username=root jdbc.password=你的数据库密码

MySQL 8 的驱动类名是 com.mysql.cj.jdbc.Driver,不是旧的 com.mysql.jdbc.Driver。URL 里的 serverTimezone 必须指定,否则连接会报时区错误。allowPublicKeyRetrieval 是配合 MySQL 8 的 caching_sha2_password 认证方式加的,不然可能在连接阶段报 Public Key Retrieval is not allowed。

Spring 侧的 applicationContext.xml 负责数据源、SqlSessionFactory 和事务:

<context:component-scan base-package="com.library"/> <context:property-placeholder location="classpath:db.properties"/> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <property name="driverClassName" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> <property name="initialSize" value="5"/> <property name="minIdle" value="5"/> <property name="maxActive" value="20"/> <property name="validationQuery" value="SELECT 1"/> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> <property name="configuration"> <bean class="org.apache.ibatis.session.Configuration"> <property name="mapUnderscoreToCamelCase" value="true"/> </bean> </property> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.library.dao"/> </bean> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <tx:annotation-driven transaction-manager="transactionManager"/>

连接池的 initialSize、minIdle、maxActive 是给并发准备的:分期项目里 maxActive 20 够用,线上环境再往上调。validationQuery 设为 SELECT 1,是让连接池在借出连接前先验证连接可用,防止数据库重启后连接池还在发死连接。mapUnderscoreToCamelCase 设为 true,数据库字段 create_time 才能自动映射到 Java 属性 createTime,这个不配,后面必然出现全是 null 的老问题。

spring-mvc.xml 只做三件事:扫描 Controller 包、开启注解驱动、放行静态资源。

<context:component-scan base-package="com.library.controller"/> <mvc:annotation-driven/> <mvc:resources mapping="/static/**" location="/static/"/> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/views/"/> <property name="suffix" value=".jsp"/> </bean>

最后那个 ViewResolver 的作用是 Controller 返回 "book/list" 时,自动拼出 /views/book/list.jsp 去找页面。JSP 统一放 views 目录下,不用在 Controller 写全路径,整洁很多。

4.3 登录、借书、还书的代码链路

登录是第一个入口,这里最容易犯的错误是拿明文密码直接比对。数据库里密码是 MD5 后的密文,所以 Service 层也要先做同样的加密再查库。Controller 只处理参数接收和页面跳转,不写业务逻辑:

@Controller public class LoginController { @Autowired private UserService userService; @RequestMapping(value = "/login", method = RequestMethod.POST) public String login(String username, String password, HttpSession session, Model model) { User user = userService.login(username, MD5Util.md5(password)); if (user == null) { model.addAttribute("error", "用户名或密码错误"); return "login"; } session.setAttribute("currentUser", user); return "redirect:/book/list"; } }

Service 返回 null 表示认证失败,Controller 把错误信息塞进 model 回到登录页。session 里存 currentUser,后续拦截器就靠这个属性判断是否登录。

借书是事务最集中的地方,涉及查询、扣库存、插入记录三步,必须加 @Transactional。不加注解的话,扣了库存但插记录失败,数据就不一致了:

@Service public class BorrowServiceImpl implements BorrowService { @Autowired private BookMapper bookMapper; @Autowired private BorrowRecordMapper borrowRecordMapper; @Override @Transactional(rollbackFor = Exception.class) public void borrowBook(Integer userId, Integer bookId) { Book book = bookMapper.selectByPrimaryKey(bookId); if (book == null || book.getStock() <= 0) { throw new BusinessException("图书不存在或库存不足"); } int activeCount = borrowRecordMapper .countActiveByUserIdAndBookId(userId, bookId); if (activeCount > 0) { throw new BusinessException("已借过这本书,请先归还再借"); } int updated = bookMapper.reduceStock(bookId); if (updated == 0) { throw new BusinessException("扣减库存失败,请重试"); } BorrowRecord record = new BorrowRecord(); record.setUserId(userId); record.setBookId(bookId); record.setStatus(0); record.setDueTime(DateUtils.addDays(new Date(), 30)); borrowRecordMapper.insertSelective(record); } }

注意 reduceStock 的 SQL 写法,它直接带条件更新:

<update id="reduceStock"> UPDATE book SET stock = stock - 1 WHERE id = #{bookId} AND stock > 0 </update>

这比先 SELECT 再 UPDATE 安全,因为 WHERE 条件里卡了 stock > 0,并发借书时数据库行锁会保证只有一方更新成功。返回 affected rows 为 0 时,说明库存已经被别人抢走。

还书就是反向操作:找到对应借阅记录,把它改成已还,图书库存加一。这里要把事务也加上,不然库存加了,借阅记录状态还是“借出”,下次统计逾期就乱套了。常见的做法是还书接口先查借阅记录,校验状态必须为 0 才能操作,然后一次 UPDATE 把 status 改成 1、return_time 设为当前时间。

5. 部署实操与避坑手册:Tomcat 环境下的配置、启动与 5 个高频问题

5.1 打包部署:IDEA 导出 WAR 与 Tomcat 启动

开发环境下 IDEA 可以直接用内置 Tomcat 或者插件跑,但部署到本地或服务器,最稳的还是 WAR 包加外部 Tomcat。先确认环境变量,再构建、部署、启动,按这个顺序来:

# 1. 确认 JDK 和 Tomcat 环境变量 echo $JAVA_HOME echo $CATALINA_HOME java -version # 2. Maven 打包,跳过单元测试 mvn clean package -DskipTests # 3. 把 WAR 拷到 Tomcat 的 webapps 目录 cp target/library.war $CATALINA_HOME/webapps/ # 4. 启动 Tomcat $CATALINA_HOME/bin/startup.sh # 5. 实时看启动日志 tail -f $CATALINA_HOME/logs/catalina.out

JAVA_HOME 没配的话,startup.sh 会报找不到 java 命令。这不是 Tomcat 的问题,是环境变量的问题。CATALINA_HOME 指向 Tomcat 解压目录。Windows 上对应的是 startup.bat,路径要写到 bin 目录,其他一致。WAR 包丢进 webapps 后,Tomcat 启动时会自动解压。访问地址是 http://localhost:8080/library/,注意是 WAR 包文件名作为上下文路径,不是 localhost:8080 直接访问。

打包这一步,IDEA 里双击 Maven 面板的 package 也行,但命令行更直观。-DskipTests的意思是跳过测试但保留编译,如果测试类里连了不存在的测试库,不加这个参数会被卡在测试阶段。

5.2 Tomcat 9 + JDK 8 + MySQL 8 的兼容参数

组合选型上,JDK 8、Tomcat 9、MySQL 8.0 是最稳的一组,JDK 8 是 Tomcat 9 官方支持的最低版本,MySQL 8 的驱动对 JDK 8 也没有兼容问题。Spring 5.3.x 对 JDK 8 是完整支持的,不要手贱去升 Spring 6.x,那套是强制 JDK 17 的。

部署前检查四个地方:

检查项推荐配置说明
MySQL 驱动8.0.x用 5.1.x 连 MySQL 8 会报 SSL/认证错误
JDBC URL 参数useSSL=false&serverTimezone=Asia/Shanghai缺一不可,缺哪个报哪个错
Tomcat 内存set JAVA_OPTS=-Xms512m -Xmx1024m默认堆太小,项目多了容易 OOM
数据库连接池maxActive=20, initialSize=5按 Tomcat 最大线程数一半起步

Tomcat 内存调整不要改 catalina.sh 里的默认值,而是在bin/setenv.sh里写,Tomcat 启动时会自动加载,升级 Tomcat 不会丢配置。Windows 对应 setenv.bat。

5.3 高频问题逐一定位:连接失败、静态资源 404、中文乱码、映射报错、部署 404

问题一:数据库连接失败,时区报错

现象:Tomcat 启动日志里出现The server time zone value '�й���ʱ��' is unrecognized,或者Public Key Retrieval is not allowed。

原因:MySQL 8.0 的 JDBC 驱动要求 URL 里显式指定时区,而旧的 5.x 驱动不会检查这个。如果是 Public Key 报错,是 caching_sha2_password 认证插件在非 SSL 连接下需要客户端先拉取公钥。

解决:把 db.properties 里的 url 改成:

jdbc.url=jdbc:mysql://localhost:3306/library_system?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true

改完重启 Tomcat,同时用命令行mysql -u root -p确认数据库能连,排除数据库本身没启动的情况。

问题二:页面能开,但 CSS、JS 全部 404

现象:HTML 和 JSP 能正常渲染,控制台一堆 404,全是 /static/css/style.css 这类请求。

原因:DispatcherServlet 的 url-pattern 配置成了/,把 Tomcat 默认的静态资源处理器盖掉了,SpringMVC 接住了所有请求却不知道去哪找静态文件。

解决:在 spring-mvc.xml 里加一行静态资源放行:

<mvc:resources mapping="/static/**" location="/static/"/>

另一种规避思路是把 DispatcherServlet 的 url-pattern 改成*.do,比如/login.do,静态请求就不会被拦截。这个方案在老项目里很常见,但新代码不太推荐,RESTful 风格下 URL 会很难看。

问题三:插入的中文变问号

现象:从页面注册的用户中文名字进数据库变成??,或者 JSP 页面显示乱码。

原因:字符集问题不是单点的,三个环节要么全对,要么全错。数据库表不是 utf8mb4、JDBC URL 没带 characterEncoding=utf8、Tomcat Connector 没设置 URIEncoding,任缺其一就会出现。

解决:数据库执行ALTER DATABASE library_system CHARACTER SET utf8mb4;确认 URL 带 characterEncoding=utf8,再去 server.xml 的 Connector 节点加:

<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8"/>

顺序先查数据库表结构,再看 URL,最后才去动 Tomcat,大多数情况是前两个就解决了。

问题四:SQL 能查到,但 Java 实体全是 null

现象:MyBatis 执行 UPDATE 或 SELECT 后,返回的对象里 userName 是 null,但数据库里 user_name 有值。

原因:MyBatis 默认不做下划线到驼峰的映射,user_name 不会自动转成 userName,需要你逐个字段写 resultMap。

解决:在 SqlSessionFactoryBean 里开启全局驼峰映射,前面 4.2 小节已经写过了。如果你用的是 MyBatis 独立配置文件,就在 mybatis-config.xml 里加:

<settings> <setting name="mapUnderscoreToCamelCase" value="true"/> </settings>

改完重启,绝大多数属性 null 问题直接消失。只有列名和属性名完全对不上的极端情况,才需要手写 resultMap。

问题五:WAR 放进 webapps 后访问 404

现象:Tomcat 启动没报错,webapps 下也生成了解压目录,但访问http://localhost:8080/library/还是 404。

原因:最常见是 WAR 包文件名和你访问的路径不一致,比如包名叫 library.war,却访问 /book/list;还有可能是 web.xml 里 DispatcherServlet 配置有问题,导致 SpringMVC 没被初始化,页面请求全部落空;端口被占用也会让 Tomcat 起了一个实例但 webapps 加载失败。

解决:先看 catalina.out 里有没有Deployment of web application directory ... has finished这一行,没有说明部署失败,往上翻找异常。再用netstat -ano | findstr 8080查端口占用,同时确认浏览器地址是http://localhost:8080/library/而不是http://localhost:8080/。项目上下文路径永远等于 WAR 文件名。

6. 进阶:分页、拦截器与日志,让系统不再像课程作业

基础 CRUD 跑通只是第一步,图书列表几十条还好,几百条数据全塞一页,页面就开始卡了。接一个 PageHelper 分页是最快的方案。pom.xml 加依赖后再调 Service 方法:

<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.3.3</version> </dependency>
PageHelper.startPage(pageNum, pageSize); List<Book> books = bookMapper.selectAll(); PageInfo<Book> pageInfo = new PageInfo<>(books); model.addAttribute("pageInfo", pageInfo);

但 PageHelper 有个必须记住的底线:startPage 后面必须紧跟第一条查询语句,中间不能夹杂查询逻辑,否则分页会作用在错误的 SQL 上。这是 PageHelper 老使用者普遍踩过的坑,效果非常隐蔽,数量对但内容错。

拦截器是第二个体面功能。借书、还书、管理页面都要登录态,每个 Controller 里手动判断 session 太啰嗦。一个 HandlerInterceptor 就能统管:

public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (request.getSession().getAttribute("currentUser") == null) { response.sendRedirect(request.getContextPath() + "/login"); return false; } return true; } }

spring-mvc.xml 里配置拦截范围,登录接口和静态资源要放行,不然用户连登录页面都进不去,因为登录页的 CSS 也被拦了:

<mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/**"/> <mvc:exclude-mapping path="/login"/> <mvc:exclude-mapping path="/static/**"/> <bean class="com.library.interceptor.LoginInterceptor"/> </mvc:interceptor> </mvc:interceptors>

最后一个实用习惯是日志。System.out.println 在课程设计里无所谓,但部署到服务器后终端输出无时无刻全是废话,排查问题时真正有用的信息被淹没了。给 Service 层加 SLF4J,把关键节点用 warn 或 info 输出:

private static final Logger logger = LoggerFactory.getLogger(BorrowServiceImpl.class); logger.info("用户 {} 借阅图书 {},当前库存剩 {} 本", userId, bookId, book.getStock());

日志级别要克制,不要每个字段都打一行,一次借书打一条信息日志,一条异常日志带上堆栈,就足够了。我自己的习惯是:所有入口方法打 info,所有 catch 到异常的打 error 并附上 requestId 或 userId,这样定位问题时,直接 grep 用户 ID 就能搜出整条操作链路。

这套做法,从课程设计一路用到生产环境,核心原则从来没变过:数据库约束负责兜底,事务保证一致性,日志记录发生了什么,拦截器守住入口。把这四样做齐,你的系统就不再仅仅是一个能跑的课程作业。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询