
1. 先搞清楚前后端之间的“连接”到底连的是什么我经常在后台收到一类问题前端页面写好了SpringBoot后端也启动了但浏览器里就是拿不到数据接口一调就报错。问的人往往把“前后端连接”想得很神秘以为要装什么插件、配什么协议或者用了什么高深的技术。先泼一盆冷水前后端连接本质上就是一次普通的HTTP请求与响应。前端不管是Vue、React还是原生HTML页面向SpringBoot启动的Tomcat服务器发一个请求SpringBoot收到后处理业务逻辑、查数据库、把结果包装成JSON返回给前端。整个链条就这么长没有玄学。但能把这条链路真正走通里面确实有一些约定俗成的套路和不少暗坑。这篇文章我就围绕着“SpringBoot实现前后端连接”这条主线把接口设计、跨域处理、前端静态资源整合、MyBatis数据打通、项目结构、启动配置和常见故障全串一遍。素材来自我这几年实际带项目、做毕设辅导、帮人调代码的实践积累里面每一段基本都是真实踩过或看到过的场景。我先给这批问题分个类你对照着自己处在哪个阶段刚写完后端接口用Postman测没问题但前端页面一调就跨域报错Vue项目npm run build生成了dist目录不知道怎么塞进SpringBoot一起发布SpringBootMyBatis整合后Mapper扫不到、SQL报错、数据查不出来端口被占用、启动报错、版本不兼容、404/405/500傻傻分不清楚打算做毕设或项目答辩需要能讲清楚SpringBoot自动装配的原理文末我会把常见故障整理成一个快速排查表方便你以后直接翻。2. 后端接口设计让前端请求“接得住”的完整套路2.1 Controller层就是前后端之间的“接待处”在SpringBoot里前端的请求最先到达的地方是Controller。你可以把它理解成前台接待处——请求来了先登记映射URL再转给后边的工作人员Service层处理最后把结果交给来访者前端。一个最基础的接口长这样RestController RequestMapping(/api/user) public class UserController { GetMapping(/info) public ResultUserInfo getUserInfo(RequestParam(userId) Long userId) { UserInfo userInfo userService.getById(userId); return Result.success(userInfo); } }这里有几个关键注解新手最容易混淆RestController表示这个类里的每个方法都直接返回数据JSON而不是跳转页面。以前SSH时代用Controller还要配合ResponseBodySpringBoot里直接用RestController一步到位。RequestMapping(/api/user)定义类级别的URL前缀相当于这个接待处分管的所有业务都挂在/api/user下面。GetMapping/PostMapping/PutMapping/DeleteMapping对应HTTP的GET、POST、PUT、DELETE方法。这个设计对应的是RESTful风格——同样的URL通过不同的请求方式表达不同的语义。2.2 前后端传参的三种姿势必须闭着眼都能写前端传参给后端最常见的就三种方式我直接列出来方式注解适用场景前端怎么调URL路径参数PathVariable查询单个资源如用户ID/api/user/1001Query参数RequestParam条件查询、翻页参数/api/user/list?page1size10JSON请求体RequestBody新增、修改传复杂对象POST一个JSON字符串对应后端写法// 路径参数GET /api/user/1001 GetMapping(/{id}) public ResultUserInfo getUser(PathVariable Long id) { ... } // Query参数GET /api/user/list?page1size10 GetMapping(/list) public ResultPageResultUserInfo list(RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer size) { ... } // JSON请求体POST /api/user/savebody 为 {name:张三,age:18} PostMapping(/save) public ResultVoid save(RequestBody UserInfo userInfo) { ... }注意三个坑RequestParam如果不传required false或defaultValue前端少传一个参数就会直接400报错而且SpringBoot默认返回的错误信息很简陋前端根本不知道缺了什么。RequestBody只能接收合法的JSON字符串如果前端传了表单格式application/x-www-form-urlencoded后端用RequestBody接收会直接抛异常。接收前端传来的时间字符串比如2024-05-01强烈建议在DTO里用JsonFormat(pattern yyyy-MM-dd HH:mm:ss)明确指定格式不然会踩时区和格式解析的坑。2.3 统一返回结构前后端协作的“标准协议”我见过很多项目接口有的是直接返回对象有的是返回Map有的是返回true/false乱七八糟。前端每调一个接口就要看一遍后端代码才能知道返回结构这绝对是协作效率的头号杀手。我的建议是项目中强制使用统一返回结构public class ResultT { private Integer code; // 状态码200成功500失败 private String message; // 提示信息 private T data; // 实际数据 public static T ResultT success(T data) { ResultT r new Result(); r.code 200; r.message success; r.data data; return r; } public static T ResultT error(String message) { ResultT r new Result(); r.code 500; r.message message; return r; } }这样前端就能在axios拦截器里统一处理// axios 响应拦截器 axios.interceptors.response.use( response { const res response.data; if (res.code ! 200) { Message.error(res.message); return Promise.reject(new Error(res.message)); } return res.data; // 直接拿到业务数据 }, error { Message.error(网络异常或服务器错误); return Promise.reject(error); } );这个小改动能为前后端联调省掉大量扯皮时间。关于统一返回结构、全局异常处理这些都是SpringBoot项目里非常值得沉淀的基础设施尤其是做毕设或团队项目这部分做扎实了后面会很省心。3. 跨域问题前后端联调里十个有九个先栽在这里3.1 为什么前端调不通、Postman却能通这是我在答疑时被问得最多的一句话。Postman这类工具发请求时没有“浏览器同源策略”的限制所以接口明明通着Postman一测就OK。但浏览器里的Vue页面跑在localhost:5173去访问后端接口localhost:8080端口不同浏览器判定为跨域于是先把请求拦下来控制台报错Access to XMLHttpRequest at http://localhost:8080/api/user/list from origin http://localhost:5173 has been blocked by CORS policy“同源”的定义很简单协议http/https、域名localhost/127.0.0.1/xxx.com、端口5173/8080三者完全一致才叫同源。开发模式下前端和后端端口必然不一样所以跨域是必定要解决的问题。3.2 三种跨域解决方案的取舍方案一在接口上直接加注解CrossOrigin(origins http://localhost:5173) GetMapping(/list) public ResultPageResultUserInfo list(...) { ... }优点是简单粗暴缺点是每个接口都要加很啰嗦。适合只有一两个接口的demo级项目。方案二全局配置类最推荐Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }这段配置的作用是允许所有路径/**的跨域请求允许所有来源、所有常用请求方法。maxAge(3600)表示预检请求的结果在3600秒内不用重新发送降低请求次数。在实现方案二时有个细节要注意如果后续做登录鉴权需要携带CookieallowedOriginPatterns不能用*allowCredentials(true)也要慎用这两者组合在某些浏览器版本下会直接报错。我当时在项目里就被这个卡了一晚上。方案三通过网关统一处理如果用了SpringCloud Gateway或者Nginx可以在网关层统一配置跨域。比如Nginx配置里加add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods GET, POST, PUT, DELETE, OPTIONS;这种方案适合微服务架构在单体SpringBoot项目里用全局配置类就够了。3.3 预检请求OPTIONS到底在干什么跨域POST请求浏览器会先发一个请求方式是OPTIONS的预检请求探测后端允不允许它跨域。预检请求不是业务请求后端不需要真正去处理它。但如果你用的权限拦截器拦截了所有请求把OPTIONS请求拦下来不放行前端就会看到跨域报错。解决方法是排查拦截器配置对OPTIONS请求直接放行不经过鉴权逻辑。我用框架的时候都是让OPTIONS请求在HandlerInterceptor.preHandle里直接返回true。Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; // 预检请求直接放行不进入鉴权 } // 其他请求走正常鉴权逻辑 }这一段是真踩出来的经验。很多项目跨域配置看着没错但请求就是过不去最后发现是拦截器把OPTIONS请求拦了。排查跨域问题时建议你先在浏览器F12里看清请求方式到底是不是OPTIONS。4. 前端打包进SpringBootVue项目如何与后端“合体”4.1 开发模式与生产模式的本质区别开发模式下Vue项目通过Vite/Webpack启动一个开发服务器比如localhost:5173它有自己的热更新能力后端接口通过跨域访问。但生产环境不可能让用户去访问两个服务所以常规做法是前端构建成静态资源文件直接丢进SpringBoot的静态资源目录最终只需要启动一个Java进程前端页面和后端接口共用同一个端口。4.2 具体操作步骤第一步在Vue项目根目录执行构建命令npm run build构建完成后生成dist目录里面通常包含index.html、static子目录JS和CSS文件。第二步把dist里的所有内容复制到SpringBoot项目的src/main/resources/static目录下。SpringBoot启动后会自动把这个目录作为静态资源根目录。访问http://localhost:8080/时默认找index.html页面就加载出来了。第三步打包部署。在SpringBoot项目根目录执行mvn clean package -DskipTests生成target/*.jar直接运行java -jar target/xxx.jar访问http://localhost:8080前端页面和后端接口就在同一个服务上了。我把这个操作后的资源对应关系用一张表列出来方便你理解前端文件SpringBoot位置访问URLdist/index.htmlsrc/main/resources/static/index.htmlhttp://localhost:8080/dist/static/js/app.jssrc/main/resources/static/static/js/app.jshttp://localhost:8080/static/js/app.jsdist/static/css/app.csssrc/main/resources/static/static/css/app.csshttp://localhost:8080/static/css/app.css4.3 一个大坑vue-router history模式刷新404Vue项目如果用了路由的history模式即URL里没有#号直接打包部署到SpringBoot后会出一个经典问题在首页点跳转没问题但一旦刷新或者直接在浏览器输入http://localhost:8080/user/123这个地址SpringBoot找不到对应的Controller或静态资源返回404。原因很好理解这个请求绕过了静态资源映射SpringBoot不知道去哪里找/user/123这个页面就返回了默认的Whitelabel Error Page404白页。解决方案是在SpringBoot里加一个兜底转发规则所有非接口请求都转发到index.html由前端路由接管。写一个Controller或者配置类Controller public class IndexController { RequestMapping(value /{path:[^\\.]*}) public String redirectToIndex() { return forward:/index.html; } }这里用正则的写法是为了只拦截不带点的路径避免影响到js/css这些带后缀的静态资源。简单说就是带点的路径如/static/js/app.js正常返回静态资源不带点的路径如/user/123就转发去index.html交给前端路由。如果是SpringBoot 2.6及以上版本你还可以考虑用如下方式配置视图控制器Configuration public class WebConfig implements WebMvcConfigurer { Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController(/{spring:[^\\.]*}) .setViewName(forward:/index.html); } }两种方式选一种就行。这个兜底转发是我文中反复强调的一个点——前端路由刷新404基本是前后端合并部署后最高频的线上故障。5. 数据底层打通SpringBoot MyBatis 从配置到Mapper5.1 一个最简单的注册功能串联整条数据链路前面讲了接口怎么接请求、前端怎么打包但一个真正能跑起来的项目绕不开数据持久化。很多新手把SpringBootMyBatis的整合视为畏途其实梳理清楚了就是三步引入依赖、配数据源、写Mapper。我以“实现一个最简单的注册功能”为例走一遍完整流程。这是很多人毕设里都绕不开的功能麻雀虽小五脏俱全。第一步在pom.xml里引入依赖dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency这里有一个非常容易踩的坑SpringBoot 3.x系列要用mybatis-spring-boot-starter的3.x版本2.x版本的依赖它根本加载不进去因为底层包名从javax迁移到了jakarta。如果你用的SpringBoot版本太高旧教程里的写法大概率会报错。这个是当前SpringBoot相关版本兼容性里最典型的例子之一。第二步配置application.yml中的数据源spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/test_db?serverTimezoneAsia/ShanghaicharacterEncodingutf8useSSLfalse username: root password: 123456 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.demo.entity configuration: map-underscore-to-camel-case: true几个关键配置解释一下serverTimezoneAsia/Shanghai不配的话连接MySQL 8.x很可能会报时区错误因为MySQL驱动默认拿的是服务器时区跟本机对不上。map-underscore-to-camel-case: true数据库字段user_name自动映射到实体类属性userName省去大量resultMap手写映射这个配置非常救命。mapper-locations告诉MyBatis去哪里找XML文件如果路径不对启动时直接报Invalid bound statement。第三步写Mapper接口和XML文件。Mapper接口Mapper public interface UserMapper { int insert(User user); }XML文件放在resources/mapper/UserMapper.xml?xml version1.0 encodingUTF-8 ? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecom.example.demo.mapper.UserMapper insert idinsert parameterTypecom.example.demo.entity.User useGeneratedKeystrue keyPropertyid insert into t_user(name, age, email) values(#{name}, #{age}, #{email}) /insert /mapper做完这三步Controller通过Service调Mapper接口数据就能写进数据库了。5.2 注解SQL vs XML怎么选MyBatis支持两种写SQL的方式注解直接写在Mapper接口方法上或者写在XML里映射。// 注解方式 Insert(insert into t_user(name, age, email) values(#{name}, #{age}, #{email})) int insert(User user);我的建议是SQL简单用注解复杂用XML。动态SQLif、foreach、choose用注解写会很难受字符串拼来拼去可读性极差。XML方式虽然文件多但胜在SQL和Java代码分离后期改SQL不用动Java代码。团队项目里统一用XML会好维护很多。5.3 经常有人问的“Service层为什么存在”在SpringBootMyBatis的架构里Controller直接调Mapper接口也是能跑通的但正规项目一定会加一个Service层。Service层存在的意义在于承载业务逻辑。我举一个实际例子注册功能就不是单纯往数据库插一条记录那么简单——你得先检查用户名有没有被注册过没有才插入插入成功后可能还要发一封欢迎邮件。检查、插入、通知这几个动作拼在一起就是一条完整业务这些逻辑放Controller里会很臃肿放Mapper里又不合适。Service层就是专为这种“多步骤业务”设计的容器。说到这我顺便把那三层的调用关系用一个最精简的链路说明一下前端请求 - Controller参数接收与校验 - Service业务逻辑编排 - Mapper单表数据读写 - 数据库。这个链路是SpringBootMyBatis项目的基准思路也是面试和毕业答辩时最常被追问的框架结构问题。6. 项目结构与启动配置Maven构建、端口修改和一些碎碎念6.1 标准项目结构长什么样很多人拿到一个SpringBoot项目不知道文件该往哪里放。我按最常见的结构列一下src/main/java/com/example/demo ├── DemoApplication.java # 启动类 ├── controller/ # 控制层接收请求 ├── service/ # 业务层接口 │ └── impl/ # 业务层实现 ├── mapper/ # MyBatis Mapper接口 ├── entity/ # 数据库实体类 ├── dto/ # 接收前端参数的类 └── config/ # 配置类跨域、拦截器、异常处理等启动类DemoApplication.java放在最外层包这样做是为了让SpringBootApplication默认包扫描能覆盖到所有子包。很多新手把启动类放在com.example.controller或某个子包下结果启动时Spring容器扫不到其他包里的Bean运行期各种空指针。这个结构问题虽然听起来很基础但实际项目中经常有人因此浪费一整天。6.2 修改启动端口不止改一个数字那么简单修改端口最直接的方式是改application.ymlserver: port: 9090但如果你是打包后部署不想改配置文件也可以用启动参数指定java -jar app.jar --server.port9090在IDEA里调试时可以在Spring Boot的启动配置Run/Debug Configurations中编辑VM选项或者Program arguments实现同样的效果。我记得有人问过“IDEA工具怎么配置SpringBoot服务的启动端口”这个问题操作路径就是打开启动配置面板在Program arguments里填--server.port9090点击“Run”之后控制台就能看到Tomcat启动在9090端口。这个方式的好处是项目里的配置文件可以保持默认每个人的本地环境各自覆盖。注意server.address不要轻易改默认绑定所有网卡就够了。端口冲突时常见的报错是Port 8080 was already in useWindows下用下面命令找占用进程netstat -ano | findstr 8080 taskkill /PID 进程号 -F6.3 Maven构建的“冷知识”本地仓库替换上瘾前先看看版本号Maven构建项目执行的命令通常是mvn clean package -DskipTests初次构建时Maven要从中央仓库下载大量依赖如果网络不太好很容易卡住。实测下来用阿里云公共仓库的镜像地址替代默认中央仓库地址下载速度会提升一大截。mirror idaliyunmaven/id mirrorOfcentral/mirrorOf name阿里云公共仓库/name urlhttps://maven.aliyun.com/repository/public/url /mirror但如果说更重要的提醒那还是版本选择。我经手的项目里有相当一部分报错最终都能追溯到SpringBoot版本偏高、依赖版本没对齐这个问题上。SpringBoot 3.x要求JDK 17起步Jakarta命名空间替代了原本的javax这个迁移影响的范围非常大。如果你还在用JDK 8且暂时不想升级那就老老实实选SpringBoot 2.7.x。技术选型不是越新越好稳定社区案例多的版本反而最省心。我在给答疑时反复讲与其纠结新特性不如先把手头项目稳定跑起来。SpringBoot版本不是越高越好先看一眼自己的JDK版本再动手。6.4 自动装配原理面试和答辩躲不开的一个话题SpringBoot和传统Spring MVC最大的区别用一个词概括就是“自动装配”Auto Configuration。SpringBoot的启动类上那个SpringBootApplication是一个组合注解实际由三个核心注解构成SpringBootConfiguration本质上是个Configuration标明这是一个配置类ComponentScan自动扫描当前包及其子包下所有标注了ComponentServiceControllerRepository的类把它们注册为Spring容器管理的BeanEnableAutoConfigurationSpringBoot自动装配的核心开关EnableAutoConfiguration的实现逻辑是SpringBoot从META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports这个文件中加载所有自动配置类的名字然后根据条件判断哪些生效。条件判断是一系列ConditionalOnClass、ConditionalOnMissingBean之类的注解——你pom.xml里引入了某个依赖启动时对应类的字节码存在于classpath中相关自动配置就激活。拿MyBatis举例引入依赖后SpringBoot自动帮我们创建SqlSessionFactory、数据源等Bean。实际面试和答辩里把这个链条讲清楚就够了引入依赖 - 启动类组合注解触发自动配置 - 条件注解判断生效 - 读取自动配置类 - 构建缺省Bean。不用把源码背下来但这条链路每个环节都能讲出名堂比单纯背概念强得多。7. 常见故障排查清单与踩坑记录7.1 一张表收藏到浏览器书签我把日常答疑中最常见的故障汇总成一张表你碰到问题可以先来这里对号入座现象可能原因处理办法启动报Port 8080 was already in use端口被其他进程占用找到占用进程终止它或改server.port前端访问接口报跨域错后端没配置CORS加全局跨域配置类或确认拦截器没有拦截OPTIONS预检请求访问页面出现Whitelabel Error Page没找到对应路由或静态资源检查URL路径若用了history路由加index.html兜底转发启动报Invalid bound statementMapper XML映射找不到检查mapper-locations路径、namespace和Mapper接口名是否一致MyBatis插入数据时提示某个字段没有setter实体类属性名与数据库字段映射失败开启map-underscore-to-camel-case或检查字段命名中文乱码编码不一致URL连接串加characterEncodingutf8前端页面统一UTF-8返回JSON日期是时间戳类型Jackson日期序列化问题在配置中添加全局日期格式化或在字段上加JsonFormat请求返回405请求方式不匹配比如POST接口用GET调检查前端请求方法和后端GetMapping/PostMapping是否一致7.2 一个印象很深的完整排查过程有一次帮朋友调一个“刷新页面404”的问题整整折腾了两小时。项目结构是Vue3SpringBoot打包成单Jar部署本地跑得好好的上传到服务器后一刷新子页面就白屏。一开始我怀疑是Nginx配置问题因为服务器上用了Nginx做反向代理。但看了配置也没发现明显错误。后来我在服务器上直接用curl测试curl -I http://localhost:8080/user/123返回404。再试根路径curl -I http://localhost:8080/返回200。这就确认是SpringBoot内部路由的问题跟Nginx无关。回到代码里排查发现我用的兜底转发Controller只处理了一级路径/{path:[^\\.]*}对于/user/123这种二级路径在SpringBoot 5.3之后的新版本中并不会匹配上所以直接走了404。修改成正则匹配多层路径之后问题解决RequestMapping(value /{path:^(?!static$).*}/**) public String redirect() { return forward:/index.html; }这个案例值得写在这是想提醒你排查问题一定要从现象出发一步步缩小范围。先确认是前端路径不对、后端接口问题还是部署环境问题再对症下药。这个思路比背一百个报错解决方案都重要。7.3 日志和控制器打印是排查故障的两条腿很多新手出问题了不会排查就是干瞪眼或者把整段报错截图发出来。我建议你第一件事是看日志尤其是SpringBoot启动时控制台打印的启动日志里面包含了端口占用、Bean创建失败、配置读取异常等关键信息。如果业务逻辑有问题就在Controller的入口和Service的关键步骤打日志PostMapping(/login) public ResultString login(RequestBody LoginRequest request) { log.info(收到登录请求用户名{}, request.getUsername()); ... }log不是Println而是Slf4j的Logger对象。推荐在类上直接用Lombok的Slf4j注解代码里就能直接使用log。这个习惯从第一天开始养成后面调试效率会高很多。7.4 定时任务和其他常见扩展点热搜词里也出现了一些高频扩展话题比如SpringBoot定时任务配合Scheduled注解用、SpringBoot整合ActiveMQ、Flink等这些都属于在“前后端连接”这条主线上衍生出来的后端能力。如果做毕设或面试准备定一个节奏前端连着后端后端连着数据库和中间件一点点把链路拉长项目深度自然就能体现出来了。8. 建立一套属于自己的SpringBoot联调流程最后分享一点我个人的习惯。经过前后端这么多项目的联调我发现一个能显著减少无用功的做法不要让前端等到后端全部写完再开始联调。正确节奏是后端先定义好接口文档哪怕只是一个简单的URL和JSON返回结构约定前端按这个约定并行开发后端按这个约定实现。联调阶段就只是验证约定是否一致而不会出现“我做完了才发现返回字段对不上”这种情况。要是接口约定用Swagger后端启动后在地址栏访问http://localhost:8080/swagger-ui/index.html就能看到在线接口文档前端照着文档调就行。不过接口文档是锦上添花真正重要的还是前后端各做各的时统一返回结构、统一错误码、统一字段命名要提前谈好。另外如果你是从零开始搭项目建议第一件事就是跑通一个最简单的“前端页面 - 后端接口 - 数据库”的完整请求。这个最小闭环打通了后面再填业务逻辑就不慌了。我在答疑时一直强调前后端连接其实没有多么宏大的工程它就是一条又一条请求链路的组合。把最基础的那条链路踩踏实了SpringBoot对你来说就不再是一个会报错的黑盒子。一点实际经验在项目里我会把前端打包产物放到一个专用目录比如webapp/目录配置WebMvcConfigurer将静态资源映射指向这个目录而不是直接覆盖src/main/resources/static。这样做的好处是不会污染后端源码目录每次重新打包前端时只需清理这一个目录即可。Configuration public class StaticResourceConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/**) .addResourceLocations(file:webapp/); } }这个做法的额外好处是部署时前端代码和后端代码物理分离想单独更新前端页面不需要重新打包整个Jar文件。缺点是配置略多适合项目规模稍大时用。是个纯粹基于个人习惯的补充方案新手做完第一版之后可以试试这种分离方案会顺手很多。