简介:一套基于SpringBoot+MyBatis+JSP实现的图书管理系统完整项目,面向Java Web学习者、课程设计及毕业设计人群。系统包含图书增删改查、分类管理、借阅归还、分页展示等核心功能,并整合了权限控制,能够帮助读者理解SpringBoot自动配置、MyBatis持久层映射、JSP视图渲染以及MVC分层协作的典型流程。压缩包共1192个文件,大小95.3MB,其中648个class文件为编译产物,173个JSP页面负责前端展示,148个XML文件用于MyBatis映射与工程配置,65个jar为第三方依赖,另有Java源码、图片、CSS与JavaScript等辅助资源,覆盖了从源码到可运行部署的完整工程。已有2207人学习,读者可通过本项目快速掌握前后端联动、数据库CRUD与分页查询的实现思路,也可将其作为实训或毕业设计的参考基础,并根据需要做二次扩展。
1. 从空目录到能跑通的图书管理系统
网上搜“图书管理系统”,翻来覆去就是 springboot + mybatis + jsp 三件套。看着像旧技术,可每年靠它毕业的人一点没少。这个组合能同时解决三件事:给学生一个能讲清楚每个环节的完整 Java Web 项目,给刚转后端的程序员一套看得懂的 MVC 全链路,给公司一个不用引入前端工程构建链的轻量后台。它把 Spring Boot 的自动装配、MyBatis 的 SQL 映射、JSP 的服务端渲染摊在明面上,翻车点集中,排查路径短。新人照着走能跑通,熟练的人能看清边界:这套方案适合中小型管理系统,不适合高并发和复杂交互前端。下面把它从建工程到做熟讲透。
2. 搭建工程骨架:Spring Boot 2.7 + MyBatis Starter + JSP 的启动组合
2.1 三个依赖必须按这个姿势配,否则一启动就 500
JSP 在 Spring Boot 里属于“二等技术支持”,官方文档不推荐但也不砍掉,所以依赖要自己手动补全。先锁定 Spring Boot 版本 2.7.18,这是 2.x 系列的最后一个稳定版本,也是目前做 JSP 项目最省心的版本。为什么不用 3.x,第 5 章单独讲,这里先把最稳的组合立住。
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-jasper</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> </dependencies>这段配置有三个关键点。第一,spring-boot-starter-web自带内嵌 Tomcat,但它不包含 JSP 编译器,所以必须单独引tomcat-embed-jasper,scope 用provided是因为部署到外部 Tomcat 时容器自己带这个库,避免两套 Jasper 打架。第二,jstl是 JSP 页面里c:forEach、c:if这些标签的来源,不引的话标签会原样输出到页面上。第三,mybatis-spring-boot-starter2.3.2 对应 MyBatis 3.5.x,和 Spring Boot 2.7 是官方测过的配对,别随手升到 3.x。
2.2 springboot项目结构:JSP 不能放 templates,必须放 webapp/WEB-INF
很多新手在这里栽跟头:在src/main/resources/templates下建了bookList.jsp,启动后访问永远 404。原因在于 Spring Boot 的模板自动配置只认 Thymeleaf、Freemarker 这类模板引擎,JSP 不归它管。JSP 由InternalResourceViewResolver通过 Servlet 容器直接编译,文件必须放在src/main/webapp下。
src/main/java/com/example/library/ ├── LibraryApplication.java ├── controller/ │ └── BookController.java ├── service/ │ ├── BookService.java │ └── BookServiceImpl.java ├── mapper/ │ └── BookMapper.java └── entity/ ├── Book.java └── BorrowRecord.java src/main/resources/ ├── application.yml └── mapper/ └── BookMapper.xml src/main/webapp/WEB-INF/jsp/ ├── bookList.jsp ├── bookAdd.jsp └── login.jsp我一般会把 JSP 放webapp/WEB-INF/jsp而不是webapp/jsp。区别在于 WEB-INF 目录对浏览器不可见,用户没法通过 URL 直接访问.jsp源码文件,只能由 Controller forward 转发过去,等于白送一层安全防护。另外启动类LibraryApplication.java的包名必须覆盖controller/service/mapper这些子包,@SpringBootApplication默认扫描的是它所在包及以下,如果启动类放错包,Mapper 接口注册不上,运行时就会报Invalid bound statement。
2.3 application.yml 三项配置决定页面能不能被找到
JSP 项目里 application.yml 的内容比普通 REST 项目多三样东西:视图解析器前缀后缀、MyBatis 映射文件位置、驼峰映射开关。
server: port: 8080 servlet: context-path: / 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: root mvc: view: prefix: /WEB-INF/jsp/ suffix: .jsp mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.library.entity configuration: map-underscore-to-camel-case: truespring.mvc.view.prefix和suffix是视图解析器的核心参数。Controller 里写return "bookList",解析器会拼成/WEB-INF/jsp/bookList.jsp交给容器渲染,这俩配置少一个页面就找不到。mybatis.mapper-locations指定 XML 映射文件的位置,classpath:mapper/*.xml对应src/main/resources/mapper目录。map-underscore-to-camel-case打开后,数据库的create_time自动映射到实体类的createTime,不用手写一堆resultMap。连接串里的serverTimezone=Asia/Shanghai不加的话,MySQL 8 驱动会直接报时区异常。
2.4 建表:图书、用户、借阅记录三张表一次性到位
图书管理系统的数据模型不复杂,三张表足够覆盖核心业务。下面是完整的建表脚本,字段类型和注释直接决定了后面 MyBatis 映射的写法。
CREATE DATABASE IF NOT EXISTS library DEFAULT CHARACTER SET utf8mb4; USE library; CREATE TABLE book ( id BIGINT AUTO_INCREMENT PRIMARY KEY, isbn VARCHAR(20) NOT NULL UNIQUE, title VARCHAR(100) NOT NULL, author VARCHAR(50), stock INT NOT NULL DEFAULT 0, state TINYINT NOT NULL DEFAULT 1 COMMENT '1可借 0下架', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE `user` ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, real_name VARCHAR(50) ); CREATE TABLE borrow_record ( id BIGINT AUTO_INCREMENT PRIMARY KEY, book_id BIGINT NOT NULL, user_id BIGINT NOT NULL, borrow_time DATETIME DEFAULT CURRENT_TIMESTAMP, return_time DATETIME, status TINYINT NOT NULL DEFAULT 0 COMMENT '0借出 1已还' );字符集选 utf8mb4 而不是 utf8,因为 ISBN 里可能有特殊字符,用户名也可能有中文和生僻字,utf8mb4 是 MySQL 8 的默认字符集,能覆盖四字节 Unicode。book.state用TINYINT而不是VARCHAR,这样在 MyBatis 的<if>判断里可以直接用Integer精确匹配,避开 OGNL 字符串比较的坑,这个坑第 4 章会详细讲。borrow_record.status是借还流水的状态位,0 表示借出未还,1 表示已还,后面做超期提醒全靠它。
3. 实现最小闭环:图书列表、新增和借阅的完整代码怎么写
3.1 BookMapper 的 XML 和注解怎么选
实体类字段直接对应数据库列,MyBatis 开启驼峰映射后,下划线命名自动转驼峰,实体里不需要额外加注解。
public class Book { private Long id; private String isbn; private String title; private String author; private Integer stock; private Integer state; private LocalDateTime createTime; // getter / setter 省略,实际代码里要补齐 }为什么要写Integer而不是int?因为 MyBatis 的<if test="state != null">判断依赖包装类型,int默认值是 0,永远不为 null,条件判断会失效。接下来是 Mapper 接口,只定义方法签名,SQL 写在 XML 里。
@Mapper public interface BookMapper { List<Book> selectList(Book query); Book selectById(Long id); int insertBook(Book book); int updateStock(@Param("id") Long id, @Param("stock") Integer stock); }@Mapper注解是关键,它让 mybatis-spring-boot-starter 启动时自动扫描并注册这个接口的代理实现。条件查询selectList接收一个Book对象作为查询条件,标题、状态都可以塞进去动态拼 SQL。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.library.mapper.BookMapper"> <select id="selectList" resultType="Book"> SELECT * FROM book <where> <if test="title != null and title != ''"> AND title LIKE CONCAT('%', #{title}, '%') </if> <if test="state != null"> AND state = #{state} </if> </where> ORDER BY create_time DESC </select> <insert id="insertBook" parameterType="Book" useGeneratedKeys="true" keyProperty="id"> INSERT INTO book(isbn, title, author, stock, state) VALUES(#{isbn}, #{title}, #{author}, #{stock}, #{state}) </insert> <update id="updateStock"> UPDATE book SET stock = stock - 1 WHERE id = #{id} AND stock > 0 </update> </mapper>selectList里<where>标签会自动去掉多余的AND,这是 MyBatis 动态 SQL 最常用的写法。CONCAT('%', #{title}, '%')这里有个细节:#{title}是预编译参数,不能直接在占位符里写%title%,因为占位符只绑定值,LIKE的模糊匹配符要拼在 SQL 文本里。updateStock这行 SQL 是整个借阅流程最关键的操作,AND stock > 0条件不满足时影响行数为 0,Service 层根据返回的行数就能判断库存是否充足,这是避免超卖最简单的做法。
这里顺带说下 MyBatis 缓存。一级缓存默认开着,作用范围是同一个 SqlSession,Web 项目里每次请求都会新建 SqlSession,所以平时感受不到。真正要留意的是二级缓存,mybatis 二级缓存实现跨 SqlSession 共享,默认不开。图书库存是实时性要求很高的数据,这张表不要开二级缓存,否则别的模块改了库存,你这边 SessionFactory 级别还缓存着旧值。
3.2 Service 层:借书那天你迟早会为事务买单
Service 层是事务的边界,Controller 里不要写业务逻辑,这是这套架构的规矩。借书操作涉及两张表:扣减库存、插入借阅记录,任何一个失败都得回滚。
@Service @Transactional(rollbackFor = Exception.class) public class BookServiceImpl implements BookService { @Resource private BookMapper bookMapper; @Resource private BorrowRecordMapper borrowRecordMapper; @Override public void borrowBook(Long bookId, Long userId) { // 返回 0 说明库存不足或书不存在,直接抛业务异常 int rows = bookMapper.updateStock(bookId, 1); if (rows == 0) { throw new BizException("库存不足"); } BorrowRecord record = new BorrowRecord(); record.setBookId(bookId); record.setUserId(userId); record.setStatus(0); borrowRecordMapper.insertRecord(record); } }@Transactional(rollbackFor = Exception.class)里的rollbackFor必须显式写,这是血泪经验。Spring 的默认行为是只对RuntimeException回滚,你自己定义的BizException如果继承的是Exception,不加 rollbackFor 就永远不回滚,库存照样扣,记录照样插,两边数据就错乱了。Spring Boot 2.x 默认用 CGLIB 代理,基于类而不是接口做代理,所以事务方法不能用private修饰,且不能通过同类内部this调用绕过代理,这个坑第 4 章展开说。
3.3 Controller、JSP 和 JSTL:三个文件拼出一个列表页
Controller 返回字符串由视图解析器拼成 JSP 路径,这个字符串是逻辑视图名,不是物理路径。
@Controller @RequestMapping("/book") public class BookController { @Resource private BookService bookService; @GetMapping("/list") public String list(Book query, Model model) { model.addAttribute("books", bookService.listBooks(query)); return "bookList"; } @GetMapping("/add") public String addPage() { return "bookAdd"; } @PostMapping("/add") public String add(Book book) { bookService.addBook(book); return "redirect:/book/list"; } }这里最容易犯的错是把@Controller写成@RestController。@RestController会把返回值当 JSON 输出,浏览器里看到的是一串字符串而不是页面。redirect:/book/list是 Post/Redirect/Get 模式,表单提交后重定向到列表页,用户按 F5 刷新不会重复提交表单。如果这里直接return "bookList",刷新一次就会插入一条重复数据。
JSP 页面里用 JSTL 标签遍历集合,这是 JSP 和纯 HTML 最大的区别。
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <head> <title>图书列表</title> </head> <body> <table border="1"> <tr><th>书名</th><th>作者</th><th>库存</th><th>操作</th></tr> <c:forEach items="${books}" var="b"> <tr> <td>${b.title}</td> <td>${b.author}</td> <td>${b.stock}</td> <td><a href="/book/borrow?id=${b.id}">借阅</a></td> </tr> </c:forEach> </table> </body> </html>页面第一行的page指令必须写,contentType="text/html;charset=UTF-8"保证中文不乱码。taglib指令引入 JSTL 核心标签库,不引的话c:forEach会被当普通文本输出。${b.title}走的是 EL 表达式,底层调getTitle()。这套写法扩展性很强,后面要做 jsp 个人信息展示页面、借阅记录列表,复制一个 JSP 改字段就行。
4. 四个让图书管理系统翻车的坑:每个都是我先踩过的
4.1 500 白屏 + 日志只有两行:tomcat-embed-jasper 没加
现象:项目启动正常,访问/book/list直接报 HTTP 500,控制台只有两行 INFO 日志,看不到完整堆栈。浏览器显示 WhiteLabel Error Page,什么有效信息都没有。
原因:spring-boot-starter-web虽然内嵌了 Tomcat,但不包含 JSP 编译引擎。JSP 文件第一次被访问时,需要 Jasper 把.jsp编译成.java再编成.class,缺了tomcat-embed-jasper这一步直接失败。更隐蔽的是 Spring Boot 3.x 下,即使加了 jasper 也会报错,因为命名空间从javax.servlet换成了jakarta.servlet,JSP 的编译 API 全部不兼容。
解决:回到 Spring Boot 2.7.18,按第 2 章的 pom 配置完整引入tomcat-embed-jasper(scope 为 provided)和jstl1.2。如果团队已经锁了 Boot 3.x,JSP 这条路基本没有后悔药,最务实的方案是换 Thymeleaf,标签写法和 JSTL 接近,迁移成本比死磕 jakarta 兼容低得多。
4.2 页面出来了但 CSS/JS 全 404:静态资源放错目录
现象:JSP 页面里的表格和文字能显示,但样式完全没有,浏览器 F12 里一堆红色 404,请求路径是/static/css/style.css。
原因:Spring Boot 的默认静态资源映射是/**,自动从classpath:/static、classpath:/public、classpath:/resources这些目录找资源。很多从 Spring MVC 时代过来的人习惯在src/main/webapp/static下建资源目录,这个位置在 Spring Boot 内嵌容器里不在默认查找列表内,所以全 404。
解决:把 CSS、JS、图片放到src/main/resources/static/css、static/js下,页面引用路径改成/css/style.css,不要写/static前缀。如果你想保留/static/**这种 URL 风格,可以在 application.yml 加spring.mvc.static-path-pattern: /static/**,然后资源还是放classpath:/static下。JSP 在 WEB-INF 下不能被浏览器直接访问,但静态资源不用藏,放 classpath 下既能被容器加载,又不会被 JSP 编译流程干扰。
4.3 MyBatis条件不生效:OGNL 表达式别用 == 比较字符串
现象:列表页加了一个分类筛选下拉框,前端传了state=1,结果 SQL 里没有拼接这个条件,查询结果始终是全部数据。这种问题在排查时容易怀疑是参数没传进来,实际上参数是对的,是<if>标签里的表达式出了问题。
原因:MyBatis 的<if test="state == '1'">里走的是 OGNL 表达式,单引号包住的'1'会被解析成 Character 字符,而state是 String 类型,String 和 Character 用==比较永远返回 false,条件被跳过。这个细节在 Java 代码里写"1".equals(state)习惯了根本注意不到,但在 OGNL 里语法规则完全不同,是 MyBatis 条件不生效最常见的原因。
解决:双引号才是字符串,把<if test="state == '1'">换成<if test='state == "1"'>。更稳妥的写法是转成 toString 比较,<if test="state != null and state == '1'.toString()">。最省心的方案是在实体类里把state定义成Integer,XML 里只用<if test="state != null">判空,既避开 OGNL 字符串陷阱,也贴合数据库TINYINT字段类型。
4.4 借阅成功但库存没扣:事务被同类自调用吃掉
现象:borrow_record表插入了借阅记录,但book.stock没扣减,两边数据对不上。日志里没有异常,看起来一切都正常。
原因:事务注解没生效。Spring 的事务是 AOP 代理实现的,只有从外部进入被代理对象的方法,事务切面才会执行。如果你在BookServiceImpl内部写了一个方法调用this.borrowBook(),这个调用发生在代理对象内部,直接绕过切面,@Transactional形同虚设。另外一个近亲病因是业务方法里try-catch捕获了异常并打了一行日志,异常被吞掉后事务管理器根本感知不到业务失败,自然不会回滚。
解决:事务方法调用一律通过注入的 Service 对象进行,不要用this调内部方法。异常的准则很简单:业务代码不做 catch 吞异常,如果有需要,catch 之后必须重新抛出RuntimeException。借书这种写操作场景,看一眼.intValue()或row > 0的判断结果,确认影响行数符合预期,再继续后续操作。
5. 版本锁死:Spring Boot 2.7.18 和 mybatis starter 2.3.2 是这套组合的甜点参数
5.1 为什么卡在 2.7.18:javax 和 jakarta 的分界线
搜索引擎里“springboot版本太高”这类问题,一大半都出在 Spring Boot 3.x 的命名空间迁移上。Spring Boot 3.0 起,整个 Java EE 规范从javax.*迁移到jakarta.*,涉及 Servlet、JSP、JSTL 等所有 Web 底层 API。这不仅仅是换个 import 路径的问题,Tomcat、Jasper 编译器的实现也跟着变了。
| 组件 | Spring Boot 2.7.x | Spring Boot 3.x |
|---|---|---|
| Servlet 命名空间 | javax.servlet | jakarta.servlet |
| tomcat-embed-jasper | 完全兼容 | 不兼容 |
| mybatis-spring-boot-starter | 2.3.x 稳定 | 需 3.0.4+ |
| JSTL 依赖 | javax.servlet:jstl:1.2 | jakarta.servlet.jsp.jstl:3.0 |
做 JSP 项目的工程结论是:非要用 JSP,就锁 Spring Boot 2.7.18,配套 mybatis-spring-boot-starter 2.3.2,这是经过大量项目验证的甜点参数组合。2.7.x 是 2.x 系列的终点,官方持续修复安全漏洞,适合中小系统。如果公司新项目硬性要求 Boot 3.x,就把 JSP 换成 Thymeleaf,模板引擎能力更强,页面标签风格和 JSTL 接近,迁移成本在可控范围内。
5.2 MyBatis 自动配置的黑匣子拆开看一眼
mybatis-spring-boot-starter 启动时做了什么,这是面试必问点,也是排查问题的底层知识。很多人把 MyBatis 初始化当成黑匣子,出了问题只会重启,其实整个流程可以简化成四步。
1. starter 读取 application.yml 里 mybatis.* 配置项 2. XMLConfigBuilder 解析 Configuration(对应源码里的 XMLConfigBuilder 类) 3. SqlSessionFactoryBean 持有 Configuration,构建 SqlSessionFactory 4. MapperScannerRegistrar 扫描 @Mapper 接口,注册代理对象Spring Boot 整合 MyBatis 后,开发人员写 Mapper 接口和 XML,容器帮你完成注册和代理。理解这个过程的价值在于:当你需要自定义 Configuration 行为,比如修改默认缓存策略、增加拦截器插件时,你知道该在哪个环节介入。下面是排查 SQL 问题最实用的配置:
mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true cache-enabled: false log-impl: org.apache.ibatis.logging.stdout.StdOutImpllog-impl是 MyBatis 配置打印 SQL 的开关,StdOutImpl 会把执行的 SQL 语句和参数直接打到控制台,排查动态 SQL 拼接问题几乎靠它。cache-enabled控制二级缓存,false表示关闭。前面说过的一级缓存和二级缓存实现差异很大:一级缓存是 SqlSession 级别,Web 请求每次新建会话,天然隔离;二级缓存跨 SqlSession 甚至跨事务,网上搜 mybatis 二级缓存实现能搜到一堆,但图书库存这种频繁更新的数据,开二级缓存就是在给自己埋雷。
5.3 换 MyBatis 版本时的依赖树排查套路
升级 MyBatis 或者 Spring Boot 之前,先看依赖树,这是最省时间的排查方式。mybatis-spring-boot-starter 自己拉了特定版本的 mybatis 和 mybatis-spring,它们和 Spring Boot 版本有固定配对关系,乱升版本最常见的结果就是启动时ClassNotFoundException或者NoSuchMethodError。
mvn dependency:tree -Dincludes=org.mybatis:*这个命令只过滤 mybatis 相关的依赖。在 mybatis-spring-boot-starter 2.3.2 下,你会看到 mybatis 3.5.13、mybatis-spring 2.1.2 这些版本,和 Spring Boot 2.7 的兼容性是有保障的。如果换成 mybatis-spring-boot-starter 3.0.4,它的依赖是 jakarta 版本,必须同步升级 Spring Boot 3.x,否则 javax 包冲突会让整个容器起不来。换版本前先跑这个命令,比启动后看红屏日志快得多。
6. 做熟之后,这套图书管理系统还能往哪里长
6.1 登录拦截器:闭环的最后一块拼图
图书管理系统的功能做完整后,还差一个登录拦截器把管理界面保护起来。不加拦截器,任何人都能直接访问/book/list,管理系统的意义就没了。一个简单的HandlerInterceptor足够:
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (request.getSession().getAttribute("loginUser") == null) { response.sendRedirect("/login"); return false; } return true; } }注册时要把登录页和静态资源放行,否则会死循环重定向:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/**") .excludePathPatterns("/login", "/css/**", "/js/**"); } }登录成功后把用户信息塞进 session,后面做超期提醒、借阅记录就能绑定到具体读者,jsp 个人信息展示页面也就有了真正的用户态数据。
6.2 借阅超期提醒:这个功能让系统真正“能用”
只把 CRUD 做完,系统还停留在演示层面。加一个每天定时跑的还书提醒任务,立刻有了运维味。Spring Boot 的@Scheduled一行注解就能搞定:
@Component public class BorrowRemindTask { @Resource private BorrowRecordMapper borrowRecordMapper; @Scheduled(cron = "0 0 8 * * ?") public void remind() { List<BorrowRecord> records = borrowRecordMapper.selectExpiring(); records.forEach(r -> System.out.println("提醒还书:" + r.getBookId())); } }cron = "0 0 8 * * ?"表示每天上午 8 点触发。别忘了在启动类加@EnableScheduling,否则这个任务根本不会被执行。selectExpiring对应的 SQL 查询status = 0 AND return_time < NOW()就能查出超期未还的记录,实际系统里把这个输出换成站内信或邮件即可。
6.3 收个尾,说点实战习惯
把这套项目完整做完后,我的习惯是在 README 里固定写四件事:启动命令、数据库初始化脚本、默认账号密码、要求的 Java 版本。JSP 项目在 IDEA 里直接 run 和打包成java -jar跑,行为会有细微差别,尤其是静态资源路径和 webapp 的加载时机。所以每次改完代码,我都会打一次包用命令跑一遍验证,这个习惯帮我拦下过很多“本地好好的,部署就挂”的尴尬。这套 springboot + mybatis + jsp 的实践链路,值得完整走一遍,希望帮到你。
本文还有配套的精品资源,点击获取