聊到 Spring Boot 做前后端连接,很多人第一个想到的就是:后端写个接口返回 JSON,前端拿 Axios 请求一下,数据出来了,就算连接上了。这话没毛病,但真到项目里,你会遇到一堆“文档里不会写”的破事:跨域报错、日期格式对不上、接口路径总变、前后端各改各的没人理接口规范……这篇文章就把“前后端连接”这件事从头到尾拆开,讲清楚 Spring Boot 做后端时,前后端到底是怎么连起来的,每一环的底层逻辑是什么,以及我实际项目里踩过的坑。适合刚接触 Spring Boot 的实习生、准备做前后端分离毕设的学生,以及被前后端联调折磨过但没系统梳理过的同学。
1. 这个项目到底在做什么:Spring Boot 连接前后端的完整思路
1.1 前后端分离是怎么一步步变成主流方案的
先说一个很多人没意识到的问题:你现在觉得“前后端连接”是理所当然的事,但放在十年前,这个词根本不存在。那时候的主流做法是服务端渲染,Java Web 项目用 JSP 或者 Thymeleaf 模板引擎,前端页面嵌在后端工程里,浏览器请求进来,后端把 HTML 拼好再返回给浏览器。这种模式下,前后端根本不需要“连接”,因为大家住在同一个工程里,任何一方改动都会直接影响另一方。
但是随着移动端兴起、前端工程化成熟,大家发现这种耦合根本扛不住需求变化。手机 App 需要接口、网页需要接口、小程序还需要接口,同一个后端逻辑要服务多个客户端,总不能给每个端都写一套 JSP 页面吧?于是前后端分离慢慢成了主流:前端是一个独立工程(Vue、React、或者最简单的 HTML 页面),后端是一个独立工程(Spring Boot),两者之间只通过 HTTP 接口通信,数据格式统一用 JSON。这就是你现在看到的所有“Spring Boot + Vue 前后端分离”项目的底层逻辑。
Spring Boot 在这套架构里扮演的角色非常纯粹,后端服务提供方。它不关心你的前端长什么样、是 Vue 还是 React 还是原生 HTML,它只负责接收 HTTP 请求、处理业务逻辑、返回标准格式的数据。这种职责单一的设计让前后端团队可以完全并行开发,只要事先把接口约定好,后端写好接口丢给前端,前端拿到接口地址就能干活,两边互不阻塞。
1.2 技术选型背后:为什么大家都选 Spring Boot
Spring Boot 能从 Java 后端生态里杀出来,成为前后端分离项目的默认选项,我理解有几个核心原因。
第一个是它大幅降低了 Spring 的使用门槛。早期用 Spring MVC 写一个 Web 接口,你要配置 web.xml、要配 Spring 容器、要配数据源、要配视图解析器,光是把项目跑起来就得折腾半天。Spring Boot 直接用“约定大于配置”的思路干掉了一堆 XML 配置,依赖写在 pom.xml 里,启动类一跑,服务就起来了。我见过接触 Spring Boot 不到一周的新人就能写接口调通,换成传统 Spring 项目,没一个月他连配置文件都理不清。
第二个是它的生态集成能力极强。前后端连接不光是一个接口的事,中间还涉及参数校验、权限认证、日志记录、文件上传、消息队列、分布式锁等等。Spring Boot 的 Starter 机制把这些东西全部封装成“依赖 + 自动配置”的模式,你往 pom 里加一个依赖,对应的能力就自动装配好了,不需要像以前那样手动写一大堆配置类。比如后面要说的跨域问题,Spring Boot 里加一个配置类就搞定,传统 Spring 项目你还得折腾拦截器。
第三个是它的工程规范非常标准化。Spring Boot 项目天然按 controller / service / mapper(dao)分层,这个结构在国内 Java 项目里几乎成了行业标准。新人进来看到这样的项目结构,很快就知道哪个 class 是干什么的。对前后端连接来说,Controller 层是唯一跟前端直接打交道的层,接口入口清晰,文件位置固定,排查问题的时候非常舒服。
1.3 前后端连接的完整链路:一次请求到底经历了什么
我建议每个做前后端联调的人都先在脑子里建立起这条链路,后面所有的问题排查都靠它。一个完整的前后端交互过程是这样的:前端页面发起一个 HTTP 请求(比如用户点击登录按钮,Axios 向/api/login发 POST 请求)→ 请求通过网络到达后端服务器 → Spring Boot 的 DispatcherServlet 根据 URL 找到对应的 Controller 方法 → Controller 调用 Service 层处理业务逻辑 → 返回一个对象 → Spring Boot 利用 Jackson 把对象序列化成 JSON 字符串 → 作为 HTTP Response 返回给前端 → 前端拿到响应数据,渲染页面或触发下一步操作。
这条链路里,每一步出问题都会导致“连不上”的现象。前端报 404 往往是 URL 路径有问题,报 405 是请求方式不对(前端用 GET,后端只写了 POST),报 403 是权限或跨域问题,报 500 是后端代码执行出错。我不知道你看过多少“我的前后端连不上”的求助帖,十有八九都是这条链路中某一环配置错了。
2. 后端接口设计:保证前端好用的底层约定
2.1 RESTful 接口规范与 URL 命名策略
前后端连接不是后端写几个接口就能完事的,连接的前提是“约定”,约定的核心就是接口规范。现在国内 Java 项目接口风格基本是 RESTful 范式,它的核心思想是:用 HTTP 方法表示操作类型,用 URL 表示资源,用状态码表示结果。
举个例子,同样是管理用户数据的接口,RESTful 风格一般这么设计:GET/api/users表示获取用户列表,GET/api/users/1表示获取 ID 为 1 的用户详情,POST/api/users表示新建用户,PUT/api/users/1表示更新 ID 为 1 的用户,DELETE/api/users/1表示删除该用户。URL 里全是名词,操作语义全集中在 HTTP 方法上。这样做的好处是接口一眼就能看出含义,前后端对接口的理解完全一致,不容易产生歧义。
但是我在真实项目里发现,很多团队并没有严格执行 RESTful,而是用“动作式 URL”,也就是/api/getUserInfo、/api/deleteUserById这种,把操作直接写在 URL 里。说实话,这不算大问题,团队内部约定一致就能跑通。但如果你做的是标准化程度要求高的项目,或者有面试官问你“你们接口怎么设计的”,RESTful 是加分项。我自己比较推荐的做法是:主干资源用 RESTful,涉及复杂业务动作(比如发布文章、审核通过、批量导入)就用 POST 加动作式 URL,比如POST /api/articles/publish。纯 RESTful 在某些业务场景下反而是死板,务实点更重要。
URL 命名还有几个细节值得注意:一是统一加/api前缀,这样前端代理、后端鉴权过滤都有清晰的边界;二是用复数名词表示资源集合;三是路径参数用{id}占位而不是拼在 query string 里(除非是筛选条件)。这些约定一旦在项目早期定下来,后面的联调效率能提升一大截。
2.2 统一返回结构:让前端拿到的数据永远都是同一种形状
前后端连接中最让前端头疼的事情之一,就是后端接口返回的数据结构不统一。有的返回{code: 200, data: [...]},有的直接返回一个数组,报错了返回一段纯文本,前端每接一个接口都要单独处理返回格式,写得想骂人。
解决这个问题的最优解就是设计一个统一的返回结果类。我在项目里一般这么定义:
@Data public class ResponseResult<T> { private Integer code; private String message; private T data; public static <T> ResponseResult<T> success(T data) { ResponseResult<T> result = new ResponseResult<>(); result.setCode(200); result.setMessage("操作成功"); result.setData(data); return result; } public static <T> ResponseResult<T> success(String message, T data) { ResponseResult<T> result = new ResponseResult<>(); result.setCode(200); result.setMessage(message); result.setData(data); return result; } public static <T> ResponseResult<T> error(String message) { ResponseResult<T> result = new ResponseResult<>(); result.setCode(500); result.setMessage(message); return result; } public static <T> ResponseResult<T> error(Integer code, String message) { ResponseResult<T> result = new ResponseResult<>(); result.setCode(code); result.setMessage(message); return result; } }然后 Controller 里所有的接口返回值统一写成ResponseResult<T>:
@RestController @RequestMapping("/api/users") public class UserController { @GetMapping public ResponseResult<List<User>> listUsers() { return ResponseResult.success(userService.list()); } }这样做的好处非常明显:不管接口正常还是异常,前端拿到的 JSON 结构永远是{code: xxx, message: xxx, data: xxx}三种字段。前端封装一个统一的请求函数,几十行代码就能处理所有接口的响应。前端的判断逻辑极其简单:code 是 200 就用 data,不是 200 就弹 message。我见过很多项目因为没做统一返回结构,前端工程师每天都在跟后端确认“这个接口失败返回是什么结构”,这种内耗毫无价值。
2.3 参数传递的四种形式:面试常问,联调常用
前后端连接中,参数传递是最日常、最容易出问题的地方,也是后端面试的高频题。Spring Boot Controller 方法接收参数的四种方式最好都搞清楚。
第一种是路径参数,用@PathVariable接收,对应 URL 里/api/users/1中的1。前端拼接 URL 时必须动态把值塞进去,比如 Axios 里写get(/api/users/${id}”)。我见过有前端把路径参数放在 query string 里传给后端,后端用@PathVariable` 接收结果自然是 404,因为路径对不上。
第二种是查询参数,用@RequestParam接收,对应 URL 里?page=1&size=10这种。关键是前端传参的 key 必须和后端参数名一致,否则就是 null。另外@RequestParam默认是必传的,前端漏传直接报 400,如果想要可传可不传的,要设置required = false或者给 defaultValue。
第三种是请求体参数,用@RequestBody接收,对应前端 POST 请求 body 里的 JSON。这是前后端分离项目里最常用的一种方式,前端传一个对象,后端用一个对应的实体类或 DTO 接收,Spring Boot 用 Jackson 自动把 JSON 反序列化成 Java 对象。这里最常见的坑是 JSON 字段名和 Java 属性名不一致,或者日期格式不对(比如前端传"2024-01-15 00:00:00",后端 LocalDateTime 解析失败报错)。
第四种是请求头参数,用@RequestHeader接收,常用于传递 token、客户端信息这些元数据。做登录认证的时候,前端把 token 放在 Header 的 Authorization 字段里,后端写一个拦截器统一读取校验,对接起来非常自然。
提示:我建议 Controller 方法的参数设计遵循“GET 优先用 @RequestParam,POST 批量数据优先用 @RequestBody”的原则。合理使用这四种传参方式,前后端对接的绝大部分参数问题都已经解决了一半。
3. 前后端连接最关键的环节:跨域处理与数据格式
3.1 为什么一前后端分离就碰上了跨域问题
前后端分离项目第一次联调时,十个团队有八个会遇到跨域报错,浏览器控制台一片红,前端急得直跺脚。这个问题的根源其实很简单:浏览器的同源策略。所谓同源,就是协议、域名、端口三者完全一致。前后端不分离的老项目,页面和接口都在同一个地址下,不存在跨域;前后端分离之后,前端开发服务器跑在http://localhost:8080,后端跑在http://localhost:9090,或者用 Nginx 部署时前端和后端分在不同域名(甚至同一个域名不同端口),协议域名端口有一处不一致,浏览器就会拦截跨域请求。
这里有个容易误解的点:HTTP 请求本身并没有被拦截,请求已经发到后端了,后端也正常处理并返回了,但浏览器发现响应头里没有允许跨域的标识,就强制把响应拦下来了,前端拿不到数据。所以你经常看到的现象是:后端日志里明明有请求记录,接口也执行成功了,但前端控制台依然报跨域错误。理解了这一点,排查跨域问题时思路会清晰很多:核心就是让后端响应头带上允许跨域的标记,或者让前端请求不发跨域请求(代理方案)。
3.2 三种解决跨域的方案:选型与配置实操
跨域解决方案里有三种是项目里真正用得上的:CORS 后端配置、前端开发代理、Nginx 反向代理。生产环境我推荐 Nginx 反向代理方案,后端开发调试期我推荐 CORS 或者前端代理,下面分别说用法。
第一种,后端直接开启 CORS。Spring Boot 里写一个配置类统一处理即可:
@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); } }这段配置的意思是:允许所有来源、常见 HTTP 方式、所有请求头访问后端接口,并且允许携带 Cookie。其中的 allowedOriginPatterns 是 Spring Boot 2.4+ 的写法,老版本用 allowedOrigins("*"),但需要注意allowCredentials(true)时allowedOrigins("*")会冲突,allowedOriginPatterns 则能解决这个问题。另外那个 OPTIONS 方法必须放行,因为浏览器在跨域 POST、PUT 请求前会先发一个 OPTIONS 预检请求,后端如果不处理这个预检,前端正式请求根本发不出去。
第二种,前端开发服务器代理。前后端分离项目中前端一般跑 Vue 或 React 的 dev server,Vue 的配置在vue.config.js里这样写:
module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:9090', changeOrigin: true } } } }原理是:前端请求的 URL 如果是/api开头,dev server 帮你转发到后端地址。因为代理转发是服务器到服务器的通信,不经过浏览器,所以不存在同源策略限制。这种方式的好处是前端代码里只需要写相对路径/api/xxx,不依赖后端具体 IP 和端口,换后端环境只需改代理配置。我个人在开发环境比较推荐这种方式,因为最贴近生产环境的部署方式(下面前端基本都是同源),还能顺手解决一部分跨域问题。
第三种,生产环境 Nginx 反向代理。打包后的前端静态文件放在 Nginx 里,再把/api请求反向代理到后端服务,这样浏览器访问的域名和路径全是同一个域名,根本不存在跨域:
server { listen 80; server_name example.com; location / { root /usr/share/nginx/html; index index.html; } location /api/ { proxy_pass http://backend-server:9090; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这段配置在 Docker 部署 Spring Boot 项目时配合得非常顺畅,也是生产环境里最常用、最稳妥的方案。关于 Docker 部署 Spring Boot,我放到第六节细讲。
3.3 数据格式约定:JSON 序列化、日期格式和 null 处理
接口打通了不代表连接就成功了,数据格式对不上照样白搭。前后端连接中数据格式的问题集中在两个地方:日期格式和 null 值处理。
日期问题几乎是每个前后端项目都会遇到的经典问题。数据库里的datetime类型,Java 实体里是LocalDateTime,Jackson 序列化后默认是类似2024-01-15T12:30:00的 ISO-8601 格式,带字母 T,前端直接展示很丑,解析还得专门处理。处理方案是在application.yml里统一配置:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8但这只能解决java.util.Date的格式,对LocalDateTime有时不生效。老手更推荐在实体类的日期字段上加注解,一劳永逸:
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime createTime;同时接收前端传来的日期字符串时,这个注解同样会让 Jackson 尝试按指定格式解析,避免“前端传2024-01-15 00:00:00后端直接报错”的尴尬情况。
null 值处理则更多是体验问题。字段值是 null 的时候,Jackson 默认序列化成"field": null,前端拿到之后如果没做判空直接obj.prop.name就会报错。方案有两个:一是后端在类上加@JsonInclude(JsonInclude.Include.NON_NULL),序列化时自动忽略 null 字段;二是在统一返回结构层面,data 为 null 也正常输出,交给自己定义。我自己习惯用NON_NULL策略,因为响应体里满屏的null不仅占用带宽,还影响前端排查问题。
4. 从零到一:完整的前后端连接 Demo 实操
4.1 创建 Spring Boot 项目:IDEA 里的标准操作
先说明一点,网上教程里创建 Spring Boot 项目的姿势五花八门,有人用网页版 Spring Initializr,有人用命令行,我下面说最常用的 IDEA 操作路径:File -> New -> Project -> Spring Initializr,然后填写项目信息(Group、Artifact),选择 Java 版本和 Spring Boot 版本,依赖这里选Spring Web就够跑通前后端连接了。如果后续要连数据库,再选MySQL Driver、MyBatis Framework或者Spring Data JPA这些。
这里我想多说一嘴版本选择的问题。有段时间 Spring Boot 3.x 刚出来的时候,网上好多人按教程创建 3.x 的项目,结果引入一些老依赖直接报错,因为 Spring Boot 3 要求 Java 17 起步,并且使用了 Jakarta 命名空间(javax全部变成jakarta),很多还没适配的老库根本跑不起来。这就是热搜词里“springboot版本太高”这个现象的来源。我的建议是:如果你是学习或者做常规毕业设计,不用非要追最新大版本,选当前稳定的小版本即可。比如 Spring Boot 2.7.x 和 3.2.x 都有大量资料,但 3.x 的坑你心里要有数。创建项目时 IDEA 可以选择版本,没必要收到“版本太高”这个问题的困扰。
4.2 后端代码实现:Controller + Service + 数据实体
为了演示前后端连接,我这边做一个最简单的用户管理系统,功能就三个:查询用户列表、新增用户、删除用户。数据不接数据库,先用内存 List 存着,聚焦在连接本身。
先把用户实体类写好:
@Data @NoArgsConstructor @AllArgsConstructor public class User { private Long id; private String username; private String email; }注意这里用了 Lombok 的注解,IDEA 里需要装 Lombok 插件,pom 里引入依赖:
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>然后是 Service 层,简单模拟数据操作:
@Service public class UserService { private final List<User> userList = new CopyOnWriteArrayList<>(); private final AtomicLong idGenerator = new AtomicLong(1); public UserService() { // 初始化两条示例数据 userList.add(new User(idGenerator.getAndIncrement(), "zhangsan", "zhangsan@example.com")); userList.add(new User(idGenerator.getAndIncrement(), "lisi", "lisi@example.com")); } public List<User> listUsers() { return userList; } public User addUser(String username, String email) { User user = new User(idGenerator.getAndIncrement(), username, email); userList.add(user); return user; } public boolean deleteUser(Long id) { return userList.removeIf(user -> user.getId().equals(id)); } }最后是重点,Controller 层,直接跟前端打交道:
@RestController @RequestMapping("/api/users") public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService = userService; } @GetMapping public ResponseResult<List<User>> listUsers() { return ResponseResult.success(userService.listUsers()); } @PostMapping public ResponseResult<User> addUser(@RequestParam String username, @RequestParam String email) { User user = userService.addUser(username, email); return ResponseResult.success("用户添加成功", user); } @DeleteMapping("/{id}") public ResponseResult<Void> deleteUser(@PathVariable Long id) { boolean deleted = userService.deleteUser(id); if (deleted) { return ResponseResult.success("用户删除成功", null); } return ResponseResult.error("用户不存在"); } }这段代码里涵盖了三种 URL 形式:路径参数{id}、查询参数username/email、纯路径/api/users。有眼尖的会发现我在 POST 接口里用的是@RequestParam而没写前端页面的 Ajax 内容,这是故意的,为了演示两种传参方式。前端如果用Content-Type: application/x-www-form-urlencoded提交表单,后端@RequestParam能接到;如果前端用 JSON 提交,后端应该改成@RequestBody。
4.3 前端代码实现:用纯 HTML + Axios 打通连接
为了让大家看全链路,我这里不用 Vue 工程(那是另外一个话题),直接用纯 HTML 页面加 CDN 引入 Axios 的方式演示,最小化前端环境依赖,你在本地双击 HTML 文件就能跑。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>Spring Boot 前后端连接 Demo</title> <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script> </head> <body style="font-family: sans-serif; margin: 40px;"> <h2>用户列表</h2> <ul id="userList"></ul> <h3>新增用户</h3> <form id="addForm"> <input type="text" id="username" placeholder="用户名" required> <input type="text" id="email" placeholder="邮箱" required> <button type="submit">添加</button> </form> <script> const API_BASE = 'http://localhost:9090/api/users'; async function loadUsers() { const response = await axios.get(API_BASE); const result = response.data; if (result.code === 200) { const ul = document.getElementById('userList'); ul.innerHTML = ''; result.data.forEach(user => { const li = document.createElement('li'); li.textContent = `${user.id} - ${user.username} (${user.email}) `; const delBtn = document.createElement('button'); delBtn.textContent = '删除'; delBtn.onclick = () => deleteUser(user.id); li.appendChild(delBtn); ul.appendChild(li); }); } else { alert(result.message); } } async function deleteUser(id) { await axios.delete(`${API_BASE}/${id}`); loadUsers(); } document.getElementById('addForm').addEventListener('submit', async (e) => { e.preventDefault(); const username = document.getElementById('username').value; const email = document.getElementById('email').value; // 使用 URLSearchParams 以表单形式提交,对应后端的 @RequestParam const formData = new URLSearchParams(); formData.append('username', username); formData.append('email', email); await axios.post(API_BASE, formData); loadUsers(); }); loadUsers(); </script> </body> </html>这里有个非常关键的点:这个 HTML 文件是通过file://协议直接打开的,页面地址是file:///.../index.html,而后端是http://localhost:9090,协议不同、域名(file vs localhost)也不同,必然触发跨域。所以如果你用第一节的 CorsConfig 配置打开后端,前端就能通;如果没配 CORS,这里就会报跨域错误。这就是我们刚才讲过的知识点的实战体现。
4.4 联调效果与抓包验证:眼见为实
启动 Spring Boot 项目,假设端口配的是 9090(在application.yml里写server.port: 9090),然后打开 HTML 页面,应该能正常看到两条示例数据。添加一个用户,列表刷新后多一条记录;点击删除,对应记录消失。
但我想提醒的是:业务正常跑通只是表面,你要学会用浏览器开发者工具(F12)里的 Network 面板观察真实的请求响应。点一下某个请求,可以看到 Request URL、Request Method、Status Code、Response Headers、Response Body。
比如正常响应的 Response 长这样:
{ "code": 200, "message": "操作成功", "data": [ { "id": 1, "username": "zhangsan", "email": "zhangsan@example.com" }, { "id": 2, "username": "lisi", "email": "lisi@example.com" } ] }如果跨域没配置好,Network 面板里这条请求大概率显示 CORS error 或者 Status 是 (failed),Response 里看不到数据。如果后端 9090 端口没启动,前端会报ERR_CONNECTION_REFUSED。这些现象对应的根因,你多抓几次包就心里有数了。
4.5 补充:前端用 JSON 提交时后端应该怎么收
上面 Demo 里前端用的是表单形式提交,Content-Type是application/x-www-form-urlencoded,后端对应@RequestParam。但真实项目里,大多数前端框架默认的 POST 提交格式是 JSON,也就是Content-Type: application/json,body 是标准 JSON 字符串。这种格式下后端要改成:
@PostMapping public ResponseResult<User> addUser(@RequestBody User user) { User savedUser = userService.addUser(user.getUsername(), user.getEmail()); return ResponseResult.success("用户添加成功", savedUser); }同时前端 Axios 写axios.post(API_BASE, { username, email })直接传对象就行。两种方案没有绝对的好坏,但我个人建议:接口比较简单的用@RequestParam更直观,接口字段多、有嵌套结构就用@RequestBody接 DTO。你只要牢记住“form 格式对应 @RequestParam,JSON 格式对应 @RequestBody”,前后端连接时的传参问题基本就捋顺了。
5. 常见问题与排查技巧实录
5.1 问题速查表:前后端连接典型症状及根治方法
这里我把做前后端连接时遇到频率最高的几个问题整理成表格,方便你直接对号入座,快速定位问题根因。每个问题都是我在项目里真实踩过的坑,值得收藏。
| 现象 | 可能原因 | 排查思路 | 解决办法 |
|---|---|---|---|
| 前端请求报 404 | URL 路径写错,或 Controller 未匹配 | 看 Network 面板确认实际请求 URL,对照 Controller 的 @RequestMapping | 修正路径,注意 @RequestMapping 放在类上时 /api/users 是完整前缀 |
| 报 405 Method Not Allowed | 请求方法不对,GET/POST 写错了 | 看请求方法,检查后端方法上的 @GetMapping/@PostMapping | 统一接口方法约定,或接受 OPTIONS 预检 |
| CORS 跨域报错 | 协议/域名/端口不一致,后端未开启 CORS | 看响应头是否包含 Access-Control-Allow-* | 配置 CorsConfig;开发期用前端代理 |
| 后端接口报 500 | 代码执行异常(NPE、数据格式错误、SQL 错误) | 看后端控制台堆栈日志 | 根据异常修代码;建议配全局异常处理 |
| 前端拿到的 data 为 null | 后端返回的字段名和前端取用不一致;或 JSON 序列化配置忽略字段 | 打开 Network 看 Response 实际 JSON | 统一字段命名(推荐驼峰),或前端按实际字段取值 |
| 日期格式对不上 | 前后端约定的日期格式不一致 | 看 JSON 里的日期格式和前端期望格式 | 用 @JsonFormat 统一yyyy-MM-dd HH:mm:ss |
| 请求跨域时被 OPTIONS 挡住 | CORS 配置没放行 OPTIONS | 浏览器 Network 里看有没有预检请求 | 在 allowedMethods 中加入 OPTIONS,或处理预检请求 |
| 后端能收到请求但响应被浏览器拦截 | CORS 配置缺失或 allowedOrigins 与 credentials 冲突 | 看浏览器 Console 具体错误信息 | allowedOriginPatterns + allowCredentials(true) |
这张表不是让你背的,重点是培养“根据现象反推链路中哪一环出问题”的思维习惯。所有连接问题都能归因到链路中的某一环:URL 匹配、方法匹配、参数解析、权限、跨域、序列化、网络可达。锁定阶段之后再去搜解决方案,效率高得多。
5.2 版本与依赖相关疑难杂症:动手前先看环境
还有一个容易踩雷的领域是版本问题。很多时候不是代码错了,是环境不对。这里把高频的几个总结一下。
Spring Boot 版本太高,导致老项目依赖不兼容。比如 Spring Boot 3.x 需要 Java 17 起步,但教室里装的是 JDK 8,IDEA 编译直接报错;或者你的项目里引进了springfox-swagger2(Swagger 的老牌库),在 Spring Boot 3 里没法用,因为它基于 javax。遇到这类问题我先看 Spring Boot 的版本和 JDK 版本,再决定是否降级到 2.7.x。记住一个原则:学习阶段跟着教程走,教程用什么版本你就用什么版本,没必要追最新。
IDEA 里创建 Spring Boot 项目报 “Cannot download” 或初始化失败,大概率是网络问题或者 Spring Initializr 地址被墙了。解决办法是在 IDEA 的Settings -> Plugins里换个镜像地址(国内有 Spring Initializr 镜像),或者直接用阿里云的脚手架地址创建项目。这类问题卡时间很多,不是技术难题,是环境信息差。
还有热搜词里出现的springboot banner 生成器,可能有些同学喜欢在启动时打一个 ASCII 艺术字 banner,这个不影响功能,纯属项目趣味,遇到问题把banner.txt删了就行;搜索词里的springdoc关闭问题也可以提一句,如果你用了 springdoc 但不想看接口文档,在 application.yml 里设springdoc.api-docs.enabled=false和springdoc.swagger-ui.enabled=false即可。这些看着八竿子打不着的热搜词,本质上反映的都是中国开发者用 Spring Boot 时的真实痛点。
5.3 排查方法论:前后端联调不传谣不甩锅
最后分享一套我实际用的排查流程,我自己叫“三层定位法”。
第一层:先在浏览器 Network 面板里看请求有没有发出去、状态码是什么。这能确认问题出在请求链路还是响应链路。
第二层:再看 Response Body。如果是后端返回的,能直接看到统一返回结构里的 code 和 message,一半的问题光靠 message 就能解决。如果 Response 是空或红色报错,问题可能出在跨域或网络。
第三层:最后再看后端控制台日志。我不止一次遇到过前端甩锅说“后端接口没通”,结果后端控制台清清楚楚打印着 NullPointerException 的堆栈。所以在团队里做联调,第一件事是统一日志输出格式,第二件事是任何一方改完接口先自测再交付,第三件事是排查问题时不猜,用数据说话。
注意:我见过最浪费时间的前后端联调方式,就是前端说“报错了”然后把截图丢过来,后端说“我这边是好的啊”。两边各看各的,扯皮半小时。正确的姿势是:前端把 Network 面板里这条请求的 Request、Response、Console 报错一次性截全,后端把控制台日志一次性贴出来,两个人对一遍就定位了。
6. 上线之前还要做的事:从 Demo 到可用项目
6.1 配置分离:不同环境不同配置
这个 Demo 能跑通,但你不可能一直把它跑在本地上。真正要上线的时候,第一个要改掉的就是“所有配置写死在 application.yml”的做法。开发环境的数据库地址、日志级别、端口可能和生产环境完全不一样,最简单的方案是在application.yml里写公共配置,然后在application-dev.yml、application-prod.yml里写环境差异配置,启动时通过--spring.profiles.active=prod指定当前环境。
对于前后端连接来说,最重要的配置项无非是server.port(后端端口),以及如果接数据库时的连接地址。把这些东西按环境拆分,配合 CI/CD 流水线,上线时只改一个环境参数,不会因为误改配置把生产环境搞坏。这一步看起来不起眼,但能避免非常多上线事故。
6.2 Docker 部署 Spring Boot 项目:一键起服务
前后端分离项目最主流的部署方式就是 Docker 了。Spring Boot 项目常规部署有几种方式:直接java -jar启动、用 systemd 托管、放进 Docker 容器。我个人强烈推荐 Docker,因为环境一致性、启动速度、回滚协议都是最优的。
部署过程分三步:先用 Maven 打包,mvn clean package,在target目录下得到可执行的 jar 包;然后在项目根目录写一个 Dockerfile:
FROM openjdk:17-jdk-alpine VOLUME /tmp COPY target/user-demo-0.0.1-SNAPSHOT.jar app.jar ENTRYPOINT ["java","-jar","/app.jar"]构建镜像命令:
docker build -t user-demo:latest .后台启动:
docker run -d -p 9090:9090 --name user-demo user-demo:latest这里-p 9090:9090表示把宿主机的 9090 端口映射到容器的 9090 端口,这样前端不管是开发环境还是 Nginx 部署,都能通过宿主机 IP 加 9090 访问后端接口。如果生产环境按前面说的用 Nginx 做同源代理,Nginx 和后端容器在同一个 Docker 内网里,直接用容器名互相访问,前端根本不需要知道后端端口,从源头上又少了一层跨域问题。
6.3 从 Demo 到真实项目的差距:中间件、鉴权、错误码
Demo 跑通只是起点,一个真实可用的前后端连接项目,还需要补充几块内容。一是持久化,把内存 List 换成 MySQL + MyBatis 或 JPA;二是统一异常处理,用@RestControllerAdvice加@ExceptionHandler把异常转换成统一返回结构;三是登录鉴权,用 JWT 或者 Spring Security 把接口保护起来,前端请求带 token,后端拦截器校验;四是接口文档,引入 springdoc 或 knife4j,让后端写的接口能自动生成文档,省去口头传话。
这些内容每一个单拎出来都能写几千字,但核心思路不会变:前后端连接仅仅是整个系统的“通信层”,连接之外的业务逻辑、数据管理、安全性才是把项目做扎实的重点。很多毕设项目给我的感觉是接口能通、页面能显示,但一追问“你们鉴权怎么做、异常怎么处理、上线怎么搞”就答不上来了。做技术,先跑通再深入,这个节奏是对的,但不能只停在跑通。
个人体会是,前后端连接的问题从来不复杂,绝大多数坑都来自约定不清和环境差异。把接口规范定好、返回结构统一、跨域配置提前放好,这个项目在连接层面的麻烦就能消掉七八成。你要是现在正在被联调折磨,建议别急着改代码,先把协议、接口文档、返回结构拉通,再一头扎进去调,会轻松很多。这套方法论我用了好几年,真觉得比任何一劳永逸的框架都管用。