news 2026/10/6 3:07:32

Java后端实战:图书管理项目从零搭建,Spring Boot+MyBatis+MySQL全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java后端实战:图书管理项目从零搭建,Spring Boot+MyBatis+MySQL全解析

还在为简历上没有能拿得出手的项目发愁?或者学完Java基础语法,翻开Spring Boot的书却一头雾水?我特别建议你从图书管理项目入手。这个看起来有点“土”的项目,恰好踩在了所有Java后端核心知识点的交汇处——数据库设计、持久层框架、业务逻辑、前端渲染,一个都不少。我回想自己刚入行时,也是靠它把一个个零散的知识点串成了线,后来带新人也喜欢让他们从头写一遍图书管理。这篇文章就把整个项目从设计到落地的完整过程拆开讲清楚,包括每一步的思考、踩过的坑,以及那些网上教程不会明确告诉你的细节。想用这个项目入门,或者拿它当面试素材的朋友,照着做就对了。

1. 项目整体设计与技术栈选型

1.1 为什么图书管理项目是Java实战的最佳切入点

很多准备转行或者刚毕业的同学喜欢一上来就挑战“仿电商系统”“秒杀系统”,结果写到一半就被库存扣减、分布式锁、消息队列这些概念劝退了。图书管理项目之所以经典,是因为它的业务模型足够简单,但技术链路又足够完整。读者、图书、借阅记录这三者之间的关系,可以很好地映射到数据库的表关联上;而借书、还书这两个核心动作,天然适合用来演示事务控制;再加上图书的增删改查、分页搜索,几乎覆盖了一个后端岗位日常80%的工作场景。

更关键的是,这个项目有非常清晰的“完成标准”。图书列表能展示、能搜索、能分页,图书能新增能编辑能删除,读者能借书能还书,登录后才能操作——做到这些,你就已经具备了一个初级后端开发的完整思维框架。不要小看这些CRUD(增删改查)操作,很多看起来的高并发系统,拆到底层业务单元,落实在代码上依然是CRUD加上更复杂的并发控制和缓存策略。但那些复杂策略是后话,先把CRUD写顺、写出规范,才是正经事。

1.2 技术栈选型:Spring Boot + MyBatis + MySQL的搭配逻辑

先解释一下技术栈的选择。我给零基础同学的建议是:Java 8或Java 17 + Spring Boot 2.x或3.x + MyBatis + MySQL + Thymeleaf。有人会问,为什么不学市面上更火的Spring Cloud微服务?这是我反复强调的一点:微服务是解决复杂系统拆分问题的,图书管理这个规模的项目用微服务属于典型的杀鸡用牛刀,而且会引入注册中心、网关、配置中心等一堆概念,让新手完全找不到重点。

Spring Boot的优势在于“约定大于配置”,它把Spring框架里大量繁琐的XML配置固化成了自动配置,你可以用极少的代码快速启动一个Web应用。MyBatis是国内企业使用率极高的持久层框架,它让你写SQL时能精确控制每一条语句,这对于刚接触数据库操作的同学来说,比Hibernate这类全自动ORM(对象关系映射)更容易理解底层发生了什么。MySQL就不用多说了,开源、免费、生态成熟,几乎每个公司都在用。至于前端选Thymeleaf而不是Vue或者React,是因为这个项目的重点是后端,Thymeleaf是服务端渲染模板,可以直接在HTML里通过表达式取到后端传过来的数据,学起来非常快。等你把后端的体系学明白了,再去做前后端分离,思路会清晰得多。

技术组件选型原因零基础友好度
Spring Boot自动配置、快速构建独立应用高
MyBatisSQL可控、贴近企业常见技术栈高
MySQL开源常用、资料丰富高
Thymeleaf后端渲染、学习曲线平缓高

2. 零基础环境准备与项目初始化

2.1 JDK、Maven、MySQL的安装与版本选择

环境这块是劝退新手的第一大坑,我在实际帮人排查问题时,发现很多项目跑不起来根本不是代码的问题,而是JDK版本和Spring Boot版本对不上。举个例子,Spring Boot 3.x要求JDK 17及以上,如果你电脑装的是JDK 8,创建项目时选了3.x版本,那编译就一定失败。所以我建议第一次做项目的同学直接统一用JDK 8 + Spring Boot 2.7.x,这个组合经过了大量生产环境的验证,资料最多,遇到的坑网上都有对应的解决方案。

安装JDK没什么特别的,去官方下载对应操作系统的安装包,配置两个环境变量就行。一个是JAVA_HOME,指向你的JDK安装目录;一个是PATH,添加%JAVA_HOME%\bin(Windows系统写法)。配完之后在命令行输入java -version,能看到版本信息就算成功。Maven的话,直接官网下载二进制压缩包,解压后同样配置MAVEN_HOME,注意Maven默认用的中央仓库在国外,下载依赖会很慢,一定要在settings.xml里配置阿里云镜像。MySQL安装稍微重一点,Windows下用安装包引导安装比较稳妥,要注意记住自己设置的root密码,后面项目连接数据库全靠它。

2.2 用Spring Initializr十分钟创建项目骨架

创建项目我这里推荐一个高效的方式:Spring Initializr。你可以在IDEA旗舰版直接新建项目时选择Spring Initializr,也可以用浏览器访问Spring官网的在线初始化页面。选择Maven工程,语言Java,版本按上面说的选Java 8,然后Spring Boot版本选2.7.x。依赖这里,勾选Spring Web、MyBatis Framework(注意不是MyBatis Spring Boot Starter这两个版本的官方命名变化)、MySQL Driver、Thymeleaf。点生成,下载下来直接解压用IDEA打开即可。

实际上我更推荐在IDEA里直接操作,因为社区版IDEA也可以配合在线Initializr创建。这里要提一个操作细节:IDEA加载完Maven项目后,右上角如果出现一个“Maven projects need to be imported”的提示,一定要点导入,不要跳过。第一次导入会下载大量依赖,墙内用户一定要确认Maven的settings.xml已经配了镜像,否则下载个半天还报错。导入完成后,可以在src/main/java下看到默认的启动类,类是打@SpringBootApplication注解的,此时直接运行它,如果能正常启动并且在日志里看到Tomcat started的提示,说明环境完全打通了。

2.3 配置application.yml:连接数据库的三处关键配置

项目骨架建好后,就要把资源目录下的application.properties改名为application.yml。推荐用YAML格式,因为它结构清晰,支持嵌套,比properties文件更容易阅读。核心是配置数据库连接。

spring: datasource: url: jdbc:mysql://localhost:3306/library?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 你自己的密码 driver-class-name: com.mysql.cj.jdbc.Driver thymeleaf: cache: false mvc: hiddenmethod: filter: enabled: true mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.library.entity configuration: map-underscore-to-camel-case: true server: port: 8080

这里有三处是新手最容易踩雷的。第一,URL里为什么一定要带characterEncoding=utf8?因为MySQL默认字符集在部分环境下不是UTF-8,不显式指定就会在处理中文时出现乱码。第二,serverTimezone=Asia/Shanghai是解决时报区问题的,不加这个参数,新版JDBC驱动会在连接时报“The server time zone value”的异常。第三,map-underscore-to-camel-case: true的意思是从数据库列名book_id自动映射到Java实体类字段bookId,这个配置能省掉大量写ResultMap映射的手动工作。至于hiddenmethod过滤器,它是为了让前端表单可以模拟PUT和DELETE请求,后面更新删除图书时会用到。

3. 数据库设计与持久层实现

3.1 四张核心表的结构设计思路

数据库是项目的根基,表结构设计得不合理,后面写代码会被反复折腾。图书管理项目我们设计四张表:图书表book、分类表category、读者表reader、借阅记录表borrow_record。为什么需要分类表?因为图书的分类信息如果直接塞在book表里的一个字符串字段中,那么想统计某个分类有多少本书就得写LIKE查询,性能差而且容易脏数据。把分类独立成一张表,图书通过category_id外键关联,这就是数据库设计里最基本的范式思想。

表结构设计要考虑几个点:字段类型选型要有分寸。比如价格字段,有些同学会用VARCHAR来存,查询时再转数值,这非常难受,应该直接用DECIMAL。我看过很多新手设计的表,书名用VARCHAR(255),ISBN也用VARCHAR(255),这就不是合格的设计。合理的做法是书名100个字符,ISBN 20个字符,够用且节省空间。库存字段stock设计成INT,默认值是0而不是NULL,要知道NULL参与算术运算会得到NULL,导致业务判断出错。时间字段统一用DATETIME,create_time和update_time加上,虽然手写很繁琐,但之后排查数据问题时会救命。

CREATE DATABASE IF NOT EXISTS library DEFAULT CHARACTER SET utf8mb4; USE library; CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL UNIQUE, description VARCHAR(200) ); CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(100) NOT NULL, author VARCHAR(50), isbn VARCHAR(20), category_id INT, stock INT DEFAULT 0, create_time DATETIME, update_time DATETIME, CONSTRAINT fk_book_category FOREIGN KEY (category_id) REFERENCES category(id) ); CREATE TABLE reader ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, nickname VARCHAR(50), phone VARCHAR(20), status TINYINT DEFAULT 1 ); CREATE TABLE borrow_record ( id INT PRIMARY KEY AUTO_INCREMENT, reader_id INT NOT NULL, book_id INT NOT NULL, borrow_time DATETIME, due_time DATETIME, return_time DATETIME, status TINYINT DEFAULT 0 COMMENT '0-未归还 1-已归还', CONSTRAINT fk_borrow_reader FOREIGN KEY (reader_id) REFERENCES reader(id), CONSTRAINT fk_borrow_book FOREIGN KEY (book_id) REFERENCES book(id) );

这里要特别说明utf8mb4这个字符集。MySQL里普通的utf8其实是utf8mb3,只能存3个字节的字符,像emoji这类4字节字符它就存不了。现在建库建表推荐一律用utf8mb4,和utf8对普通中文来说没有区别,但更全面。

3.2 实体类与Mapper层的映射细节

数据库表设计好了,接下来就是Java实体类。教大家一个非常实用的判断标准:实体类字段名和数据库列名要保持同一个命名风格——数据库是下划线风格,Java实体类是驼峰风格,然后靠MyBatis的map-underscore-to-camel-case配置自动完成映射。但有些同学会发现,自动映射之后查出来的对象字段依然是null,这时候要看是不是实体类里的字段没有setter和getter。我在帮人调代码时经常遇到,实体类里只定义了私有字段,Lombok的依赖没正常引入,或者IDEA没启用注解处理器,导致运行时所有字段都null。

这里分享一个经验:如果你用的是Lombok,一定要在IDEA中安装Lombok插件并勾选“Enable annotation processing”。不然代码编译期看着没问题,一运行全是空对象。不想用Lombok也没关系,就手动写getter和setter,IDE可以直接快捷键生成,没问题。

服务层代码示例——bookService里查图书列表:

@Service public class BookServiceImpl implements BookService { @Autowired private BookMapper bookMapper; @Override public PageResult<Book> getBookPage(int pageNum, int pageSize, String keyword) { int offset = (pageNum - 1) * pageSize; List<Book> bookList = bookMapper.selectPage(keyword, offset, pageSize); Long total = bookMapper.countPage(keyword); return new PageResult<>(bookList, total, pageNum, pageSize); } }

3.3 手写SQL还是用注解?MyBatis的两种玩法

MyBatis中执行SQL有两种方式:注解和XML映射文件。我的建议是,入门阶段哪怕用注解,也一定要尝试XML方式。因为注解写复杂SQL时会非常痛苦,动态SQL没法优雅地拼接,而且一旦需要调整SQL就要重新编译,不如XML灵活。XML中的动态SQL标签(比如<if>、<foreach>)是MyBatis的灵魂所在。

分页查询的SQL特别能说明这个问题。如果用LIMIT做分页,有两个参数:偏移量和每页条数。关键字搜索是可选的,没有关键字就查所有,有关键字就模糊匹配书名或作者。这种“可选条件”就得用动态SQL:

<select id="selectPage" resultType="com.example.library.entity.Book"> SELECT id, title, author, isbn, category_id AS categoryId, stock, create_time, update_time FROM book <where> <if test="keyword != null and keyword != ''"> title LIKE CONCAT('%', #{keyword}, '%') OR author LIKE CONCAT('%', #{keyword}, '%') </if> </where> ORDER BY id DESC LIMIT #{offset}, #{pageSize} </select>

用<where>标签的好处是会自动处理首行AND的问题。很多人不知道,你在写动态SQL时如果第一个条件前面加了AND,最终拼接出来SQL会变成WHERE AND title LIKE...,直接报语法错误。<where>标签会智能地剔除掉这个多余的AND,这是一个很容易踩的坑。

4. 核心业务功能实战实现

4.1 图书列表分页查询:前端+后端的完整链路

分页是这个项目里第一个完整闭环功能,我建议大家跟着这个思路走一遍。Controller接收页面传过来的pageNum(当前页)和pageSize(每页大小),默认值是第1页、每页5条。收到参数后调用Service,Service计算出偏移量,调用Mapper查询数据列表和总记录数,返回给Controller再放进Model里渲染到页面。

关键点在于“为什么要这样做分页”。数据库分页用LIMIT时,第pageNum页的数据是在第(pageNum - 1) × pageSize条记录之后的那几条。这个公式看着简单,却经常有同学写成pageNum × pageSize,导致永远跳不过第一页数据。总记录数也需要单独count一次,因为LIMIT查询结果不直接告诉你总共有多少条,而分页条组件需要总页数来显示页码。这一块逻辑理解了,不管以后换成PageHelper插件还是MyBatis-Plus,你都能明白它底层在做什么。

Controller层代码如下:

@Controller @RequestMapping("/book") public class BookController { @Autowired private BookService bookService; @GetMapping("/list") public String list(@RequestParam(defaultValue = "1") int pageNum, @RequestParam(defaultValue = "5") int pageSize, @RequestParam(required = false) String keyword, Model model) { PageResult<Book> pageResult = bookService.getBookPage(pageNum, pageSize, keyword); model.addAttribute("page", pageResult); model.addAttribute("keyword", keyword); return "book/list"; } }

4.2 借书还书:事务处理与业务规则校验

借书和还书是整个项目业务逻辑的核心,也是面试时最值得讲的亮点。很多人把借书写成了简单的INSERT INTO borrow_record,这是不合格的。真正的业务人员眼里,借书包含了三个动作:检查书是否存在且库存大于0,检查读者是否已借了同书未还,插入借阅记录并扣减库存。这三个动作必须全部成功,或者全部不做。比如管库的朋友只想扣库存,不想记录谁借了这本书,这就会造成账实不符。解决这个问题的方式就是数据库事务。

Spring的@Transactional注解就是用来做这个的。它背后的机制是AOP(面向切面编程),在执行方法前开启数据库事务,方法正常结束就会提交事务,方法抛出运行时异常就回滚所有已执行的SQL。注意,Spring默认只对RuntimeException回滚,如果方法抛出的受检异常(Exception子类)默认是不会触发回滚的。所以自定义业务异常时,最好继承RuntimeException,否则你辛辛苦苦写好的事务,异常一抛出去,数据照样是残缺的。

@Transactional(rollbackFor = Exception.class) public BorrowRecord borrowBook(Integer readerId, Integer bookId) { Book book = bookMapper.selectById(bookId); if (book == null || book.getStock() <= 0) { throw new ServiceException("图书不存在或库存不足"); } Long count = borrowRecordMapper.countUnreturned(readerId, bookId); if (count > 0) { throw new ServiceException("你已借过这本书,请先归还当前借阅"); } bookMapper.decreaseStock(bookId); BorrowRecord record = new BorrowRecord(); record.setReaderId(readerId); record.setBookId(bookId); record.setBorrowTime(LocalDateTime.now()); record.setDueTime(LocalDateTime.now().plusDays(30)); record.setStatus(0); borrowRecordMapper.insert(record); return record; }

上面这段代码演示了完整的借书逻辑,但在Web应用中@Transactional还有一个经常被忽略的问题——同一个类内部调用。假如这个方法在同一个Service类里被另一个方法直接调用,事务会失效,因为Spring的事务是通过代理对象实现的,类内部调用走的是this本身而不是代理。遇到事务不生效时,先检查自己是不是踩了这个点。

4.3 用户登录与Session管理

图书管理系统的操作应该是受保护的,所以登录功能必不可少。我通常建议用Session方式做,因为简单且足够符合这个项目的需求。用户提交用户名密码后,Service层查询数据库验证密码,匹配就放到Session里,之后每次请求都通过拦截器校验Session中有没有用户信息。

这里有个常见的安全规范:数据库里不能存明文密码。要使用哈希算法加盐存储,比如BCrypt。BCrypt每次生成的哈希值都不一样,因为它的算法内部自动带盐,所以就算两个用户的密码相同,数据库里存的哈希串也不同。Spring Security里有这个工具类,但我们不用引入整个Spring Security,可以用spring-security-crypto这个轻量依赖,足够用来做BCrypt加密验证。密码这块是面试高频考点,能把加密原理讲明白是加分项。

Session的有效期是默认的30分钟,有需要可以在application.yml里通过server.servlet.session.timeout配置。开发调试时这个配置烦人,可以把有效期调长一点,避免刚写好的功能因为Session过期被重定向到登录页。

4.4 Controller层设计:如何组织接口更清晰

很多零基础的同学写Controller时,会陷入“一个方法塞一堆逻辑”的误区。一个良好的Controller应该只负责接收参数、调用Service、返回结果,它不承载业务判断。比如新增图书,Controller只需要拿到Book对象并调用Service的addBook方法,而书名的非空校验、ISBN格式校验都应该放在Service里。这样做的好处是,以后同一个功能如果还要给移动端提供API,可以复用Service层的逻辑。

这里推荐一个实用的分层规范:Controller只定义路由和参数绑定,Service定义业务接口和实现,Mapper定义数据库操作。三层各司其职,一出问题时很容易定位。图书管理项目的接口路由设计,建议全部以资源名组织:

功能请求方式路径
图书列表GET/book/list
新增图书POST/book/add
修改图书POST/book/update
删除图书GET/book/delete?id=1
执行借书POST/borrow/add
执行还书POST/borrow/return

删除图书这里为什么会设计成GET请求?很多教程都是这么做的,因为用浏览器地址栏直接访问方便。但在生产级项目中,修改数据的请求不应该用GET,因为GET请求会被浏览器缓存,还可能被爬虫扫描。严谨的做法是用DELETE加上表单的_method参数。这里就回到之前hiddenmethod过滤器的作用,它让POST表单可以伪装成PUT或DELETE。面试时能说出这个细节,比尬聊八股文强得多。

5. 前端页面渲染:Thymeleaf与模板复用

5.1 Thymeleaf的常用语法:三分钟上手

前端页面是这个项目里比较容易忽略但又很重要的一环。有人问做Java后端是不是前端可以随便折腾一下就行了,我的看法是,你不需要做出精美的UI,但必须能搭建一个完整可用的页面,因为需求方要看到的效果是“点击按钮能出结果”。Thymeleaf就是围绕这个目标设计的。

Thymeleaf的核心语法就几个:th:text用来输出文本,th:each用来遍历集合,th:if用来做条件判断,th:href用来拼接链接。和数据展示最相关的是th:each,比如在图书列表页面遍历page.records:

<tr th:each="book : ${page.records}"> <td th:text="${book.id}">1</td> <td th:text="${book.title}">书名</td> <td th:text="${book.author}">作者</td> <td th:text="${book.isbn}">ISBN</td> <td th:text="${book.stock}">库存</td> <td> <a th:href="@{/borrow/add(bookId=${book.id})}">借阅</a> <a th:href="@{/book/delete(id=${book.id})}">删除</a> </td> </tr>

注意一个细节:th:href后面括号里的参数叫“表达式的基本语法”,它和拼接URL是不一样的。很多人都喜欢用字符串拼接'/book/delete?id=' + ${book.id},这样也能跑,但稍微有一点动态参数就很容易出小毛病。用@{/book/delete(id=${book.id})}的写法,Thymeleaf会帮你正确地处理参数编码,既干净又安全。

5.2 页面复用:公共导航栏与模板片段

写前端页面时,最烦人的是几个页面都有一模一样的导航栏和底部信息。Thymeleaf的公共模板片段就是解决这个问题的方案。在templates/common.html中写下导航栏的HTML代码,用th:fragment="navbar"标记,然后其他页面通过th:replace="common :: navbar"引入。这样改导航栏时只需要改一处,所有页面同步生效。

这个思路看起来简单,但它背后是“模板布局”的概念,在企业级项目中,无论是Freemarker还是前后端分离的Vue组件,都遵循类似的复用逻辑,所以学这个项目时养成分文件、复用的习惯,对以后面对更复杂的前端体系帮助很大。模板片段还可以传参数,比如导航栏高亮的菜单不同,可以传递一个active参数到片段中判断。

6. 常见问题与排查技巧实录

6.1 中文乱码:一个字符集问题引发的血案

如果你在页面提交中文数据后,存到MySQL里变成了乱码,十有八九是字符集没有统一。注意,字符集问题至少有三个环节:数据库表字符集、数据库连接URL的字符集参数、页面编码。这三个任何一个不一致都会出问题。我们在建库时用了utf8mb4,URL配置了characterEncoding=utf8,页面HTML的head标签里要声明<meta charset="UTF-8">,三个条件缺一不可。

同时要注意IDEA的文件的编码格式。IDEA右下角会显示文件编码,如果项目文件是GBK编码但页面声明UTF-8,等你引入模板时就会有部分中文是乱码的。解决方式是把IDEA的全局编码统一为UTF-8:设置里的File Encodings,把Global Encoding和Project Encoding都改成UTF-8,顺便勾上Transparent native-to-ascii conversion。改完需要重新加载项目才能全部生效。

6.2 数据库连接失败排查三步走

数据库连接失败是新手最常遇到的基础问题。我总结了一个三步排查法。第一步,先确认MySQL服务有没有启动,Windows下是按Win+R输入services.msc看MySQL服务状态,Linux/Mac是systemctl status mysqld。很多同学代码一点问题没有,就是服务没启动,查半天才发现。

第二步,确认账号密码和URL是否正确。特别是密码在application.yml里有没有被意外加了空格。第三步,确认URL里的参数是否完整,比如少了serverTimezone=Asia/Shanghai就会直接连不上。很多教程让你把参数照抄一遍,但没告诉你不加会怎样,这里补充一下。排查时可以先用Navicat或命令行直接连接MySQL,能连上说明账号密码没问题,再回头检查代码。

6.3 Mapper注入失败、依赖下载慢的解决方案

@Autowired private BookMapper bookMapper;结果启动时报错“Field bookMapper in ... required a bean of type ... that could not be found”,这个报错几乎每个用MyBatis的新手都会遇到一次。原因是Mapper接口没有被Spring扫描到。解决方式是在启动类上加@MapperScan("com.example.library.mapper"),或者在每个Mapper接口上加@Mapper注解。前者更推荐,因为只写一次就行。

依赖下载慢的话,还是回到Maven镜像问题。settings.xml文件中,在mirrors节点中加入阿里云镜像后,下载速度会指数级提升。还有一个小技巧:IDEA右侧Maven面板中,Plugins里的依赖如果出现红色波浪线,可以在终端执行mvn clean install -U强制重新下载。用旧版本Spring Boot的同学,还会遇到javax.servlet和jakarta.servlet包名变化的问题,这是Spring Boot 3.x的重大变更,社区版资料和AI生成的代码都默认用旧包名,遇到导包不对时留意一下版本差异。

6.4 “浏览器访问报404”的排查思路

页面渲染出来了,但访问某个路径报404,这种问题的排查方式有一个固定套路。先在浏览器开发者工具的Network标签页里看请求有没有发出去,返回的是什么状态码。如果是404,再看两个地方:第一个是Controller里的@RequestMapping路径有没有写对,比如类上写了/book,方法上写了/list,那完整路径是/book/list不是/bookList;第二个是控制器有没有被Spring扫描到,启动类默认扫描的包是启动类所在的包和它的子包,如果你的Controller放在别的包下,Spring根本找不到。

另外还有一个非常反直觉的坑:当你用书面的方式自定义了@RequestMapping("/book"),但在方法内部返回的视图名写成了return "redirect:/book/list",这表示让浏览器重新发送一次GET请求到/book/list,地址栏的URL会变成/book/list。如果视图名没有加redirect:,则会被Thymeleaf解析为book/list.html模板页面,两者返回的内容类型完全不同。搞清楚这两者的区别能省下你一整晚排查烦恼的时间。

7. 项目扩展:从入门到精通的进阶路线

7.1 项目还能往哪些方向加功能

图书管理项目的基本功能做完之后,你可以有无数个方向让它变得更像一个“生产级”项目。我建议按下面这个顺序逐层加码。

第一个阶段,可以给图书加上封面图片上传功能,这就涉及文件上传的配置、静态资源映射,基础但实用。第二个阶段,引入Redis缓存热点图书数据,每次点开图书详情时先查缓存,缓存没有再查数据库,同时学习缓存穿透、缓存击穿、缓存雪崩这三大经典问题,这是面试必问的。第三个阶段,把原来的Session登录换成JWT(JSON Web Token)无状态登录,然后顺手接上Spring Security或者自定义拦截器做接口鉴权,这是现在企业项目主流的认证方式。第四个阶段,把前后端分离做起来,后端提供JSON接口,前端用Vue开发,跨域问题怎么解决,到这里你的知识体系已经覆盖了现在很多公司的岗位要求。

7.2 面试中如何把这个项目讲出亮点

如果这个项目要写进简历,千万不要写“实现了图书的增删改查”——这个描述直接暴露了项目含金量不高。要学会用技术术语包装你的业务思考。比如,“设计了多表关联的数据库模型,通过外键约束维护数据完整性,并针对高频查询字段创建索引”;再比如,“利用Spring Boot的事务控制机制,保证借阅流程中库存扣减与记录写入的原子性,并处理了并发借阅场景下的数据一致性风险”。这些描述是你真实做过的事情,不是背八股文,面试官追问细节时你才有底气。

面试官大概率会问的一个问题是:“如果两个人同时借同一本只剩一本库存的书,怎么办?”这时候你可以回答数据库层面的行锁或者乐观锁,比如在库存字段上使用UPDATE book SET stock = stock - 1 WHERE id = ? AND stock > 0,利用数据库行锁保证库存不会变成负数,再加上事务的控制达到最终一致性。这就能让面试官看到,你不只是会写CRUD,而是真正思考过生产环境下的问题。

我个人在实际操作中有个体会,这个项目练完一遍之后,不要急着删掉,留着它。后面学的每一个新框架、新技术,都可以拿这个项目当试验田,加个中间件、重构个模块,让它跟着你的水平一起成长。我自己的第一个图书管理项目,后来被我折腾成了聚合支付、对接OSS、上K8s的完整系统。从一个简单的项目出发,逐步演进,这种学习路径比动不动重开一个项目扎实得多。这个内容后续还可以这样扩展,等你真正跑完一遍流程,再回头来看当初收藏的各种教程,会发现自己已经能看懂它们到底在讲什么了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/6 3:06:55

基于SSM与微信小程序的会议室预约系统毕业设计实战与踩坑指南

大学时候做毕业设计&#xff0c;我在"会议室预约"和"宿舍报修"之间纠结了很久。最后选了SSM 微信小程序的会议室预约系统&#xff0c;这个决定后来证明是相当值的&#xff1a;题目不算烂大街到毫无新意&#xff0c;但业务逻辑足够清晰&#xff0c;答辩时能…

作者头像 李华
网站建设 2026/10/6 3:05:59

京东自动下单工具源码拆解:自动登录、补货监控与下单全链路

简介&#xff1a;这是一套面向Python学习者与电商自动化爱好者的京东抢购助手完整源码&#xff0c;包含自动登录、定时预约、补货监控、自动加购物车与自动下单等核心功能&#xff0c;适合作为课程设计、期末大作业或毕设的参考项目&#xff0c;也便于具备一定Python基础者研读…

作者头像 李华
网站建设 2026/10/6 3:05:24

QEMU折腾后WSL2虚拟化禁用?从Hyper-V到CUDA的完整排查修复指南

先说我这边遇到的场景吧。前阵子为了在Windows上跑ARM64的OpenEuler和Alpine镜像&#xff0c;我用了QEMU。当时照着一些老教程折腾&#xff0c;为了让QEMU的TCG模拟不那么卡&#xff0c;我在BIOS里关了虚拟化&#xff0c;也跟着把Windows的“虚拟机平台”功能给勾掉了。结果等我…

作者头像 李华
网站建设 2026/10/6 3:05:07

InfiniBand是什么?与以太网、RDMA、AI集群网络的本质区别

第一次听到 InfiniBand&#xff08;简称 IB&#xff09;这个词的人&#xff0c;十有八九会被它的中文直译名“无限带宽”唬住。我当年第一次在机房里见到 IB 实机&#xff0c;也下意识觉得这是更贵、更快的“万兆以太网”。等到真正把 IB 链路拉起来、跑完一轮 MPI 带宽测试&am…

作者头像 李华
网站建设 2026/10/6 3:02:47

从工业视觉到YOLOv8:瓶装白酒疵品检测数据集与训练实战

简介&#xff1a;瓶装白酒疵品检测数据集.zip 是一份面向工业质检场景的图像数据集&#xff0c;聚焦瓶装白酒的外观瑕疵识别&#xff0c;适合计算机视觉、机器学习方向的开发者与研究者用于训练疵品检测模型。压缩包内共包含4516张JPG格式图片和1个JSON文件&#xff0c;图片覆盖…

作者头像 李华
网站建设 2026/10/6 3:02:36

本科毕设恶意代码检测平台:静态特征+机器学习实战指南

简介&#xff1a;本资源是一套完整的本科毕业设计项目——恶意代码检测分类平台&#xff0c;面向计算机科学、网络安全及人工智能方向的高年级本科生与初学者&#xff0c;聚焦于恶意软件行为识别与机器学习分类实践。项目基于Python实现&#xff0c;整合了前端Web界面&#xff…

作者头像 李华