很多人学 Java,语法和框架知识背得头头是道,但真要独立从零做一个能跑起来的完整项目,往往心里发虚。我带的几批零基础学员里,十个有八个卡在“不知道从哪里开始写第一行代码”。如果只能选一个项目来练手破局,我始终推荐图书管理系统。这个项目业务模型清晰、功能闭环完整,从数据库建表到后台接口再到前端页面,每一步都能落地,不像电商系统那样涉及太多复杂状态,也不像纯 CRUD 项目那样没有味道。这篇博文会把整个项目的完整流程拆给你看,从环境准备、数据库设计、核心业务代码,到前端页面、打包部署、踩坑排查,全部覆盖,照着一步步做,你就能拥有一个拿得出手的 Java 实战项目。
1. 项目概述:为什么图书管理系统是最合适的入门实战项目
先说清楚这个项目到底练了什么。图书管理系统的核心业务就是围着“书”和“人”转:用户登录、注册,管理员维护图书信息,读者可以借书、还书,借出去的书还能查询记录。听起来简单,但它天然包含了软件开发中最常用的知识点:三层架构、数据库设计、增删改查、分页搜索、登录会话、事务控制、接口封装。这些能力是后期上手任何 Java 后端工作的基础,没跑通过一个完整项目,后面学什么框架都像在沙子上盖楼。
市面上有不少图书管理项目教程,有的用 JSP + Servlet 手写,有的用 Spring Boot + MyBatis。这两条技术路线我都有实际经验,简单对比一下:
| 技术方案 | 上手难度 | 学习价值 | 就业匹配度 | 开发效率 |
|---|---|---|---|---|
| JSP + Servlet + JDBC | 较低 | 理解底层原理好 | 较低,已非主流 | 低,代码冗余多 |
| Spring Boot + MyBatis + Thymeleaf | 中等 | 贴近真实开发 | 高,主流技术栈 | 高,企业常用 |
有些人觉得零基础就该从 JSP 开始,我不同意。JSP + Servlet 能帮你理解 HTTP 请求和 MVC 的底层逻辑,但它的页面和代码耦合严重,写起来啰嗦,而且现在大部分公司都不再使用这老套方案。Spring Boot 看似“框架重”,其实恰恰是因为它把配置自动化了,新手反而省心。我的建议是:把 Servlet 和 Filter 作为前置概念了解一下,动手项目直接用 Spring Boot + MyBatis + Thymeleaf。这个组合既能让你掌握实际工作中的主流技术栈,又不会因为引入 Vue 这类前端框架导致前后端分离的学习曲线太陡。
在开始前的知识准备方面,完全不写 Java 代码就直接上手项目我也见过不少人尝试,结果往往在报错中崩溃。至少需要掌握:变量和类型、条件判断、循环、数组和集合、类与对象、方法调用。不需要精通,能看懂“定义一个实体类,创建 List 往里面放对象,再用 for 循环取值”就够用了。会用 MySQL 建库建表、会写基本的 SELECT、INSERT、UPDATE、DELETE 语句,再加上一点点 HTML 知识,就可以开干。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与工程起步:本地开发环境搭建的完整步骤
工欲善其事,必先利其器。这一步虽然没什么技术含量,但环境不一致导致的坑能让你怀疑人生。我建议统一用下面的版本组合:JDK 8 或 JDK 17,IDEA 社区版或专业版都行(社区版免费够用),Maven 3.6 以上,MySQL 8.x,数据库管理工具用 Navicat 或者 MySQL Workbench。
JDK 安装后记得配好 JAVA_HOME 环境变量,命令行里输入 java -version 能看到版本号才算配好。IDEA 里新建项目时选择 Spring Initializr,如果网络允许就直接用官方脚手架,网络不行的话把 Server URL 换成阿里云的镜像地址 https://start.aliyun.com。填写包名的时候别乱起,用 com.library 这种规范写法,这关系到后面代码的包结构。依赖勾选上 Spring Web、Thymeleaf、MyBatis Framework、MySQL Driver、Lombok。注意,MyBatis Framework 在依赖列表里叫这个名字,别选成 MyBatis Generator。
依赖选好之后,Maven 会开始下载一大堆 jar 包。这一步国内网络经常卡死,解决办法是在 Maven 的 settings.xml 里配置国内镜像源。我用的是阿里云镜像,配置好之后下载速度能提升非常多。IDEA 里如果下载失败,就执行 mvn -U clean install 强制更新。新手在依赖上花的时间是值得的,因为后面写代码时所有报错,大多数都能归结为“某个 jar 没下全”。
工程建好后,最重要的是理解目录结构。src/main/java 下放 Java 代码,按 controller、service、mapper、entity 分包,src/main/resources 下放配置文件和静态资源,templates 下放 Thymeleaf 模板页面。这就是三层架构的物理体现:Controller 接收请求,Service 处理业务逻辑,Mapper 操作数据库。刚入门的人总喜欢把业务代码全写在 Controller 里,图省事,后期维护的时候会非常痛苦,我后面会详细讲为什么必须分层。
pom.xml 里的关键依赖大概是这样一个结构,可以直接参考:
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.7</version>
</dependency>
最后一个 PageHelper 是后面分页要用的插件,新手可能不熟悉,记住它的作用就行了:帮我们把分页 SQL 自动拼好,省得手写 LIMIT。
application.yml 配置是项目能连上数据库的关键,下面这份是经过实测的:
yaml复制server:
port: 8080
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/library?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: yourpassword
thymeleaf:
cache: false
mybatis:
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
注意两个细节:JDBC 连接串里 serverTimezone=Asia/Shanghai 是必须的,不加的话 MySQL 8 会报时区错误;map-underscore-to-camel-case: true 的意思是数据库字段 user_name 可以自动映射到实体类的 userName,少写一堆 ResultMap。log-impl 配置了 StdOutImpl 后,控制台会打印实际执行的 SQL,项目调试阶段离不开它。
3. 数据库设计与初始化:用四张表支撑起整个业务闭环
数据库设计是图书管理系统里最重要的一步,很多人项目写到一半推翻重来,就是因为建表时没想清楚。我给出的方案是基于真实开发习惯优化的,只用了四张核心表:用户表、图书分类表、图书信息表、借阅记录表。
用户表设计如下:
sql复制CREATE TABLE `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`username` varchar(50) NOT NULL COMMENT '用户名',
`password` varchar(64) NOT NULL COMMENT '密码',
`real_name` varchar(50) DEFAULT NULL COMMENT '姓名',
`phone` varchar(20) DEFAULT NULL COMMENT '手机号',
`role` tinyint(4) NOT NULL DEFAULT '1' COMMENT '角色:0管理员,1普通用户',
`status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:1正常,0禁用',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
分类表就简单了,三个字段:id、分类名、排序号。不把分类直接设计成图书表里的一个字符串字段,是为了避免“同一本书在不同记录里分类叫法不一致”的问题。图书信息表是核心,SQL 如下:
sql复制CREATE TABLE `book` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`isbn` varchar(30) DEFAULT NULL COMMENT '国际标准书号',
`name` varchar(200) NOT NULL COMMENT '书名',
`author` varchar(100) DEFAULT NULL COMMENT '作者',
`publisher` varchar(100) DEFAULT NULL COMMENT '出版社',
`type_id` bigint(20) DEFAULT NULL COMMENT '分类ID',
`price` decimal(10,2) DEFAULT NULL COMMENT '价格',
`total` int(11) NOT NULL DEFAULT '0' COMMENT '总藏书量',
`stock` int(11) NOT NULL DEFAULT '0' COMMENT '当前可借库存',
`cover` varchar(255) DEFAULT NULL COMMENT '封面图路径',
`deleted` tinyint(4) NOT NULL DEFAULT '0' COMMENT '逻辑删除:0未删,1已删',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='图书表';
这里有两个设计上的考虑值得说一说。第一,total 和 stock 分开存,总藏书量是不变的,可借库存会随借还书增减,还书时 stock + 1,total 不会被动到,便于统计全馆藏书量。第二,删除图书用的是 deleted 字段做逻辑删除而不是 DELETE 语句,这是企业开发的通用做法,好处是历史借阅记录不会因为毁尸灭迹而断裂,而且万一删错了还能恢复。
借阅记录表是业务闭环的关键,设计如下:
sql复制CREATE TABLE `borrow_record` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NOT NULL COMMENT '借书用户ID',
`book_id` bigint(20) NOT NULL COMMENT '图书ID',
`borrow_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '借书时间',
`due_time` datetime DEFAULT NULL COMMENT '应还时间',
`return_time` datetime DEFAULT NULL COMMENT '实际归还时间',
`status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态:0借出,1已还',
`renew_count` int(11) NOT NULL DEFAULT '0' COMMENT '续借次数',
PRIMARY KEY (`id`),
KEY `idx_user_book` (`user_id`, `book_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='借阅记录表';
借书时生成一条状态为 0 的记录,还书时 update 这条记录的 return_time 和 status。有了用户和时间字段,后续要查某个读者借过什么书、某本书被借过几次都非常方便。
关于外键问题,我明确说一下:数据库物理外键在真实的业务系统里很少建,原因是在高并发环境下,外键约束会让删除和更新操作不得不去检查关联表,锁竞争一大就慢,而且后期分库分表物理外键完全没法用。所以表与表之间的关联关系,通常由应用层在 Service 里保证。比如借书前先查用户 ID 是否存在、图书 ID 是否存在,对应关系由业务代码校验。这个做法新手可能觉得不踏实,但你以后进公司会发现大都是这个套路。
4. 核心业务代码实现:登录、图书管理、借还书流程的关键写法
4.1 登录与拦截器:会话管理如何做才不漏气
用户登录是所有功能的入口。我推荐的登录实现是:登录成功后在 session 中保存一个 userInfo 对象,后续其他接口通过拦截器判断 session 里有没有这个对象。这样的好处是简单直观,适合做单服务器的小型系统,而且学习价值高,能让你理解会话的本质。
Controller 层登录方法的核心代码可以这样写:
java复制@PostMapping("/login")
public String login(String username, String password, String code, HttpSession session, Model model) {
User user = userService.login(username, password);
if (user == null) {
model.addAttribute("error", "用户名或密码错误");
return "login";
}
session.setAttribute("userInfo", user);
return "redirect:/book/list";
}
service 层做真正的校验,查库时记得密码不能明文存储。项目演示时我用的是 MD5 加盐,但在生产环境,更推荐 BCrypt 加密方式。实际上现在新的 Spring Security 自带 BCryptPasswordEncoder,哪怕只是练手项目,也建议尽早接触。
拦截器是所有登录校验的核心,注册一个 WebMvcConfigurer:
java复制@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/login", "/doLogin", "/register", "/css/**", "/js/**", "/images/**", "/error");
}
新手最容易在这块出错的地方是:没有排除静态资源的路径。不加排除的话,页面引用的 CSS、JS 文件也会被拦截器拦住,明明登录成功了页面却样式全丢,控制台还报一堆 302 重定向。我在项目里专门给学员强调过,/css/**、/js/** 这种静态路径必须放行。
4.2 图书增删改查:分页、搜索和逻辑删除的通用写法
图书列表页是最典型的列表查询场景,里面包含了分页和搜索两个高频功能点。我用了 PageHelper 插件,示例代码很简洁:
java复制public PageInfo<Book> searchBooks(String keyword, Long typeId, Integer pageNum, Integer pageSize) {
PageHelper.startPage(pageNum, pageSize);
List<Book> books = bookMapper.searchBooks(keyword, typeId);
return new PageInfo<>(books);
}
使用 PageHelper 有一个非常重要的前提:PageHelper.startPage(pageNum, pageSize) 这一行之后必须紧接着写你的查询代码,中间不能有任何别的数据库查询操作,否则分页就串到别的 SQL 上去了。这个坑我踩过不止一次,查出来的数据总是不对,最后才发现原来是 two 个查询之间夹了一句日志打印触发了数据库访问。
对应的 Mapper XML 里,核心查询 SQL 长这样:
xml复制<select id="searchBooks" resultType="com.library.entity.Book">
select id, isbn, name, author, publisher, type_id, price, total, stock
from book
where deleted = 0
<if test="keyword != null and keyword != ''">
and (name like concat('%', #{keyword}, '%')
or author like concat('%', #{keyword}, '%')
or isbn like concat('%', #{keyword}, '%'))
</if>
<if test="typeId != null">
and type_id = #{typeId}
</if>
order by id desc
</select>
关键词搜索用 LIKE,这里把书名、作者、ISBN 三个字段都参与匹配,一个输入框就能覆盖常见的查询需求。注意 concat('%', #{keyword}, '%') 这种写法,是因为 #{keyword} 是预编译参数,不能直接拼在字符串里。
新增和编辑功能可以共用一个表单页面,新增时所有字段为空,编辑时后端把图书信息回填到 model 里,前端 Thymeleaf 自动渲染初始值:
java复制@GetMapping("/book/edit/{id}")
public String edit(@PathVariable Long id, Model model) {
model.addAttribute("book", bookService.getById(id));
model.addAttribute("typeList", typeService.listAll());
return "book/form";
}
删除操作走的是逻辑删除,一行 update 搞定:
java复制@PostMapping("/book/delete/{id}")
public String delete(@PathVariable Long id) {
bookService.logicDelete(id);
return "redirect:/book/list";
}
Service 层实现里就是 update book set deleted = 1 where id = #{id},这样做的意义在前面数据库设计部分详细说过了,核心就一句:保护历史数据。
4.3 借书与还书:事务和并发控制才是精髓
借书是整个项目业务上最复杂的一环。表面上是插一条记录、减一个库存,但实际要考虑的情况非常多:读者是否已登录、书是否存在、库存是否大于 0、同一个读者是否重复借阅同一本书。我把 Service 层借书逻辑完整贴出来,这是整套代码的核心精华:
java复制@Transactional(rollbackFor = Exception.class)
public void borrowBook(Long userId, Long bookId) {
User user = userMapper.selectByPrimaryKey(userId);
if (user == null || user.getStatus() != 1) {
throw new RuntimeException("用户不存在或已被禁用");
}
Book book = bookMapper.selectById(bookId);
if (book == null || book.getDeleted() == 1) {
throw new RuntimeException("图书不存在");
}
if (book.getStock() <= 0) {
throw new RuntimeException("库存不足");
}
int count = borrowRecordMapper.countBorrowing(userId, bookId);
if (count > 0) {
throw new RuntimeException("你已借过这本书,请先归还");
}
borrowRecordMapper.insert(new BorrowRecord(userId, bookId, new Date()));
int rows = bookMapper.decreaseStock(bookId);
if (rows == 0) {
throw new RuntimeException("库存扣减失败");
}
}
@Transactional 注解保证了整个方法是一个事务:插入借阅记录和扣减库存这两个操作,要么都成功,要么都失败。没有事务控制的话,万一插入记录成功但扣库存失败,数据就彻底对不上了。rollbackFor = Exception.class 表示任何异常都触发回滚,这个参数尽量写上,因为 Spring 默认只对运行时异常回滚,对检查异常不回滚。
我要特别提醒一个事务失效的坑:同一个类里,A 方法调用 B 方法,B 方法上面写了 @Transactional 也不会生效。因为 Spring 事务是基于代理实现的,通过 this 直接调用内部方法,代理根本拦不到。所以如果你在 Controller 里直接调用了 BookService.borrowBook,但 borrowBook 内部又调用了同类中的另一个事务方法,别指望内部方法有事务保护。解决办法是把需要事务保护的方法放到另一个 Service 里,或者在自己的 Service 里注入自己。这个坑很多工作两年的开发都未必讲得清楚,面试问事务失效场景高频出现。
还书流程逻辑上刚好是借书的逆操作:
java复制@Transactional(rollbackFor = Exception.class)
public void returnBook(Long recordId) {
BorrowRecord record = borrowRecordMapper.selectById(recordId);
if (record == null || record.getStatus() == 1) {
throw new RuntimeException("借阅记录不存在或已归还");
}
record.setReturnTime(new Date());
record.setStatus(1);
borrowRecordMapper.updateStatus(record);
bookMapper.increaseStock(record.getBookId());
}
还书时计算是否逾期,是在还书这一刻实时计算的,根据 due_time 和当前时间比较得出,不需要每天跑定时任务去改数据。这也是一个常见的设计选择:能实时算的就不提前落地存字段。
5. 前端页面与交互效果优化:Thymeleaf 模板渲染的关键细节
图书管理系统的前端是用 Thymeleaf 模板引擎 + Bootstrap 开发的。为什么不推荐传统的 JSP?因为 Spring Boot 对 JSP 的支持很别扭,能不用就不用。Thymeleaf 的语法和 HTML 天然兼容,浏览器里直接打开页面能看到静态设计,各种标签属性只是在服务端渲染时才生效。
页面结构上,我用 th:fragment 抽取公共部分。所有后台页面都有顶部导航栏和左侧菜单,如果每个页面都复制一遍,改一个菜单名得改全部页面,处于维护噩梦。公共代码放在 common.html 中:
html复制<div th:fragment="header">
<nav class="navbar navbar-default">...</nav>
</div>
其他页面通过 <div th:replace="common :: header"></div> 引入,这是模板引擎最实用的功能,能省掉大量重复代码。
列表页的开发核心是列表循环和分页。Thymeleaf 的写法如下:
html复制<tr th:each="book : ${pageInfo.list}">
<td th:text="${book.id}"></td>
<td th:text="${book.name}"></td>
<td th:text="${book.author}"></td>
<td th:text="${book.stock}"></td>
</tr>
分页区域的样式,我用 Bootstrap 的分页组件,关键是要保留搜索参数。比如当前在搜索“三体”这个关键字还选了分类,翻到第二页时 URL 要带上 keyword=三体&typeId=2,否则分页一翻关键字就丢了。手工拼接链接时注意这一点,否则用户一翻页就感觉“搜索失效了”,很多新手项目都死在这个细节上。
表单页上,新增和编辑共用同一个 form.html,提交路径根据 book.id 是否为空动态拼接。前端表单校验我做了一层:借书时如果库存为 0,按钮直接置灰;搜索框如果想清空直接提交空字符串即可。但这个只能算用户体验优化,真正的防御必须做在后端。所有后端接口不只是校验“参数非空”,还要校验业务状态。
这个项目我没有采用前后端分离,因为零基础阶段再引入 Vue 全家桶和跨域处理,学习成本突然翻倍。等你把图书管理系统这套 Java 后端逻辑跑顺了,前端单独用 Vue 重写一遍是一个绝佳的进阶练习题目,到时候你会发现后端接口基本不用改,只是把返回 JSON 就行。
6. 常见问题与排查技巧实录:新手最容易踩的坑都在这
项目做完之后,没遇到过报错是不可能的。我的经验是:报错不可怕,可怕的是不会看报错。下面把这些年带项目时最常见的坑整理成一个速查表,建议收藏放在手边。
| 报错现象 | 根本原因 | 解决方法 |
|---|---|---|
| 启动时报 Failed to configure a DataSource | 没有配置数据源或配置了但数据库没启动 | 确认 application.yml 里 datasource 正确,确认 MySQL 服务已启动 |
| java.sql.SQLException: The server time zone value | JDBC 连接串没加时区参数 | 在 url 后加 serverTimezone=Asia/Shanghai |
| 页面乱码 | 数据库连接串缺少 characterEncoding | 在 url 里加 useUnicode=true&characterEncoding=utf8 |
| Invalid bound statement (not found) | Mapper 接口和 XML 没有正确绑定 | 检查 mapper-locations 配置路径是否正确,XML 的 namespace 是否等于接口全限定名 |
| 引入 Lombok 后找不到 get/set 方法 | 缺少注解处理器或依赖 | 确认依赖已引入,IDEA 安装 Lombok 插件,开启 Annotation Processing |
| 端口 8080 被占用 | 其他程序占了端口 | 改 server.port,或查占用进程杀掉 |
| 静态资源访问 404 | 拦截器拦截了静态路径或资源位置放错 | 静态资源放 static 目录,拦截器排除 /css/** 等路径 |
| 从后台保存中文数据变问号 | 数据库连接、数据库表字符集、页面编码三重问题 | 统一所有环节使用 utf8mb4,确认 yml 配置 |
其中,Mapper XML 绑定失败这个错,出现的频率极高。很多人写完 Mapper 接口和 XML 文件,一启动就报 Invalid bound statement (not found)。我要强调一下,XML 文件里 mapper 标签的 namespace 属性和接口全限定名必须完全一致,一个字母都不能差,而且方法名要和 XML 里的 id 对应。IDEA 里如果 mapper 文件在 java 目录下面但没被当作资源文件编译进去,要检查 pom.xml 里是否缺少 resources 配置。
SQL 调试的技巧我单独说一下。配置里开了 StdOutImpl 日志后,控制台会打印 MyBatis 执行的所有 SQL。很多人看到 SQL 打出来了但结果不对,就直接昏头。我的做法是把控制台打印出的完整 SQL 复制到 Navicat 里手动跑一遍,如果手动跑结果对,那问题就在传参或映射;如果手动跑结果都不对,那肯定是 SQL 本身写错了。这种“剥离框架直接在数据库里验证”的排查思路,效率比在代码里盲猜高十倍。
还有一类问题最容易让人崩溃,就是“本地好好的,换台电脑就炸”,最常见的是 MySQL 版本不同导致的驱动问题。MySQL 5.x 和 8.x 的驱动类名不一样,5.x 用 com.mysql.jdbc.Driver,8.x 用 com.mysql.cj.jdbc.Driver。如果项目在别人电脑上运行为什么报驱动找不到,记得先去检查对方 MySQL 版本,然后再看 pom 里依赖的 mysql-connector-j 版本是不是匹配。这种问题不是代码写得不对,而是环境差异,新手一定要学会看关键报错信息,特别是 Caused by 那一行。
7. 打包部署与项目进阶扩展:把练手项目变成真正能展示的作品
项目在本地能跑还不够,能打成一个可执行的 jar 包运行在云服务器上,才算完整的闭环。Spring Boot 项目打包非常简单:
bash复制mvn clean package -DskipTests
执行完,target 目录下会生成一个 library-0.0.1-SNAPSHOT.jar,直接上传到服务器上,用 java -jar library-0.0.1-SNAPSHOT.jar 就能启动。服务器上需要提前装好 JDK 和 MySQL,并修改 application.yml 里的数据库连接为线上地址。如果怕改动配置影响本地运行,可以配合 Spring Boot 的多环境配置文件,application-dev.yml 和 application-prod.yml,启动时通过 --spring.profiles.active=prod 指定用哪套配置。
打包部署环节最容易踩的坑主要有两个:一是云服务器的安全组或防火墙没放行 8080 端口,导致本地访问不到;二是线上数据库的初始化脚本没跑,一登录就报“表不存在”。建议部署时把建表 SQL 脚本也同步传到服务器,在 MySQL 里执行一遍,顺便验证线上环境数据库连接。
项目做完之后,如果按能力提升想让这个项目更上一个台阶,我有几条明确的进阶建议:
第一,把登录态从 Session 改成 JWT。Session 有状态,依赖服务器保存会话数据,不利于多机部署;JWT 无状态,把用户信息加密放在客户端 token 里,服务器不需要保存任何东西。做这个改动的时候,你顺带会理解什么是无状态认证、什么是拦截器、什么是过滤器,这些都是面试后端岗的高频区。
第二,引入 Redis 做缓存。图书列表页的数据变化不频繁,却每次请求都查数据库,完全可以缓存起来。给系统加上 Redis 之后,你能真真切切感受到缓存命中带来的性能提升,也能学习到缓存和数据库的一致性问题怎么处理。
第三,用 MyBatis-Plus 替代原生 MyBatis。在这个项目里你手写过 CRUD、写过 XML,已经理解了 MyBatis 的底层工作方式。这时候换成 MyBatis-Plus 体验一下代码自动生成的效率提升,再对比手写和自动的区别,会对“框架究竟帮我们省了什么”有更深的理解。
第四,给项目补充单元测试。哪怕只是简单的 Controller 层 MockMvc 测试和 Service 层事务回滚测试,都会让你后面的工作轻松很多。真实项目中测试代码的量和业务代码不相上下,越早养成写测试的习惯越好。
我在实际带项目时发现,做完成一个项目后再往上面加功能,是最容易让人“从会写到会设计”的环节。很多同学项目能跑就不动了,其实没有榨干这个项目的学习价值。建议你至少再给它加上一个“统计图表”功能,比如展示各分类藏书量的柱状图,后端查数据、前端绘图表,这个扩展会让你重新审视整个系统的扩展性,把前面学的所有东西再融会贯通一遍。
图书管理项目做完,你动手写代码的底气会完全不一样。它就像学车时的科目二,场地不大,但是倒库、侧方位、直角转弯这些基本功全都有了。你要是卡在环境配置上,就按第 2 节再走一遍;要是卡在业务代码上,就直接抄第 4 节的逻辑自己重写一遍。项目这个东西,自己动手敲过一遍,才真正长在自己身上。
