ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot+MyBatis+JSP图书管理系统实战:从搭建到避坑指南

SpringBoot+MyBatis+JSP图书管理系统实战:从搭建到避坑指南 简介面向Java Web初学者的图书管理系统完整项目包以Spring Boot为核心整合MyBatis持久层与JSP视图技术覆盖图书增删改查、分类管理、借阅归还和分页展示等功能模块适合课程设计或毕业设计实战。压缩包共1192个文件其中class编译产物、JSP页面、XML配置映射、Java源码与依赖JAR包构成主体另有少量静态资源与说明文档整体约95.3MB可直接导入开发工具运行目录结构清晰便于定位代码。项目采用MVC架构清晰展示数据库表设计、Mapper接口、Service业务处理、Controller控制及JSP渲染的完整开发链路同时包含分页插件和Spring Security权限控制等企业级扩展点便于在此基础上二次开发读者可对照代码逐步理解各层协作方式。目前已有2207人学习该项目包适合需要快速搭建图书管理项目或梳理Spring Boot与MyBatis整合思路的开发者参考使用。1. 还在用 JSP 做图书管理系统这个组合其实比想象中能打毕业设计、课程设计、甚至一些小团队的内部图书借阅工具springboot mybatis jsp 这套组合的出现频率高得离谱。很多人一听 JSP 就皱眉觉得是上古技术但 Spring Boot 官方虽然推荐模板引擎JSP 在 Servlet 容器里依然是合法且稳定的视图层方案——特别是导师指定必须用 JSP或者你要接手一个老项目时这套技术栈反而是最稳的选择。这篇笔记我会从工程骨架、表设计、Mapper 层、页面渲染一路讲到实际部署时最容易翻车的地方按着走能让你少走不少弯路。如果你正卡在「基于springboot的图书借阅管理系统的设计与实现」这类题目上这篇就是给你准备的。2. springboot mybatis jsp 选型与工程骨架先搞清楚为什么是它们仨2.1 三个组件各自的职责边界拿到这个标题先别急着建项目。我见过太多人一上来就点 Next结果 springboot 版本选了个 3.xJSP 页面怎么都访问不了折腾两天才发现是版本兼容问题。先明确三件事springboot 负责什么内嵌 Tomcat、自动配置、依赖管理把整个应用以 jar 包形式跑起来。它不关心页面长什么样。mybatis 负责什么SQL 与 Java 方法之间的映射把数据库表和实体类串起来。它不关心请求怎么来、响应怎么回。jsp 负责什么视图渲染把 Model 里的数据嵌进 HTML 模板。它不关心数据从哪来、业务怎么算。这三者的边界一旦清晰很多问题就能快速定位。页面 404你去看视图解析器SQL 查不出数据你去看 Mapper应用起不来你去看依赖冲突。分工明确是这套组合最大的优点也是新手最容易绕晕的地方——因为问题出现时你往往分不清该查哪一层。我自己的排查顺序永远是「先确认请求到了 Controller再看 Service 和 Mapper最后才看 JSP」这个习惯能帮你少做一半无用功。2.2 用 Spring Initializr 把工程骨架拉起来常见做法是去 Spring Initializr 网站或者 IDEA 内置的初始化向导生成基础工程。关键参数我一般这样填Group 填 com.exampleArtifact 填 bookmanager打包方式选 jar。虽然 JSP 开发中 war 更传统但 Spring Boot 内嵌 Tomcat 跑 jar 配合 src/main/webapp 目录下的 JSP 文件也能正常工作而且部署更省事。Java 版本选 8 或 11。springboot 版本选 2.7.x这是 JSP 方案里最少坑的组合。springboot 版本太高3.x意味着 Jakarta EE 迁移JSP 标签库依赖全变新手不建议碰。依赖勾选 Spring Web、MyBatis Framework、MySQL Driver。生成后我会手动把包结构理成下面这样后续写代码时定位非常快src/main/java/com/example/bookmanager ├── controller # 控制器接收请求、调 service、返回视图名 ├── service # 业务层处理借书、还书、超期计算 ├── mapper # MyBatis 的 Mapper 接口一个表对应一个 ├── entity # 实体类和数据库表字段一一对应 └── config # 拦截器、WebMvc 配置pom.xml 里除了 Spring Boot 父依赖和刚才勾选的 starter还要手动补两个 JSP 专用的依赖——这是新手最容易漏的一步dependency groupIdorg.apache.tomcat.embed/groupId artifactIdtomcat-embed-jasper/artifactId /dependency dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependencytomcat-embed-jasper 是让内嵌 Tomcat 能编译 JSP 的关键漏掉它页面会直接 404 或变成下载文件。jstl 是页面里做循环和条件判断的标签库后面渲染图书列表时必用。注意 Spring Boot 2.7.x 配的是 javax.servlet 坐标别去网上复制 jakarta 版本的依赖版本一错全部白搭。2.3 配置文件的三个关键段端口、数据源、视图解析application.yml 是这套组合的命脉。JSP 项目我最关注三段配置少一段都会出幺蛾子server: port: 8080 servlet: encoding: charset: UTF-8 force: true spring: datasource: url: jdbc:mysql://localhost:3306/bookdb?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mvc: view: prefix: /WEB-INF/jsp/ suffix: .jsp mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.bookmanager.entity configuration: map-underscore-to-camel-case: true先解释这三段各自的用途。第一段是服务端口和字符编码。JSP 页面中文乱码八成是这里没配force: true——这个参数强制请求和响应都走 UTF-8不配的话 Tomcat 默认用 ISO-8859-1 解码中文必乱。第二段是数据源。serverTimezoneAsia/Shanghai必须写不然新版 MySQL 驱动会报时区错误应用直接起不来。characterEncodingutf8保证中文字段读写不乱码这两个参数算是 MySQL 连接的标配。第三段最关键spring.mvc.view.prefix和suffix告诉 Spring Boot控制器返回bookList时去/WEB-INF/jsp/bookList.jsp找页面。如果你把 JSP 放在 webapp 根目录而不是 WEB-INF 下prefix 就要改成/或者去掉。这里有个容易踩的坑jar 包方式运行时JSP 文件必须位于src/main/webapp目录下Spring Boot 打包时才会把它们收进 jar如果你把 JSP 放在类路径的 static 或 templates 下运行时一定 404。mybatis 那段的mapper-locations指向 XML 映射文件的位置type-aliases-package让 XML 里写 resultType 时不用带全限定类名map-underscore-to-camel-case自动把数据库的create_time映射成实体类的createTime省去大量手写别名。这三项是我每次新建 mybatis 项目都会先配好的后面写代码会非常顺畅。3. 数据库设计与 MyBatis 映射层把图书借阅的增删改查落成代码3.1 四张核心表图书、读者、借阅记录、管理员图书管理系统的核心数据模型我一般拆成四张表。最少用两张图书加借阅记录也能跑但要支持登录和读者管理就得四张。建表 SQL 大概长这样CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(20) NOT NULL, title VARCHAR(100) NOT NULL, author VARCHAR(50), publisher VARCHAR(100), category VARCHAR(50), stock INT DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE reader ( id INT PRIMARY KEY AUTO_INCREMENT, card_no VARCHAR(20) UNIQUE NOT NULL, name VARCHAR(50) NOT NULL, phone VARCHAR(20), max_borrow INT DEFAULT 5 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE borrow_record ( id INT PRIMARY KEY AUTO_INCREMENT, book_id INT NOT NULL, reader_id INT NOT NULL, borrow_time DATETIME, due_time DATETIME, return_time DATETIME, status TINYINT DEFAULT 0 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE admin_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(100) NOT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有两个细节值得强调。第一status字段用 TINYINT 存借阅状态0 表示借出未还1 表示已还。不要用 VARCHAR 存已借/已还状态判断、统计超期、写 SQL 聚合都要跟字符串打交道麻烦得很。第二borrow_record里的book_id和reader_id要建索引因为列表查询和超期统计都会按这两个字段做关联和过滤。字段名用下划线风格配合前面配置里的map-underscore-to-camel-case开关实体类里直接写 camelCase 属性Mapper XML 里不用手动做别名映射——这是一个能省大量重复代码的细节。如果你的系统不需要读者管理可以砍掉 reader 表在 borrow_record 里直接存借阅人姓名。这个取舍看需求但大多数毕设和课设题目都带读者信息管理四张表是我的默认方案。3.2 Mapper 接口与 XML 的对应关系参数传递的三种姿势MyBatis 的 Mapper 接口和 XML 通过命名空间绑定。我一般这样写接口Mapper public interface BookMapper { ListBook selectByCondition(Param(title) String title, Param(category) String category, Param(offset) int offset, Param(limit) int limit); Book selectById(Param(id) Integer id); int insert(Book book); int updateById(Book book); int deleteById(Param(id) Integer id); }对应的 XML 映射文件mapper namespacecom.example.bookmanager.mapper.BookMapper select idselectByCondition resultTypeBook SELECT * FROM book where if testtitle ! null and title ! AND title LIKE CONCAT(%, #{title}, %) /if if testcategory ! null and category ! AND category #{category} /if /where ORDER BY id DESC LIMIT #{offset}, #{limit} /select insert idinsert useGeneratedKeystrue keyPropertyid INSERT INTO book (isbn, title, author, publisher, category, stock) VALUES (#{isbn}, #{title}, #{author}, #{publisher}, #{category}, #{stock}) /insert update idupdateById UPDATE book SET title #{title}, author #{author}, publisher #{publisher}, category #{category}, stock #{stock} WHERE id #{id} /update /mapper参数传递有三种姿势。第一种是单个参数XML 里用 #{任意名字} 都能取到。第二种是用 Param 标注多个参数XML 里必须用注解里写的名字——我上面 selectByCondition 就是标准的多参数写法。第三种是传实体对象XML 里直接用 #{属性名}insert 和 update 用的就是这种。我比较推荐多参数场景强制用 Param因为可读性好多个参数顺序乱了也不怕——不用 Param 的话 MyBatis 会按 arg0、arg1 定位SQL 写长了很容易搞错。resultTypeBook能直接写短类名靠的就是 2.3 节配置里的type-aliases-package。没有这个配置就得写全限定名com.example.bookmanager.entity.Book长且容易拼错。XML 里where标签会自动处理第一个 AND 关键字这也是 MyBatis 故意设计的功能后面避坑章节我会细讲。3.3 二级缓存与条件查询缓存开了反而翻车的场景mybatis 的一级缓存是 SqlSession 级别的默认开启但 Spring 整合后每次数据库操作都是新的 SqlSession一级缓存基本感受不到。二级缓存是 namespace 级别的跨 SqlSession 共享。要在某个 Mapper 上开启XML 里加一行cache evictionLRU flushInterval60000 size1024 readOnlytrue/这个配置的意思是缓存淘汰策略用 LRU最近最少使用60 秒刷新一次最多缓存 1024 个对象引用只读模式。图书这种低频修改的数据开二级缓存确实能减少数据库压力但注意readOnlytrue时MyBatis 直接返回缓存对象的引用。如果业务层改了返回的 Book 对象下次查询拿到的就是被污染的数据。readOnlyfalse时 MyBatis 会序列化拷贝再返回但要求实体类实现 Serializable 接口。我见过最多的翻车场景是开了二级缓存管理员在后台改了图书库存前台列表页还是旧数据。原因就是 UPDATE 操作没有让缓存失效或者自定义查询 SQL 绕过了缓存管理。MyBatis 默认 insert/update/delete 会清当前 namespace 的缓存但你要是开了缓存又写了多表 join 查询脏数据还是会出现。我的建议很直接图书管理系统这个体量二级缓存属于锦上添花别为了展示技术硬开。等流量真大到需要缓存了你该上的是 Redis而不是在 mybatis 的 XML 里纠结。4. JSP 页面与 Spring MVC 对接让页面真正动起来4.1 列表页JSTL EL 表达式渲染图书列表控制器返回视图名后JSP 页面负责把数据渲染成表格。列表页的核心代码长这样% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html body h2图书列表/h2 table border1 tr thISBN/thth书名/thth作者/thth库存/thth操作/th /tr c:forEach items${bookList} varbook tr td${book.isbn}/td td${book.title}/td td${book.author}/td td${book.stock}/td td a href/book/edit/${book.id}编辑/a a href/book/delete/${book.id} onclickreturn confirm(确定删除?)删除/a /td /tr /c:forEach /table /body /html注意第一行 contentType 里的 charsetUTF-8少了它页面中文必乱码——这是 JSP 页面最常见的低级错误没有之一。${bookList}是控制器通过 Model 传过来的集合名字必须和控制器里model.addAttribute(bookList, list)的 key 完全一致。c:forEach的 items 是集合var 是循环变量这两个属性是 JSTL 循环的标准写法。很多新手在这里翻车就是因为 key 对不上页面 500 或直接空白。控制器的写法我也顺手贴一下Controller RequestMapping(/book) public class BookController { Autowired private BookMapper bookMapper; GetMapping(/list) public String list(Model model) { ListBook books bookMapper.selectAll(); model.addAttribute(bookList, books); return bookList; } }这里有个 Spring MVC 的重要机制Controller 加 GetMapping方法返回 String 时Spring 会把返回值交给视图解析器拼接成完整路径。你看到返回值bookList实际去找的就是/WEB-INF/jsp/bookList.jsp这就是 2.3 节里 prefix 和 suffix 的用途。如果你改成return redirect:/book/list含义就变了——那是告诉浏览器发一个新请求而不是转发到 JSP。4.2 表单提交与回显新增图书和编辑图书共用页面图书管理系统的增改操作我通常让新增和编辑共用一个 form.jsp 页面。控制器在 GET 请求里把要编辑的 Book 塞进 ModelJSP 页面用 EL 表达式回显form action/book/save methodpost input typehidden nameid value${book.id}/ 书名: input typetext nametitle value${book.title}/br/ 作者: input typetext nameauthor value${book.author}/br/ 分类: select namecategory option value文学 ${book.category 文学 ? selected : }文学/option option value科技 ${book.category 科技 ? selected : }科技/option option value历史 ${book.category 历史 ? selected : }历史/option /selectbr/ 库存: input typetext namestock value${book.stock}/br/ button typesubmit保存/button /form新增时 ${book} 是 nullEL 表达式对 null 的取值会直接输出空字符串所以这个页面新增和编辑都能用。编辑时后端把 book 对象塞进 Model表单自动回显旧值用户改完提交Controller 里判断 id 是否为空来决定 insert 还是 update。这是最省事的做法也是我强烈推荐的模式——不要为新增和编辑各写一个页面维护成本翻倍还容易改出不一致的问题。控制器接收时要注意表单字段的 name 属性要和 Book 实体的属性名一致。Spring MVC 会自动做类型转换表单提交的字符串12会转成 Integer 的 12。如果用户填了非数字内容会抛 MethodArgumentTypeMismatchException页面直接 400。生产环境最好在 Controller 里加异常处理或前端校验但课设和毕设阶段保证输入合法即可。4.3 图片坐标定位与刷新一次两个 JSP 里常见的小问题热搜词里有「jsp图片如何对坐标定位」和「jsp页面让加载完后刷新一次」我放在一起说掉。图片坐标定位常见场景是图书封面点击区域或楼层导览图。JSP 页面本质是 HTML用map标签就能做热点区域img src/static/img/floor.jpg usemap#floorMap width600 height400/ map namefloorMap area shaperect coords10,10,200,150 href/book/list?categoryliterature alt文学区/ area shapecircle coords300,200,50 href/book/list?categorytech alt科技区/ /mapcoords 的四个值对应矩形的左上角和右下角像素坐标circle 的三个值是圆心 x、圆心 y 和半径。坐标从图片左上角算起单位是像素。你如果不知道具体坐标浏览器按 F12 打开开发者工具鼠标悬停到图片区域控制台会显示当前像素位置——这是我做楼层导览和座位预约功能时常用的土办法比反复改参数刷新页面快得多。至于「加载完后刷新一次」常见需求是页面加载后自动从数据库拉取最新数据或者强制清除浏览器缓存刷新。实现方式有两种。一种是 JSP 页面加一段 JavaScriptscript window.onload function() { if (!sessionStorage.getItem(refreshed)) { sessionStorage.setItem(refreshed, true); location.reload(); } }; /script另一种是服务端响应头控制RequestMapping(value /book/list, produces text/html;charsetUTF-8) public String list(Model model, HttpServletResponse response) { response.setHeader(Cache-Control, no-cache, no-store, must-revalidate); ListBook books bookMapper.selectAll(); model.addAttribute(bookList, books); return bookList; }第一种适合页面初始化时需要自动刷新一次的场景用 sessionStorage 标记防止无限刷新死循环第二种适合强制浏览器不缓存页面每次访问都拿最新服务端数据。具体选哪种取决于诉求——防缓存用第二种初始化数据用第一种。但这里的坑也不少第五节我会专门讲一个血泪教训。5. 避坑指南这套组合里最常翻车的 6 个问题5.1 mybatis 条件不生效 标签的玄学现象按书名和分类查询时条件时灵时不灵有时候查出来是全表数据。原因SQL 里直接手写了WHERE title #{title}没走where标签。当 title 是空字符串时SQL 实际拼接成WHERE title 自然查不到数据。或者用了if但 test 条件只写了title ! null没判断空字符串。解决所有条件查询都用whereif组合test 里必须同时判断 null 和空字符串。我在 3.2 节里的写法是标准模板直接抄就能避免这个问题。另外注意where标签会自动去掉第一个多余的 AND——这是 MyBatis 设计好的行为别自己手写WHERE 11再拼 AND那是老 JDBC 时代的做法在 mybatis 里既有注入风险又影响性能。5.2 springboot 版本太高导致 JSP 无法解析现象pom 里用了 springboot 3.xJSP 页面访问报 404 或者直接白屏Tomcat 日志里找不到 JSP 编译相关的信息。原因Spring Boot 3.x 基于 Jakarta EE 9javax.servlet全部迁移成jakarta.servletJSP 标签库和 Jasper 的依赖坐标系全变了。网上大部分 JSP 教程的写法是旧的 javax 坐标套到 3.x 上必然翻车。解决如果你非要用 JSPspringboot 版本老老实实选 2.7.xJava 8依赖用 javax.servlet 坐标。如果你想用 3.x把视图层换成 Thymeleaf——Spring Boot 官方也推荐这个方案JSP 在 3.x 里属于「能跑但费劲」。这个决策在项目启动第一天就该定下来因为改版本意味着部分依赖重写成本很高。经常有人问 springboot 版本怎么选我的经验是教学项目和内部工具求稳用 2.7.x新项目不碰 JSP 才上 3.x。5.3 mybatis 配置打印 SQL 的两种方式现象代码没报错但查出来的数据不对想看看 mybatis 实际执行了什么 SQL。原因mybatis 默认不打印 SQL日志级别没开。很多人以为加了log-impl: StdOutImpl就够了其实这是个常见的理解误区。解决方式一在 application.yml 里加mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl方式二针对 Mapper 包开 debug 日志logging: level: com.example.bookmanager.mapper: debug第一种直接把 SQL 打到控制台适合本地调试简单粗暴。第二种走 logback 日志体系生产环境可以调级别关闭输出适合线上排查。我建议本地用第一种省事直观上了测试环境用第二种方便保留日志定位问题。注意方式二的包名必须和 Mapper 接口所在的包完全一致写错一个字母就静默失效你还会怀疑是配置格式写错了。5.4 mybatis 缓存导致的数据不一致现象管理员改了图书库存前台列表还是旧数据重启应用或手动清缓存后恢复。原因开了二级缓存且 UPDATE 操作命中的缓存没有及时刷新或者自定义查询 SQL 绕过了缓存管理。MyBatis 默认的 insert/update/delete 会清缓存但如果你开了缓存又用了多表关联查询缓存 key 的管理会非常混乱。解决按 3.3 节的思路小系统别开二级缓存把cache标签直接去掉。如果实在要缓存把readOnly设为 false 并让实体实现 Serializable。我的最终建议是图书管理系统这个量级数据库压力根本轮不到 mybatis 二级缓存来解决把精力放在分页查询的索引优化上收益大得多。mybatis 缓存是面试题里经常问的热门话题但生产环境的使用要极度克制。5.5 jsp 页面加载完后自动刷新导致表单数据丢失现象用了 4.3 节那段window.onload location.reload()脚本后用户填了一半的表单被清空或者页面每次进入都多刷一次。原因location.reload()会重新发起当前页面的请求表单里未提交的数据全部丢失。而且 sessionStorage 的标记位在同一标签页里会一直存在第二次进入页面虽然不会无限刷新了但第一次进入时数据已经被清了。这个方案放列表页没什么感觉放表单页就是事故。解决刷新逻辑只放在列表页或数据展示页表单页绝对不要加。如果你是防缓存优先用服务端响应头方案。如果是为了拿最新数据正确做法是在 Controller 里查完数据再渲染而不是让页面刷新。刷新的粒度要控制好——「页面加载后自动刷新」这个需求听起来简单但对用户输入场景的破坏力极大能不用就不用。5.6 静态资源被 Spring MVC 拦截现象JSP 页面里的 CSS、JS、图片全部 404浏览器控制台显示静态资源请求异常。原因请求路径/css/style.css被 DispatcherServlet 拦截Spring 尝试把这个路径当成 Controller 方法处理自然找不到对应 handler。这个问题在 JSP 项目里几乎必踩一次——JSP 放在 WEB-INF 下它引用的相对路径是基于当前请求 URL 计算的目录结构一深相对路径就全乱。解决统一用/static/前缀引用静态资源并在配置类里注册资源映射Configuration public class WebConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/static/**) .addResourceLocations(classpath:/static/); } }然后把页面里的 CSS、JS、图片引用全部改成/static/css/style.css这种绝对路径。这个配置是 Spring Boot 的默认行为理论上不写也能用——但当你手动配置了 WebMvcConfigurer 或拦截器时默认的静态资源映射可能被覆盖这时候就必须显式声明。我一般会在项目第一天就把这个配置类写上省得后面被静态资源 404 打断节奏。6. 收尾把分页和登录拦截补上才算能交差的图书管理系统到这步增删改查已经跑通但作为图书管理系统还差两个标配功能分页和登录拦截。分页我建议用 PageHelper它和 Spring Boot 整合极其简单。依赖加进 pom 后在 MyBatis 查询前调用PageHelper.startPage(pageNum, pageSize)插件会自动为下一条查询生成 LIMIT 语句。Controller 写法GetMapping(/list) public String list(RequestParam(defaultValue 1) int pageNum, RequestParam(defaultValue 10) int pageSize, Model model) { PageHelper.startPage(pageNum, pageSize); ListBook books bookMapper.selectAll(); PageInfoBook pageInfo new PageInfo(books); model.addAttribute(pageInfo, pageInfo); return bookList; }这里有一个 PageHelper 的经典坑startPage只对紧随其后的第一条查询生效中间不能穿插其他数据库操作。JSP 页面里用${pageInfo.list}渲染当前页数据${pageInfo.total}显示总数${pageInfo.pages}显示总页数生成上一页下一页链接时把 pageNum 传给 Controller 即可。登录拦截用 HandlerInterceptor 实现注册一个 WebMvcConfigurer 的 Bean重写addInterceptors方法放行登录接口和静态资源其余请求校验 session 里的用户标记。这个功能不算复杂但这是交付验收时最容易被打回扣分的地方——没有登录的图书管理系统就像没有门的图书管谁都能进。做这个标题的项目最大的经验我放在最后说不要为了显得高级去引入 Redis、MQ 或者前后端分离这套组合的价值就在于简单、直接、好维护。我见过太多人把图书管理系统做成微服务架构结果连跑起来都要开五个窗口最后 de 的时间比码代码还多。把 springboot mybatis jsp 这套组合的表层逻辑吃透比堆技术栈有用得多——毕竟你是要交差不是要造火箭。希望帮到你。本文还有配套的精品资源点击获取
返回列表