简介:基于SSM框架实现的图书管理系统项目,面向正在学习SSM整合开发的Java后端初学者或需要完成课程设计的学生,用于快速掌握企业级Web应用从配置到业务落地的完整过程。资源包内共有三个文件,包括项目源码压缩包、使用手册文本和数据库脚本,压缩包体积约91.12兆,其中手册能引导环境部署,SQL文件可初始化库表。目前已有3254人学习参考。项目展示了Spring容器管理、SpringMVC请求处理以及MyBatis数据访问的协作流程,通过注解与XML配置结合的方式实现业务逻辑,并完成了权限登录、图书借还、邮件提醒和答题互动等功能模块,同时兼顾了防SQL注入等安全细节。使用说明书与数据库脚本有助于快速搭建开发环境并初始化数据,用户可在现有结构上继续扩展图书预约、借阅统计等功能,适合作为毕业设计或实训项目的参考起点。
1. 为什么选SSM做图书管理:课程设计常客背后的工程价值
“图书管理系统”和SSM框架的组合,在 Java Web 领域几乎成了某种默认选项:页面不花哨,技术不超前,但Spring管理对象、SpringMVC处理请求、MyBatis读写数据库,这三件事被一个完整业务串得明明白白。这套代码最典型的用途是高校课程设计或毕业设计的起点——你拿到的不是一堆零散示例,而是一个能跑通「登录 → 录入图书 → 借书 → 还书 → 查询统计」全流程的真实工程。对刚学完 Spring 基础但没见过完整项目的开发者来说,它恰好补上“框架概念怎么落到实际业务”这一课:事务配在哪个文件、拦截器怎么拦请求、SQL和Java对象的映射在哪里做、一个请求从浏览器到数据库再返回总共经过几层。本文按这套经典 SSM 图书管理系统逐步拆解,从架构、核心代码到启动部署,最后聊聊怎么把它改造成 Spring Boot 风格的项目。
2. 技术栈与目录结构:三层架构如何用一个请求串起来
2.1 SSM 三层的职责边界
SSM 不是三个框架的简单叠加,而是各管一段:Spring 用 IoC 容器管理 Service 层对象和数据库连接池,解决解耦问题;SpringMVC 负责 Web 层的路由分发,把 URL 映射到 Java 方法上;MyBatis 把 SQL 操作从业务代码里抽出去,集中在 Mapper 接口和 XML 中。三者通过配置文件互相咬合,形成了 Controller → Service → Dao 的经典分层。
这里有个很多新手会混淆的点:三层架构和 SSM 三个框架不是一一对应关系。Controller 层一般只做参数接收和视图跳转,Service 层承载业务规则和事务边界,Dao 层只负责单表或多表查询。真正的框架分工是——SpringMVC 主要影响 Controller 层和请求流程,MyBatis 主要影响 Dao 层,而 Spring 用 Bean 装配把所有层串起来。理解了这个错位,后面读代码就不会觉得“Controller 里为什么没有 SQL、Mapper 里为什么没有 if else”是奇怪的事了。
2.2 目录结构与文件清单
这套系统的标准 Maven 工程布局如下:
src/main/java/com.library ├── controller # 控制层:登录Controller、图书Controller、借阅Controller ├── service # 业务层:接口 + 实现类,事务注解在这里 ├── dao # 数据访问层:Mapper接口 ├── pojo # 实体类:Book、Reader、LoanRecord等 └── interceptor # 登录拦截器 src/main/resources ├── applicationContext.xml # Spring 根配置:数据源、事务、扫描包 ├── spring-mvc.xml # SpringMVC 配置:视图解析器、静态资源 ├── mybatis-config.xml # MyBatis 全局配置:别名、驼峰映射 ├── jdbc.properties # 数据库连接参数 └── mapper # MyBatis 的 SQL 映射 XML 文件 src/main/webapp ├── WEB-INF/web.xml # Web 应用入口,启动时装载 Spring 容器 └── jsp # 登录页、图书列表、借阅管理页面 pom.xml # Maven 依赖清单| 文件 / 目录 | 职责 |
|---|---|
| controller 包 | 接收请求参数、调用 Service、返回 ModelAndView 或字符串视图名 |
| service 包 | 业务校验、事务控制、跨表操作编排 |
| dao 包 + mapper 目录 | 接口定义方法,XML 提供 SQL 实现 |
| pojo 包 | 与数据库表对应的 JavaBean,含 getter/setter |
| applicationContext.xml | Spring 的 Bean 定义:连接池、事务管理器、组件扫描 |
| spring-mvc.xml | SpringMVC 的控制器扫描、注解驱动、视图解析器 |
| web.xml | Servlet 容器加载配置,绑定 Spring 和 SpringMVC |
第一次打开工程时别急着跑,先按上面这张表找到对应文件翻一遍。我一般习惯从 web.xml 入手,因为它决定了容器启动时先加载哪个配置文件、哪些 URL 交给哪个 Servlet 处理,等于整个应用的启动地图。
2.3 配置文件如何串起来
web.xml 是入口,它做了两件关键的事:配置 ContextLoaderListener 让 Spring 容器随应用启动而创建,配置 DispatcherServlet 拦截所有 HTTP 请求。核心片段如下:
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" version="3.1"> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:applicationContext.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>contextConfigLocation 参数指定了 Spring 根容器的配置文件路径;DispatcherServlet 的 init-param 单独指定 spring-mvc.xml,形成父子容器。子容器能访问父容器的 Bean,反之不行,所以 Service 通常配在 applicationContext.xml 中,Controller 则放在 spring-mvc.xml 的扫描范围内。这样分层后,Controller 可以直接注入 Service,而 Service 不会反向依赖 Controller。
这里有个细节值得注意:servlet-mapping 的 url-pattern 是/而不是/*,这意味着 DispatcherServlet 会处理除 JSP 以外的所有请求。静态资源如果没在 spring-mvc.xml 里放行,会出现 CSS、JS 加载不出来的问题。常见的解决方式是在 spring-mvc.xml 中加<mvc:default-servlet-handler/>,让容器默认 Servlet 兜底处理静态文件。
3. 核心模块拆解:借一本书在 SSM 中的完整调用链
3.1 登录拦截:Interceptor 如何守住页面入口
图书管理系统不可能让未登录用户直接访问借阅接口,所以工程里通常会写一个 LoginInterceptor 实现 HandlerInterceptor 接口。这套拦截器在 SpringMVC 中比 Filter 更贴近业务:它只拦截经过 DispatcherServlet 的请求,可以拿到 HandlerMethod 从而精确判断控制器方法。
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); Object admin = session.getAttribute("admin"); if (admin == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return false; } return true; } }这段逻辑非常简单:从 Session 中取管理员对象,没有就重定向到登录页。但它能拦住多少东西取决于在配置中的拦截路径,通常在 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="/css/**"/> <mvc:exclude-mapping path="/js/**"/> <mvc:bean class="com.library.interceptor.LoginInterceptor"/> </mvc:interceptor> </mvc:interceptors>path 是/**表示所有路径都进拦截器,exclude-mapping 放行了登录接口、登录页面和静态资源。参数里最难调的就是 exclude-mapping 的粒度——放多了等于没拦,放少了登录页 CSS 都加载不出来。工程实践的通用做法是只放行“登录相关 + 静态资源”,其余业务路径一律过拦截器。
3.2 借书主流程:Controller 层的参数处理
借书是这个系统最核心的业务,它的调用链也是三层架构的缩影。Controller 层拿到的是页面表单传过来的 bookId 和 readerId 两个参数,先做基础校验再交 Service。
@Controller @RequestMapping("/loan") public class LoanController { @Autowired private LoanService loanService; @RequestMapping("/borrow") public String borrow(@RequestParam("bookId") Integer bookId, @RequestParam("readerId") Integer readerId, Model model) { boolean result = loanService.borrow(bookId, readerId); if (result) { model.addAttribute("message", "借书成功"); } else { model.addAttribute("message", "借书失败:库存不足或读者状态异常"); } return "loan/result"; } }@RequestParam 把请求参数绑定到方法入参,Model 用于向视图传递提示信息。返回值 "loan/result" 是视图逻辑名,由 InternalResourceViewResolver 拼成/WEB-INF/jsp/loan/result.jsp。注意这里 borrow 方法的返回是 String,不是 ModelAndView——这是 SpringMVC 很常见的写法,字符串代表视图名,Model 作为数据载体,两种方式功能等价,写字符串更轻。
借书失败和成功的提示没有用抛异常的方式,而是返回布尔值。这是中小型系统的典型取舍:业务失败属于正常流程分支,用返回值传递比异常捕获更直观;只有系统级异常(比如数据库连接失败)才交给全局异常处理器。
3.3 Service 层:事务边界在那里生效
Service 是借书流程的“大脑”。一次借阅既要扣减图书库存,又要新增一条借阅记录,这两步不能一成一败,所以必须放在同一个事务里。
@Service public class LoanServiceImpl implements LoanService { @Autowired private BookDao bookDao; @Autowired private LoanRecordDao loanRecordDao; @Override @Transactional public boolean borrow(Integer bookId, Integer readerId) { Book book = bookDao.selectById(bookId); if (book == null || book.getStock() <= 0) { return false; } bookDao.decreaseStock(bookId); LoanRecord record = new LoanRecord(); record.setBookId(bookId); record.setReaderId(readerId); record.setLoanTime(new Date()); record.setStatus(1); loanRecordDao.insert(record); return true; } }@Transactional 标注在接口实现类的 public 方法上,Spring 通过动态代理为这个方法生成事务代理。这里有几个关键参数没写全但默认值很值得注意:默认传播行为是 REQUIRED,即当前没有事务就新建一个;默认回滚条件是 RuntimeException,如果代码中抛出受检异常而没配置 rollbackFor,事务不会回滚。所以严谨的写法是@Transactional(rollbackFor = Exception.class)。
把事务放在 Service 层而不是 Controller 层,原因是 Controller 只负责协议转换,不该知道“步骤一、步骤二”这种业务编排。如果事务放在 Controller,一个 Controller 调用两个 Service 方法时,事务边界会变得模糊,后期维护很难受。这个设计原则在小项目里看不出差别,但一旦业务复杂,Service 层的事务边界就是系统的安全线。
3.4 Mapper 层:SQL 与结果映射的协同
借书流程里两个核心 SQL 分别是减库存和新增记录。MyBatis 有两种写法:注解 SQL 和 XML SQL。图书管理系统这类需要多表关联、动态条件的项目,XML 是主流选择。
<update id="decreaseStock"> UPDATE book SET stock = stock - 1 WHERE id = #{id} AND stock > 0 </update> <insert id="insert" parameterType="com.library.pojo.LoanRecord"> INSERT INTO loan_record (book_id, reader_id, loan_time, status) VALUES (#{bookId}, #{readerId}, #{loanTime}, #{status}) </insert>decreaseStock 里 WHERE 条件带stock > 0是一个典型的防并发手段:即使两个请求同时读到库存为 1,数据库层面也只会让一条 UPDATE 成功,因为第二条执行时 stock 已经变成 0,满足不了条件,影响行数为 0。这个 trick 比“先查再判再更新”可靠得多,是并发扣减库存的常见实现思路。
Mapper 接口和 XML 的绑定关系靠的是接口全限定名与 XML 的 namespace 一致,方法名与语句 id 一致。同时 MyBatis 的全局配置里建议开启驼峰映射,这样数据库字段loan_time就能自动映射到 JavaBean 的loanTime属性,省掉大量 resultMap 手工配置。
4. 数据库设计与实体映射:五张表如何支撑借阅闭环
4.1 核心表结构与建表 SQL
图书管理系统的数据库设计并不复杂,但五张表之间关联关系清晰:category 图书分类、book 图书、reader 读者、loan_record 借阅记录、admin 管理员。其中 loan_record 是关联 book 和 reader 的中枢表,一次借阅就是在这张表插入一条数据同时给 book 表减库存。
CREATE TABLE category ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL UNIQUE ); CREATE TABLE book ( id INT AUTO_INCREMENT PRIMARY KEY, isbn VARCHAR(20) UNIQUE, name VARCHAR(100) NOT NULL, category_id INT, stock INT DEFAULT 0, publish_date DATE, FOREIGN KEY (category_id) REFERENCES category(id) ); CREATE TABLE reader ( id INT AUTO_INCREMENT PRIMARY KEY, card_no VARCHAR(20) UNIQUE, name VARCHAR(50) NOT NULL, phone VARCHAR(20), status TINYINT DEFAULT 1 -- 1 正常,0 冻结 ); CREATE TABLE loan_record ( id INT AUTO_INCREMENT PRIMARY KEY, book_id INT NOT NULL, reader_id INT NOT NULL, loan_time DATETIME, return_time DATETIME, status TINYINT DEFAULT 1, -- 1 借出,0 已还 FOREIGN KEY (book_id) REFERENCES book(id), FOREIGN KEY (reader_id) REFERENCES reader(id) );注意 loan_record 里没有写UNIQUE(book_id),因为同一本书可以有多条历史借阅记录。如果要限制一本书同时只能被借一次,应在业务层查询未归还记录时做判断——更好的做法是给 book 表加一个status字段表示在架/借出,但多数课程设计项目会选择直接用库存数模拟这个状态,属于实用主义的简化。
4.2 实体类与 MyBatis 映射配置
POJO 层的 Book 类与表字段严格对应,类型使用包装类而不是基本类型,这是 MyBatis 映射的硬性要求——如果使用 int 而数据库某行为 NULL,映射时会因无法装箱而报错。
public class Book { private Integer id; private String isbn; private String name; private Integer categoryId; private Integer stock; // 省略 getter / setter }查询图书列表时,页面上通常要显示分类名称而不是分类 ID,这就涉及多表关联查询。MyBatis 中的标准做法是定义一个扩展查询对象或用 resultMap 的 association 标签。
<resultMap id="BookWithCategoryMap" type="com.library.pojo.Book"> <id property="id" column="id"/> <result property="name" column="name"/> <result property="isbn" column="isbn"/> <result property="stock" column="stock"/> <association property="category" javaType="com.library.pojo.Category"> <id property="id" column="category_id"/> <result property="name" column="category_name"/> </association> </resultMap> <select id="selectBookWithCategory" resultMap="BookWithCategoryMap"> SELECT b.*, c.name AS category_name FROM book b LEFT JOIN category c ON b.category_id = c.id WHERE b.name LIKE CONCAT('%', #{keyword}, '%') </select>这里把分类名称取别名为 category_name,通过 association 映射到 Book 对象里嵌套的 Category 属性。SQL 用 LEFT JOIN 是为了处理图书没有分类时的场景——如果使用 INNER JOIN,那些分类 ID 为空的图书会直接从查询结果中消失,页面列表就少了数据,这种隐藏 bug 在测试数据不全时很难发现。
4.3 模糊查询与动态 SQL
图书管理系统中列表页的搜索框几乎都会用到动态查询。MyBatis 的<if>标签就是为这种场景设计的,它可以按条件拼接 WHERE 子句。
<select id="searchBooks" parameterType="map" resultType="com.library.pojo.Book"> SELECT * FROM book <where> <if test="name != null and name != ''"> AND name LIKE CONCAT('%', #{name}, '%') </if> <if test="categoryId != null"> AND category_id = #{categoryId} </if> </where> ORDER BY id DESC </select><where>标签会自动处理第一项前面的 AND,避免出现WHERE AND name LIKE这种语法错误。使用 CONCAT 拼接百分号而不是直接写'%${name}%',是为了防止 SQL 注入——${}是字符串拼接,#{} 是预编译参数占位,后者才是正确姿势。很多新手在这个细节上翻车:表面看功能正常,参数值传个%也能跑,但一旦遇到恶意输入,后果是数据库层面被脱裤。MyBatis 的 #{} 对单引号会自动转义,这才是动态查询该有的打开方式。
5. 环境准备与启动指南:六步跑通,附现场排查手册
5.1 Maven 依赖与 JDK 版本
拿到源码第一步是确认环境版本。这套系统是典型的 Java 8 + Tomcat 8.5 + MySQL 5.7 组合,pom.xml 的依赖核心如下:
<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.1.20.RELEASE</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.6</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.6</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.49</version> </dependency> </dependencies>MySQL 驱动版本要匹配数据库版本:MySQL 5.x 用 5.1.49,MySQL 8.x 需要换用 8.0 系列驱动且连接 URL 要加serverTimezone=Asia/Shanghai参数。如果你本机装的是 MySQL 8,直接把驱动版本升级、在 jdbc.properties 的连接串里加上时区参数即可,否则连接阶段就会报时区错误。
5.2 数据库初始化与连接配置
把 SQL 脚本导入本地数据库后,修改 jdbc.properties 中的连接信息。这套字段是整个系统跑通的第一道关卡:
jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/library?useUnicode=true&characterEncoding=utf8 jdbc.username=root jdbc.password=your_password这里最容易踩的坑是 characterEncoding=utf8 必须与数据库创建时的字符集一致。如果建库用了 utf8mb4,连接串写 utf8 也能连,但中文可能在某些特殊字符场景下乱码。建议两边统一用 utf8mb4,连接串同步修改。密码不要写在代码里写死,这是本地调试项目,直接放 properties 文件没问题,但提交到公开仓库前一定改掉。
5.3 IDEA + Tomcat 部署步骤
用 IDEA 打开 Maven 工程后,依次执行:配置 Tomcat Server → 添加 Artifact 为 war exploded → 部署。war exploded 模式比 war 模式更适合调试,修改 JSP 或 Java 文件后无需重启容器,热部署能直接生效。
Run Configuration 中 VM options 一般不用动,但 Application context 建议设为/library,这样访问路径就是http://localhost:8080/library/。applicationContext.xml 中数据源 Bean 会把 jdbc.properties 的属性通过占位符注入,启动时若看到Property 'url' not set这类报错,多半是占位符没解析,检查 spring 配置文件里有没有配<context:property-placeholder location="classpath:jdbc.properties"/>。
5.4 常见问题排查:从 404 到乱码的五种翻车现场
现象一:启动后所有页面 404,但 Tomcat 正常启动。原因:DispatcherServlet 的 url-pattern 写成了/*而不是/,导致 JSP 请求也被 Servlet 接管,而 Servlet 没有对应的 RequestMapping,容器返回 404。解决:回到 web.xml 把 servlet-mapping 改成<url-pattern>/</url-pattern>,保存后重启。这个错误隐蔽在“Tomcat 都能启动,就是访问啥都 404”,排查时第一时间看 servlet-mapping 的配置。
现象二:JSP 页面中文全部乱码,数据库中文也变成问号。原因:三层编码不一致。请求到 Controller 需要 CharacterEncodingFilter 设置 UTF-8,但该过滤器要放在 web.xml 过滤器链的最前面才生效;数据库连接串没带 characterEncoding 参数;MySQL 表默认字符集不是 utf8mb4。解决:给 web.xml 加 CharacterEncodingFilter 并放在 DispatcherServlet 之前,连接串补上字符集参数,MySQL 端用ALTER TABLE xxx CONVERT TO CHARACTER SET utf8mb4修正已有表。
现象三:点击借书按钮后提示“库存不足”,但数据库里 stock 明明大于 0。原因:不是库存真的不足,而是 Service 层查询到的 Book 对象为 null。常见情况是传入的 bookId 参数没绑定上,Controller 参数名和表单字段名不一致,导致 ID 为 null,查询自然返回空。解决:检查 JSP 表单中 input 的 name 属性是否与 Controller 的 @RequestParam 一致,加上日志打印 bookId 值即可确认。
现象四:事务没回滚,减库存后插入借阅记录失败,库存照样少了。原因:@Transactional 注解标注在 private 方法上,或者同类内部通过 this 调用,Spring 代理没有介入。解决:把注解移到 public 方法,且借阅记录插入要放在不同 Service 中通过注入调用,不能 this.borrow 内部直接调 this 的其他方法。每次写完事务逻辑,用“数据库操作分两步以上的方法”自查一遍注解位置和调用方式。
现象五:XML 中 SQL 报Invalid bound statement (not found)。原因:Mapper 接口找到了,但对应的 XML 没被 MyBatis 加载。常见是 mapper 目录没有在 applicationContext.xml 中配置 mapperLocations,或者 XML 文件没被 Maven 打包进 classpath。解决:在 Spring 配置中给 sqlSessionFactory 加 mapperLocations 指向classpath:mapper/*.xml,同时检查 resources 目录结构,让 XML 和 jdbc.properties 都在 src/main/resources 下。
6. 二次开发思路:从 SSM 到 Spring Boot 的三步改造
6.1 改造一:把三大配置文件收拢进 application.yml
SSM 项目后期的改造方向几乎都是迁往 Spring Boot。第一步不是重写业务代码,而是把 web.xml、applicationContext.xml、spring-mvc.xml 三份配置合并成一份 application.yml。依赖替换也很直接:spring-webmvc 换成 spring-boot-starter-web,mybatis 换成 mybatis-spring-boot-starter,DispatcherServlet 由 Boot 自动装配完成,过滤器链用 FilterRegistrationBean 注册。Controller、Service、Mapper 代码几乎不用动,真正要调整的只有配置和入口类。
@SpringBootApplication @MapperScan("com.library.dao") public class LibraryApplication { public static void main(String[] args) { SpringApplication.run(LibraryApplication.class, args); } }@MapperScan 替代了原来 mybatis-spring 的 MapperScannerConfigurer 配置,这也是改造中必须加的一行——漏掉它,所有 Mapper 接口都无法注入 Service,启动即报错。视图层改造可以选择保留 JSP,也可以顺手换成 Thymeleaf。保留 JSP 需要额外加 spring-boot-starter-tomcat-jsp 依赖并设置视图前缀后缀;换 Thymeleaf 则模板语法需要重写,工作量取决于页面数量。
6.2 改造二:给查询加上 PageHelper 分页
图书列表页的分页在很多 SSM 项目里是用手写 LIMIT 实现的,改造成 Spring Boot 阶段可以把分页逻辑统一交给 PageHelper 插件。依赖引入pagehelper-spring-boot-starter后,Service 层查询只需在返回 List 前调用分页方法,不需要改 SQL。
public PageInfo<Book> getBooks(int pageNum, int pageSize) { PageHelper.startPage(pageNum, pageSize); List<Book> books = bookDao.selectAll(); return new PageInfo<>(books); }PageHelper.startPage 之后的第一条 MyBatis 查询会自动拼接 LIMIT,PageInfo 对象里封装了总页数、当前页、总记录数这些分页所需数据。这里有个经典翻车点:startPage 和查询之间不能插入其他 SQL 操作,否则插件会作用到错误的查询上。所以写这个方法时,我习惯把 startPage 紧跟查询语句,中间不放任何日志查询或缓存读取,这是 PageHelper 文档里反复强调的规则。
6.3 改造三:借阅热点的本地缓存
这种管理系统有一个共同的痛点:图书列表页和借阅排行属于高频查询,每次都全量查库里非常浪费。改造时我不建议直接上 Redis,而是先在 Service 层加一层本地缓存,用 ConcurrentHashMap 存储热门图书 ID 列表,缓存过期时间设为 5 分钟,被借阅时主动失效。这样不需要引入额外服务,改动量小,跑一段时间观察命中率再决定是否升级为 Redis。
从这段改造经历中得到的最大教训是:所有代码改动前必须先理清“请求从进入到返回的完整链路”。每拿到一个 SSM 项目,我现在的固定顺序是看 web.xml 确认装载配置,找 Controller 看路由和参数,翻 Service 看事务边界,盯 Mapper XML 确认 SQL 和映射逻辑,四个文件看完再动手改代码——顺序不对很容易在配置层迷路。这套图书管理系统的价值也正在于此:麻雀虽小,但 Web 开发从配置到业务的完整链路一条不少。希望这篇梳理能帮你快速跑通它,找到可以下手改造的第一行代码。
本文还有配套的精品资源,点击获取