Java 项目实战: 交友项目 - 前后端分离的工程实践

发布时间:2026/10/15 7:37:41
Java 项目实战: 交友项目 - 前后端分离的工程实践 概述交友项目的前端是 Flutter 写的 App后端是一堆 Java 服务两边从第一天起就是两拨人在写代码。分离本身不是难点难的是约定——接口返回长什么样、字段叫什么、跨域怎么办、上线怎么部署。这篇只讲工程落地这一层。纲要接口契约怎么约定统一响应结构ErrorResult/ HTTP 状态码口径URL 资源化命名与 HTTP 方法表达动作分页结构PageResult的固定字段全局异常处理器如何把异常翻译成统一响应接口文档与协作流程串行协作先文档后开发与并行协作Mock 先行文档与实现漂移的风险与三种应对跨域问题与解决跨域从哪来端口不同即跨域前端代理 / 后端 CORS / 网关统一处理 三条路径对比开发期用代理、生产期用网关的常见组合联调与调试手段Postman / Apifox 自测、浏览器 Network 面板联调问题的四步定位顺序、跨域与业务错误的区分部署方式的变化前端npm run build产出静态资源、Nginx 托管Nginx 配置示例静态目录 /api/反向代理端到端数据流Mermaid 时序图实践坑清单登录页原型与接口对照API 速览常见误解澄清接口契约怎么约定前后端分离的核心不是你写页面我写接口而是接口契约驱动。契约不定死两边各写各的联调时就是互相甩锅现场。契约里最容易被忽略、也最影响协作效率的是返回结构——前端要能用一套代码处理所有接口的成败。统一响应结构后端所有接口的响应体应当是同一层封装。交友项目在mf-model的vo包里放了一份错误响应体errCode给机器判断errMessage给人看packagecom.mf.model.vo;importlombok.AllArgsConstructor;importlombok.Builder;importlombok.Data;importlombok.NoArgsConstructor;/** * 统一错误响应体 * errCode —— 业务错误码前端按码分支处理 * errMessage —— 面向用户的提示文案 */DataNoArgsConstructorAllArgsConstructorBuilderpublicclassErrorResult{privateStringerrCode;privateStringerrMessage;publicstaticErrorResulterror(){returnErrorResult.builder().errCode(999999).errMessage(系统异常稍后再试).build();}publicstaticErrorResultfail(){returnErrorResult.builder().errCode(000001).errMessage(发送验证码失败).build();}publicstaticErrorResultloginError(){returnErrorResult.builder().errCode(000002).errMessage(验证码失效).build();}publicstaticErrorResultmobileError(){returnErrorResult.builder().errCode(000004).errMessage(手机号码已注册).build();}}错误码用静态工厂方法集中定义好处是错误码和文案只有一处来源改文案不用满项目搜字符串。HTTP 状态码的口径要和响应体配套。交友项目的做法是成功走ResponseEntity.ok(...)200错误走ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)500body 统一是ErrorResult。第 01 章LoginController里就是这么写的packagecom.mf.server.controller;importcom.mf.server.service.UserService;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.http.ResponseEntity;importorg.springframework.web.bind.annotation.*;importjava.util.Map;RestControllerRequestMapping(/user)publicclassLoginController{AutowiredprivateUserServiceuserService;/** * 获取登录验证码 * 请求参数phone */PostMapping(/login)publicResponseEntitylogin(RequestBodyMapmap){Stringphone(String)map.get(phone);userService.sendMsg(phone);returnResponseEntity.ok(null);// 正常返回状态码 200}/** * 校验登录 * 请求参数phone、verificationCode * 响应{ token, isNew } */PostMapping(/loginVerification)publicResponseEntityloginVerification(RequestBodyMapmap){Stringphone(String)map.get(phone);Stringcode(String)map.get(verificationCode);MapretMapuserService.loginVerification(phone,code);returnResponseEntity.ok(retMap);}}另一种常见口径是HTTP 状态码永远是 200业务成败全靠 body 里的code区分。两种都行但必须全项目统一——最怕的是 A 接口用状态码、B 接口用 code前端拦截器只能疯狂写特判。全局异常处理器契约要有异常出口。业务代码只管抛异常由ControllerAdvice统一翻译成响应体前端永远只面对一种错误格式packagecom.mf.server.exception;importcom.mf.model.vo.ErrorResult;importorg.springframework.http.HttpStatus;importorg.springframework.http.ResponseEntity;importorg.springframework.web.bind.annotation.ControllerAdvice;importorg.springframework.web.bind.annotation.ExceptionHandler;ControllerAdvicepublicclassExceptionAdvice{ExceptionHandler(BusinessException.class)publicResponseEntityhandlerException(BusinessExceptionbe){be.printStackTrace();ErrorResulterrorResultbe.getErrorResult();returnResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResult);}ExceptionHandler(Exception.class)publicResponseEntityhandlerException1(Exceptionbe){be.printStackTrace();returnResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ErrorResult.error());}}配套的BusinessException只需要携带一个ErrorResultpackagecom.mf.server.exception;importcom.mf.model.vo.ErrorResult;importlombok.Data;DatapublicclassBusinessExceptionextendsRuntimeException{privateErrorResulterrorResult;publicBusinessException(ErrorResulterrorResult){super(errorResult.getErrMessage());this.errorResulterrorResult;}}这样 Service 层写throw new BusinessException(ErrorResult.loginError())前端收到的永远是{errCode:000002,errMessage:验证码失效}。URL 命名规范资源化路径动作交给 HTTP 方法。交友项目 app-server 用RequestMapping(/user)做模块前缀方法级再细化约定项示例为什么重要模块前缀/user、/movements、/messages前端按模块聚合请求代理和网关可按前缀分流资源用名词复数/movements、/comments避免/getMovements、/doLike这类动词路径动作交给 HTTP 方法POST /user/login、GET /user/{id}语义清晰缓存/重试策略可按方法区分路径参数用{}/user/{id}与 SpringPathVariable天然对应查询参数用 kebab/小驼峰统一page、pagesize分页参数名跨接口一致前端不用记两套版本前缀/api/v1/...在交友项目这种内部 App 场景里没有强上因为 App 是强制升级的不存在老客户端长时间存活的问题。如果是对外开放 API版本前缀几乎是必选项。分页结构约定分页是最高频的返回形态字段名必须一次定死。交友项目用PageResult五个字段包打天下packagecom.mf.model.vo;importlombok.AllArgsConstructor;importlombok.Data;importlombok.NoArgsConstructor;importjava.io.Serializable;importjava.util.Collections;importjava.util.List;DataAllArgsConstructorNoArgsConstructorpublicclassPageResultimplementsSerializable{privateLongcounts0L;// 总记录数privateIntegerpagesize10;// 页大小privateLongpages0L;// 总页数privateIntegerpage1;// 当前页码privateList?itemsCollections.emptyList();// 列表数据publicPageResult(Integerpage,Integerpagesize,Longcounts,Listlist){this.pagepage;this.pagesizepagesize;this.itemslist;this.countscounts;this.pagescounts%pagesize0?counts/pagesize:counts/pagesize1;}}注意pages是构造器里算出来的不是让前端自己除。前端只管items渲染列表、counts显示共 N 条落一屏加载更多就page。字段名counts、pagesize、items这种非标准写法一旦定了就别改——改一次前端全量返工。接口文档与协作流程分离之后接口文档就是契约本身。讲义里明确点出来“两端之间通过接口文档定义规范前端和后端基于文档进行数据请求和格式的响应。” 团队协作通常两种走法串行后端先把文档写出来接口路径、请求参数、响应示例前端照着文档先把页面和请求逻辑写完后端再实现。风险是前端干等文档。并行后端先出文档 Mock 数据YApi、Apifox 都能一键生成 Mock 服务前端直接连 Mock 地址开发后端同时写实现。切换真实环境时只改一个 baseURL。交友项目的实际做法偏并行——App 端用原型图03-资料/交友项目移动端原型V1.2先定页面和交互接口文档定请求/响应格式后端在mf-app-server里实现。第 3.1.3 节把 Postman 列为标配工具就是为了后端在交付前能自测接口。文档与实现漂移是分离模式最大的协作风险。文档写的是username代码里返回的是user_name联调时前端只会看到undefined。三种应对从轻到重手段做法成本契约评审接口定稿前前后端一起过一遍双方签字低但靠人自动化校验用 OpenAPI/Swagger 注解生成文档与代码同源中需改造接口测试兜底后端每个接口配 Postman/Swagger 用例回归时跑一遍中能提前发现前端契约测试Mock 层按文档写死结构后端不符合就联调失败高但最拦得住一句话能把文档从代码里自动生成出来就别手写文档。跨域问题与解决前端跑在一个端口、后端跑在另一个端口浏览器就认为这是跨域。交友项目后端mf-app-server监听18080如果前端本地开发服务跑在8080端口不同 → 协议 域名 端口任一不同即为跨域 → 浏览器会在发请求前先做同源检查不通过就把响应拦下来。需要先纠正一个认知跨域是浏览器的安全策略不是后端的 bug。后端其实收到请求并处理了只是响应里缺了允许跨域的响应头浏览器拒绝把结果交给 JS。所以 Network 面板里经常看到请求有状态码、却报了 CORS——请求成功了是浏览器不给你用。三条解决路径路径做法适用场景代价前端代理开发服务器Vite/webpack devServer把/api代理到后端开发期首选只在本机生效上线无效后端 CORS后端加CrossOrigin或全局 CORS 配置前后端同域不同端口的小项目开发配置可能被带上生产网关统一处理网关如 Spring Cloud Gateway统一加 CORS 响应头生产环境推荐需要网关层支持实际项目里最省心的组合是开发期用前端代理生产期用网关。开发期用代理的原因很直接——前端配置里改一行target就完事不用动后端代码也避免了把允许 localhost:8080 跨域这种调试配置带进生产环境。生产期的跨域处理应该收敛到网关一处而不是散在每个业务服务里各配各的。如果要在后端配 CORS注意区分前端代理能不能解决和必须后端解决两种场景代理走的是服务器到服务器压根不经过浏览器的同源检查所以开发期代理能覆盖绝大多数情况。CORS 的响应头细节Access-Control-Allow-Origin等属于网关配置范畴本系列另有《网关的 cors 跨域配置》一篇专门展开这里不重复。联调与调试手段联调出问题时别急着怀疑代码按固定顺序排查能省一大半时间。第一步请求发出去了吗打开浏览器开发者工具的 Network 面板前端点了按钮但列表里一条记录都没有说明问题在前端——请求根本没构造出来或者被某个return提前跳过了。这一步能直接砍掉一半的排查范围。第二步状态码是什么4xx 是请求方的问题路径写错 404、参数缺失 400、鉴权失败 401/4035xx 是服务端的问题业务异常 500、服务没起来连不上。看到 500 就去后端看堆栈别在前端瞎改。第三步响应体内容对不对状态码 200 也不代表对。展开响应体字段名和文档对得上吗data里是不是空的这一层最常见的问题是字段名拼写或大小写不一致。第四步是跨域还是业务错误区分方法很明确跨域时浏览器控制台会显式打印CORS或 “blocked by CORS policy”且 Network 里请求往往显示成功却被拦业务错误则是一个正常返回的响应体里面errCode/errMessage有值。前者去改代理或网关配置后者去查业务代码。工具层面后端用 Postman 或 Apifox 单独跑接口能快速回答是前端请求的问题还是后端逻辑的问题。交友项目讲义把 Postman 作为环境搭建的一部分就是这个用途。部署方式的变化分离部署带来的是两边可以各自独立上线。前端Flutter 项目打包成 APK 交给客户端团队交友项目是 App不是浏览器页面如果换成 Web 前端则是npm run build产出dist静态资源目录扔给 Nginx 托管。静态资源不经过 Java 进程天然可以上 CDN。后端只提供 APImf-app-server按接口压力独立扩容和前端发布互不影响。典型的 Nginx 配置长这样——静态资源和后端接口在同一个域名下前端请求/api/时反向代理到后端服务同源问题自然消失server { listen 80; server_name localhost; # 前端静态资源Vue/React 打包产物 location / { root /usr/share/nginx/html; index index.html; # 单页应用刷新 404 兜底 try_files $uri $uri/ /index.html; } # 后端接口反向代理 location /api/ { proxy_pass http://127.0.0.1:18080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }注意proxy_pass结尾有没有/会导致路径拼接结果不同http://127.0.0.1:18080/会把/api/剥掉http://127.0.0.1:18080则会把/api/原样带上。这一条坑过很多人。工程目录本章快照大致是这样组织的mf ├── mf-autoconfig # 自动装配组件短信等 ├── mf-model # 实体类 VO含 ErrorResult、PageResult ├── mf-commons # 工具类JwtUtils 等 ├── mf-app-server # 与手机端交互的入口模块18080 │ └── src/main/java/com/mf/server │ ├── controller # 对外 HTTP 接口 │ ├── service # 业务编排 │ └── exception # 全局异常处理 └── mf-dubbo # Dubbo 子模块 ├── mf-dubbo-interface # 接口定义 ├── mf-dubbo-db # 数据库服务18081 └── mf-dubbo-mongo # MongoDB 服务端到端数据流把一次业务请求从浏览器到后端再回来的完整链路画清楚分离模式的数据流就一目了然了Dubbo 服务/DB后端应用(mf-app-server)Nginx浏览器/AppDubbo 服务/DB后端应用(mf-app-server)Nginx浏览器/AppGET /api/user/1 (静态页已加载)反向代理 /user/1 (剥掉 /api)查询用户数据领域对象JSON 响应体 { code, data }原样转发 JSON前端渲染页面前端拿到 JSON 后只管渲染后端只管返回数据中间靠 Nginx或网关把两边拼在一个域名下。实践坑清单坑表现处理方案返回结构不统一有的接口{code,data}有的直接返回数组全项目统一封装异常也走同一结构字段命名风格不一致后端user_name前端读userName拿到undefined服务端序列化统一用驼峰或前端拦截器做转换日期格式不统一一会儿时间戳、一会儿yyyy-MM-dd字符串全局配置 Jackson 日期格式契约里写死分页参数名不一致这个接口page、那个接口pageNum分页字段进契约统一用page/pagesize跨域配置开发/生产不一致本地好使一上线全报 CORS开发用代理、生产用网关配置分离接口文档过期照着文档写联调时字段全对不上文档从代码生成或契约评审 接口测试错误码含义不明前端只知道 500不知道具体原因errCode分段管理前端按码分支proxy_pass尾斜杠代理后路径多了一层/api明确剥前缀proxy_pass http://host:port/;登录页原型与接口对照03-资料/交友项目移动端原型V1.2/登录页.html是 App 端登录页的原型正好可以拿来对照接口契约怎么落地┌──────────────────────────────┐ │ │ │ 项目 · 交友 │ ← 品牌区 │ │ │ ┌────────────────────────┐ │ │ │ 请输入手机号 │ │ ← 手机号输入框 │ └────────────────────────┘ │ │ ┌──────────────┬─────────┐ │ │ │ 请输入验证码 │ 获取验证码│ │ ← 验证码输入 获取按钮 │ └──────────────┴─────────┘ │ │ │ │ ┌────────────────────────┐ │ │ │ 登 录 │ │ ← 主按钮 │ └────────────────────────┘ │ │ │ │ ☐ 已阅读并同意《用户协议》 │ ← 协议勾选 └──────────────────────────────┘要点手机号输入框11 位数字校验未填或格式错时获取验证码按钮应置灰。获取验证码按钮点击调POST /user/login参数{ phone: 138xxxx }成功后按钮进入 60 秒倒计时。登录按钮点击调POST /user/loginVerification参数{ phone: ..., verificationCode: 123456 }。登录响应{ token: xxx, isNew: false }。token是后续所有请求的鉴权凭证isNew为true时前端跳首次登录信息完善页。协议勾选前端校验未勾选不发请求——这类校验能放到前端的就别占后端资源。API 速览接口方法路径请求参数响应获取登录验证码POST/user/login{ phone }空 body200校验登录POST/user/loginVerification{ phone, verificationCode }{ token, isNew }错误响应统一—任意—{ errCode, errMessage }分页响应统一—列表类接口page、pagesize{ counts, pagesize, pages, page, items }官方文档Spring MVC — CORSControllerAdvice 与异常处理Nginx proxy_pass 指令MDN — 同源策略Vite — Server Proxy常见误解澄清“前后端分离就是前端写页面、后端写接口。”这只是分工描述。真正决定成败的是接口契约驱动和独立部署——没有统一契约分工再清楚也是联调灾难。“跨域是后端的 bug。”跨域是浏览器的同源策略在起作用后端请求其实处理了。排查时先看是不是 CORS 提示别上来就改后端业务代码。“前端不需要了解后端。”不了解契约前端根本没法联调。至少要知道接口返回结构、错误码含义、分页字段名。“分离之后就不用管 SEO 了。”对 Web 端的 C 端页面分离尤其是纯前后端分离 JS 渲染确实会削弱 SEO需要 SSR/预渲染补救。但交友项目是 App走的是原生客户端不涉及这个问题——这也是它敢全量分离的原因之一。总结分离模式的工程重心在约定统一响应结构ErrorResult、分页结构PageResult、URL 命名规范一次定死、全项目一致。错误处理要收敛到全局异常处理器业务代码只抛异常前端只面对一种错误格式。接口文档就是契约能自动生成就别手写文档漂移是分离模式头号协作风险。跨域开发期用前端代理、生产期用网关别把调试用的 CORS 配置带上生产。部署上前端交给 Nginx 托管静态资源并反向代理/api/后端只管出 API、独立扩容。联调按请求发了吗 → 状态码 → 响应体 → 跨域还是业务错四步走比盲目改代码快得多。下一篇按讲义顺序进入「开发工具与环境搭建」讲 Linux 虚拟机、Android 模拟器、Postman 的安装以及 Maven 聚合工程从零搭起来的过程。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询