ARTICLE DETAIL

资讯详情

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

SpringBoot+Vue前后端分离图书管理系统实战:从CRUD到联调排错全解析

SpringBoot+Vue前后端分离图书管理系统实战:从CRUD到联调排错全解析 上周刚帮朋友把一个图书管理系统调通技术栈就是SpringBootMySQL前端用Vue前后端分离核心功能是图书信息的增删查改。这个项目看起来简单实际上隐藏的坑不少数据库驱动版本不匹配、跨域请求被拦截、IDEA创建SpringBoot项目时依赖选错这些我都替大家踩过一遍。今天把完整的实现思路、关键代码和排错经验整理出来适合刚学完Java基础、想拿一个能跑起来的项目练手的朋友也适合做课程设计时参考。后面所有内容我都会按实际动手的顺序来写先讲设计思路再讲环境准备、后端接口、前端页面最后把联调过程中最容易出问题的地方集中列一遍。跟着做最终结果是一个网页上能看到图书表格支持新增、编辑、删除、查询页面操作会立刻同步到MySQL数据库。1. 项目整体设计与思路拆解1.1 这个项目到底是做什么的图书管理系统的核心业务很朴素维护一张图书信息表管理员在网页上输入书名、作者、价格等信息可以存到数据库里。之后可以随时查看列表、修改某本书的信息、删除不要的记录或者根据关键字找到某本书。这就是标准的增删查改也叫CRUD。不要小看这四个字。任何一个信息管理类项目不管是后台管理系统、电商平台还是进销存软件底层都逃不开对某张表的增删查改。把图书管理系统跑通等于把Web开发里最关键的“数据流转”链路跑通了。前端负责交互后端负责业务逻辑和数据库访问数据库负责持久化存储。我当时给朋友定的功能就三条图书列表查询进入页面默认显示全部图书支持按书名关键字搜索。新增图书弹窗填写表单保存后列表刷新。编辑和删除点击行上的按钮可以对这本书做修改或删除。没有加登录、权限、分页因为一旦加了这些学习时会分散注意力。等基础CRUD跑通再自己往上面扩展分页、模糊查询、登录认证难度会小很多。1.2 为什么选择SpringBoot MySQL 前后端分离SpringBoot现在是Java后端开发的事实标准。它内置了Tomcat默认使用Maven管理依赖还提供了大量的自动配置。过去用SSM搭建项目要手写一堆XML配置文件还要处理Spring和SpringMVC之间的整合。SpringBoot把这些默认配置都封装好了只要在pom.xml里引入依赖写一个启动类就能跑起一个Web应用。对初学者来说最大的好处是“少碰配置多写业务”。MySQL同样是很多小中型项目的首选数据库免费、易安装、资料多。它和SpringBoot配合非常成熟JDBC驱动稳定ORM框架也支持得很好。更重要的是市面上大部分课程设计和技术教程都用MySQL遇到问题时搜到的答案也多。前后端分离则是现在前端工程化的主流做法。简单说后端不再直接返回HTML页面而是返回JSON数据前端负责渲染页面、发起请求、展示数据。一个SpringBoot后端可以同时服务PC网页、小程序、移动App只要前端发HTTP请求拿到JSON自己去展示即可。前后端通过RESTful接口交互开发时两边可以并行推进调试时只要用Postman先测接口再让前端对接出了问题也容易定位。1.3 增删查改的完整链路我用最通俗的方式描述一次“新增图书”的完整流程用户在网页上填写图书信息点击保存。Vue前端用axios发起POST请求把表单数据转成JSON发送到后端接口。SpringBoot的Controller接收JSON把参数绑定到实体类对象。Service层处理业务逻辑调用Mapper接口。Mapper通过MyBatis把数据写入MySQL。后端返回成功提示给前端前端重新请求列表接口并刷新页面。其他三个操作也类似查询是GET请求修改是PUT请求删除是DELETE请求。所以后端设计的好坏直接决定了前端对接是否顺畅。RESTful接口风格把URL和HTTP方法天然地对应到操作上比如GET /api/books表示查列表POST /api/books表示新增PUT /api/books/1表示修改id为1的图书DELETE /api/books/1表示删除。这种风格约定清晰前后端沟通时不容易产生歧义。2. 环境准备与项目初始化2.1 基础环境清单动手之前先把开发环境准备好。我平时用的版本组合不一定是最新但一定稳妥。下面这个表格可以直接照着装软件推荐版本说明JDK1.8 或 11SpringBoot 2.x 推荐JDK 8/11SpringBoot 3.x需要JDK17Maven3.6.3 以上管理依赖IDEA自带也可以IDEA2021.3 及以上社区版也可以用创建SpringBoot项目没问题MySQL5.7 或 8.0建议8.0驱动注意用com.mysql.cj.jdbc.DriverNavicat任意版本数据库可视化工具不用命令行也方便Node.js14.18 以上前端项目运行环境npm依赖安装需要Vue CLI4.5 以上用Vite创建项目也可以这里以CLI为例MySQL安装配置教程网上很多我只提醒三个坑安装8.0版本时选择字符集为utf8mb4root密码尽量用纯数字加字母避免特殊字符在JDBC连接串里转义问题安装完成后用Navicat连接测试能连上再继续。2.2 IDEA创建SpringBoot项目打开IDEA选择新建Project左侧选Spring Initializr右侧配置好Group和Artifact。Group一般写com.exampleArtifact写book-manage。JDK版本选择1.8或者11如果打算用SpringBoot 2.7.xJDK选1.8完全够用。然后选择依赖。想要快速跑通CRUD最少需要这几个Spring Web提供SpringMVC和内置Tomcat负责接收HTTP请求。MySQL DriverJDBC连接MySQL。MyBatis FrameworkORM框架把Java对象映射成SQL参数。Lombok简化实体类代码自动生成getter/setter减少样板代码。选完依赖后点击FinishIDEA会自动下载依赖并创建项目结构。这里我特别提醒一句SpringBoot版本别一味追新。SpringBoot 3.x现在也很普及但它从JDK8升到了JDK17并且原来的javax.servlet包改成了jakarta.servlet很多老教程里的代码直接拷过来会报错。如果你刚开始学建议在pom.xml里把SpringBoot版本固定到2.7.18或类似的2.x版本等把原理弄明白了再升级也不迟。2.3 数据库设计一张表搞定图书信息图书管理系统的数据库表设计非常简单一张表就够。打开Navicat创建一个数据库book_db然后执行下面的建表语句CREATE DATABASE IF NOT EXISTS book_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE book_db; CREATE TABLE book ( id int(11) NOT NULL AUTO_INCREMENT, book_name varchar(100) NOT NULL COMMENT 书名, author varchar(50) DEFAULT NULL COMMENT 作者, price decimal(10,2) DEFAULT NULL COMMENT 价格, publisher varchar(100) DEFAULT NULL COMMENT 出版社, create_time datetime DEFAULT NULL COMMENT 创建时间, PRIMARY KEY (id) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENT图书表;这里有几个字段设计的细节要注意id用自增主键Int够用不用BigInt因为简单项目不会达到几十亿数据。price用decimal(10,2)不要用float或double否则涉及金额计算时容易出精度问题。book_name加了not null避免写入空书名。create_time可以给前台展示用后台插入数据时手动设置当前时间。字段类型选对了后面Java实体类映射时就不用反复转换。3. 后端核心实现从实体类到接口3.1 数据访问层MyBatis-Plus还是JPA实现CRUD的方式很多常见的是Spring Data JPA和MyBatis。我在这个项目里选择了MyBatis-Plus因为它在MyBatis基础上封装了内置的CRUD方法不需要自己写XML和SQL非常适合快速开发。使用MyBatis-Plus需要额外添加依赖。在pom.xml中加入dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency实体类对应数据库表的字段Data TableName(book) public class Book { TableId(type IdType.AUTO) private Integer id; private String bookName; private String author; private BigDecimal price; private String publisher; private Date createTime; }TableName指定实体类对应的表名TableId标记主键并设置为自增。book_name这种数据库下划线字段MyBatis-Plus默认开启驼峰映射会自动映射到bookName不需要额外配置。Mapper接口写法更简单public interface BookMapper extends BaseMapperBook { }继承BaseMapper之后MyBatis-Plus已经帮我们实现了selectList、selectById、insert、updateById、deleteById这些常用方法。在一张单表面前几乎不需要写一行SQL业务代码会清爽得多。3.2 业务层与ControllerRESTful接口设计Service层负责封装业务逻辑。虽然这个项目业务不复杂但建议保留Service层养成好习惯。定义一个BookService接口再写实现类public interface BookService { ListBook listBooks(String keyword); Book getBookById(Integer id); boolean addBook(Book book); boolean updateBook(Book book); boolean deleteBook(Integer id); }实现类注入BookMapper调用MyBatis-Plus提供的方法。列表查询带关键字搜索时可以用QueryWrapper构造条件Override public ListBook listBooks(String keyword) { QueryWrapperBook wrapper new QueryWrapper(); if (keyword ! null !keyword.isEmpty()) { wrapper.like(book_name, keyword); } wrapper.orderByDesc(id); return bookMapper.selectList(wrapper); }like的用法要留意wrapper.like(book_name, keyword)生成的SQL是WHERE book_name LIKE %keyword%适合模糊查询。Controller层暴露接口RestController RequestMapping(/api/books) public class BookController { Autowired private BookService bookService; GetMapping public Result list(RequestParam(required false) String keyword) { return Result.success(bookService.listBooks(keyword)); } GetMapping(/{id}) public Result getById(PathVariable Integer id) { return Result.success(bookService.getBookById(id)); } PostMapping public Result add(RequestBody Book book) { book.setCreateTime(new Date()); bookService.addBook(book); return Result.success(null); } PutMapping(/{id}) public Result update(PathVariable Integer id, RequestBody Book book) { book.setId(id); bookService.updateBook(book); return Result.success(null); } DeleteMapping(/{id}) public Result delete(PathVariable Integer id) { bookService.deleteBook(id); return Result.success(null); } }这里我用了一个统一返回类Result结构很简单Data public class Result { private Integer code; private String message; private Object data; public static Result success(Object data) { Result result new Result(); result.setCode(200); result.setMessage(success); result.setData(data); return result; } }统一返回格式的好处是前端可以固定解析code、message、data三个字段不需要每次对接时临时约接口格式。3.3 跨域配置前后端联调绕不开的坑前后端分离开发时前端跑在http://localhost:5173后端跑在http://localhost:8080。浏览器默认禁止一个源访问另一个源的接口这就是跨域问题。如果不在后端配置跨域前端会报Access to XMLHttpRequest has been blocked by CORS policy之类的错误。解决方式有很多种最简单的是在后端加一个CORS配置类Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedMethod(*); config.addAllowedHeader(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }addAllowedOriginPattern(*)表示允许所有来源addAllowedMethod(*)表示允许所有HTTP方法addAllowedHeader(*)允许所有请求头。开发阶段这么写最省事生产环境再按需收紧。如果项目里有Spring Security还需要在SecurityConfig里再放行一次OPTIONS请求但当前这个简单项目没引入Security可以做忽略处理。跨域配置是前后端联调时必然遇到的问题建议提前写在项目里。4. 前端页面实现Vue Axios4.1 创建Vue项目前端我选择Vue 3 Vite Element Plus。Vite启动速度快组件库用起来顺手页面代码也直观。假设已经在系统里安装了Node.js打开命令行执行npm create vitelatest frontend -- --template vue cd frontend npm install再安装前端必备依赖npm install axios npm install element-plus然后在main.js中引入Element Plusimport { createApp } from vue import App from ./App.vue import ElementPlus from element-plus import element-plus/dist/index.css const app createApp(App) app.use(ElementPlus) app.mount(#app)这里要说一句前端项目创建看似简单但很多人卡在npm下载慢、版本不兼容上。如果npm安装依赖很慢可以用淘宝镜像源具体命令大家可以自己查我不展开多说。4.2 封装axios请求前端请求接口不能每次都写完整的axios配置最好封装到一个模块里。我习惯在src/utils/request.js里统一封装import axios from axios import { ElMessage } from element-plus const request axios.create({ baseURL: http://localhost:8080/api, timeout: 10000 }) request.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res }, error { ElMessage.error(error.message) return Promise.reject(error) } ) export default request这里把后端统一返回结构处理掉了前端组件里只需要关注data字段。如果有登录需求一般会在请求拦截器里加入tokenrequest.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config })我这个项目没做登录token处理先不写进主流程但封装思路是一样的将来扩展登录功能时直接加拦截器即可。再写一个api模块比如src/api/book.jsimport request from /utils/request export function listBooks(params) { return request({ url: /books, method: get, params }) } export function addBook(data) { return request({ url: /books, method: post, data }) } export function updateBook(id, data) { return request({ url: /books/${id}, method: put, data }) } export function deleteBook(id) { return request({ url: /books/${id}, method: delete }) }路径全部写成相对路径baseURL统一替换。以后如果后端接口改了域名只需要修改request.js里的baseURL不用一个个组件去改。4.3 图书列表、新增、编辑、删除页面页面主体是一张表格外加一个新增按钮和搜索框。代码思路是这样的页面初始化时调用listBooks接口把返回的数组渲染到表格里。点击新增按钮弹出一个Dialog表单内容清空。点击编辑按钮把当前行数据复制到表单再打开Dialog。点击删除按钮先弹确认框确认后调删除API。新增和编辑提交成功后重新加载列表。核心模板部分大概长这样template div classcontainer el-input v-modelkeyword placeholder请输入书名 stylewidth: 200px / el-button typeprimary clickloadData查询/el-button el-button typesuccess clickopenAdd新增/el-button el-table :datatableData border stripe el-table-column propid labelID width80 / el-table-column propbookName label书名 / el-table-column propauthor label作者 / el-table-column propprice label价格 / el-table-column proppublisher label出版社 / el-table-column label操作 width180 template #default{ row } el-button sizesmall clickopenEdit(row)编辑/el-button el-button sizesmall typedanger clickhandleDelete(row.id)删除/el-button /template /el-table-column /el-table el-dialog v-modeldialogVisible :titleform.id ? 编辑图书 : 新增图书 width400px el-form :modelform label-width80px el-form-item label书名 el-input v-modelform.bookName / /el-form-item el-form-item label作者 el-input v-modelform.author / /el-form-item el-form-item label价格 el-input v-modelform.price / /el-form-item el-form-item label出版社 el-input v-modelform.publisher / /el-form-item /el-form template #footer el-button clickdialogVisible false取消/el-button el-button typeprimary clickhandleSave保存/el-button /template /el-dialog /div /template脚本部分script setup import { ref, onMounted } from vue import { listBooks, addBook, updateBook, deleteBook } from /api/book const keyword ref() const tableData ref([]) const dialogVisible ref(false) const form ref({}) function loadData() { listBooks({ keyword: keyword.value }).then(res { tableData.value res.data }) } function openAdd() { form.value {} dialogVisible.value true } function openEdit(row) { form.value { ...row } dialogVisible.value true } function handleSave() { if (form.value.id) { updateBook(form.value.id, form.value).then(() { dialogVisible.value false loadData() }) } else { addBook(form.value).then(() { dialogVisible.value false loadData() }) } } function handleDelete(id) { ElMessageBox.confirm(确定删除该图书吗, 提示, { confirmButtonText: 确定, cancelButtonText: 取消, type: warning }).then(() { deleteBook(id).then(() { loadData() }) }) } onMounted(() { loadData() }) /script这里有个细节值得提一下编辑时用展开运算符{ ...row }复制当前行数据而不是直接用row引用避免修改表单时直接误改了表格里已经渲染的数据。这个小坑我见过很多次。前端整体代码并不复杂核心就是axios请求后端接口再把返回数据绑定到表格。掌握这个模式之后换成其他管理后台项目也只是换换字段和接口地址。5. 联调测试与常见问题排查实录5.1 启动顺序与常见报错后端启动很简单IDEA里直接运行启动类BookManageApplication控制台出现Tomcat started on port(s): 8080说明启动成功。前端启动是在frontend目录下执行npm run devVite默认跑在5173端口启动后浏览器访问http://localhost:5173。如果直接访问页面大概率会有跨域问题因为前端5173向后端8080发请求。我一开头已经写了CorsConfig这个配置会在后端启动时自动生效。如果你用的是Vue CLI也可以在vue.config.js里配置devServer代理那样就不需要后端跨域配置但要注意代理只对开发环境生效。我更喜欢后端配CORS因为生产环境也是同一个后端服务在支撑访问起来不用改前端配置。启动过程中最常见的错误第一个是数据库连接失败。比如提示Access denied for user rootlocalhost说明MySQL用户名或密码不对。再比如提示Could not create connection to database server很可能是JDBC驱动版本不匹配。SpringBoot 2.7默认自带的是MySQL 8.0驱动如果你的MySQL是5.7一般也能兼容但如果提示SSL连接问题需要检查一下URL。5.2 问题速查表我把联调时最常遇到的问题整理成了一张表方便大家对照排查问题现象可能原因解决办法后端启动即报错pom.xml依赖下载失败或数据库没启动检查网络刷新Maven启动MySQL服务提示Access denied用户名或密码错误核对数据库账号密码必要时在Navicat里重新连接测试提示Unknown database数据库名写错确认application.yml中的url库名存在提示Communications link failureMySQL端口不是3306或url端口写错确认url端口例如localhost:3306前端请求跨域报错没配置CORS或配置没生效重新检查CorsConfig并重启后端前端请求返回404接口路径不对先看后端Controller的RequestMapping再比对前端api里的路径新增或编辑后列表没刷新没有调用loadData在保存成功后加一句loadData()页面表格中文乱码数据库连接url没有指定utf8字符集url后加?characterEncodingutf8Lombok生成的getter/setter找不到IDEA没装Lombok插件安装插件并重启IDEA注意pom里也加了依赖SpringBoot 3.x项目引用了javax包版本太高包名换成jakarta换成SpringBoot 2.7.x或改import为jakarta这张表是我多年实践中的经验浓缩大部分情况都能直接对号入座。5.3 几个提升效率的小技巧第一接口调试一定要用Postman或者Apifox。前端页面还没写好的时候后端接口是否正常完全可以用工具先验证。比如直接用POST请求往http://localhost:8080/api/books发送JSON看看数据库会不会多一条数据。这个流程走通再和后端、数据库相关的问题区分开后面真正遇到问题时排查范围会小很多。第二IDEA里可以安装RestfulTool插件启动后会列出项目里所有接口点击就能快速发起请求不用每次手动写URL。另一个插件是MyBatisX它能在service和mapper之间快速跳转写代码时会顺手很多。第三开发时把SpringBoot的热部署打开改完Java代码自动重启后端。在pom.xml里加入spring-boot-devtools依赖IDEA设置里开启Build project automatically就能省去手动重启的时间。但要注意热部署只在开发阶段用生产环境不需要。最后还想分享一点整个项目跑通之后我个人最大的感受是练习CRUD项目最重要不是背代码而是理解数据是怎么流动的。从浏览器的表单到后端Controller再到Service、Mapper、MySQL每一步都有对应的调试手段。只要把这条链路弄清楚以后不管切换到什么框架遇到问题都会有排查思路。另外别追求一步到位。我见过很多朋友一开始就想做一个“完整系统”于是加了登录、权限、分页、上传图片、导出Excel结果最后连核心功能都没跑通。图书管理系统最好的练习方式就是先用手写的方式把表格和表单做出来把数据库读写调通再慢慢往外面包功能。先跑通再扩展这句话在编程学习里永远不会过时。
返回列表