JV-05 前后端接口
05 · 前后端接口(Front-Back Interface)
📅 预计 45 分钟 | ⭐ = 高频考点 | 📌 中英术语见文末
✍️ 配套练习:网页 quizshengxia.dev/java?module=05
5.1 JSON:前后端通用的"普通话" ⭐
一张格式统一的"快递单"
寄快递,如果 A 公司要求"姓名电话写一行",B 公司要求"分三栏加括号",对接就是灾难,于是有了统一运单。JSON 就是互联网的"统一运单"——一种约定俗成的数据格式,谁都能看懂、谁都能解析。它长得像 JS 对象,但任何语言都能生成和解析。前端的 JavaScript 能解析,后端的 Java、Python 也能解析,所以它是前后端通信的事实标准。
{"name": "国睿", "age": 20} // 对象:键值对
["Java", "Python"] // 数组:一串值
{"code": 200, "data": [{"id": 1, "title": "接口"}]} // 嵌套
JSON 的六种值类型:字符串(双引号包)、数字、布尔(true/false,小写)、null、数组 [ ]、对象 { }。注意布尔和 null 必须小写,True、NULL 都是非法的。JSON 里可以嵌套任意层:对象里套数组,数组里再套对象,真实接口返回的数据基本都是这种三层结构——code 表示结果码、message 表示提示、data 装真正的业务数据。
格式硬性要求:键必须加双引号;字符串只能用双引号不能用单引号;不允许有注释;不允许有 undefined;整个文档必须是 UTF-8 编码。这些要求让 JSON 既严格又好解析——一端生成、任意一端都能读。
JSON 与 XML 对比:
| 对比项 | JSON | XML |
|---|---|---|
| 结构 | {} [] 简洁 |
<tag> 标签冗长 |
| 可读性 | 高 | 中 |
| 解析速度 | 快 | 较慢 |
| 数据量 | 小 | 大(标签冗余) |
| 当前使用 | 接口主流 | 老系统、配置文件 |
Java 里的 JSON:用 Gson(Google 出品)或 Jackson 做序列化和反序列化:
Gson gson = new Gson();
String json = gson.toJson(stu); // 序列化:对象 → JSON
Student back = gson.fromJson(json, Student.class); // 反序列化:JSON → 对象
序列化是把 Java 对象变成 JSON 字符串(发出去给前端),反序列化是把 JSON 字符串变回 Java 对象(接住前端的请求)。两个方向正好相反,是接口编程里天天用的两个动作。
嵌套泛型要小心:反序列化 List<Student> 这种带泛型的类型时,直接 fromJson(json, List.class) 会把元素转成 LinkedTreeMap 而不是 Student,运行时强转会报类型转换错误。正确做法是用 TypeToken 把完整的泛型信息告诉库:Type type = new TypeToken<List<Student>>(){}.getType(),再 fromJson(json, type)。这是接口开发里非常容易踩的坑,值得单独记住。
💡 记忆口诀:
{ }是对象、[ ]是数组;键必须加双引号,布尔和null必须小写——这是 JSON 与 JS 对象的区别。
⚠️ 常见错误
- 键忘了加双引号:
{name: "国睿"}不是合法 JSON。为什么:JSON 规范要求键必须是带双引号的字符串,这是它和 JS 对象字面量的关键区别。怎么避免:手写 JSON 时键一律加双引号;用库生成就不会犯这个错。 - 字符串用了单引号:JSON 只认双引号。为什么:规范如此,单引号在 JSON 里没有特殊含义。怎么避免:转义时用
\",别用单引号替代。 - 末尾多了逗号:
{"a":1, "b":2,}最后一项后多了逗号。为什么:JSON 不允许尾逗号,很多解析器直接抛异常。怎么避免:生成时用库;手写时检查最后一项后面不能有逗号。 - 类型对不上:
fromJson时字段类型与 JSON 不一致(字符串给了数字)会抛异常。为什么:库严格按声明类型转换,转不了就报错。怎么避免:字段类型先核对,日期格式用统一的字符串约定。 - 类没写无参构造:很多 JSON 库靠反射 + 无参构造创建对象,缺了它直接报错。怎么避免:实体类写上无参构造(有参构造写了也补一个无参的),这也是很多框架的通用要求。
5.2 参数传递:四种"把数据带到服务器"的方式
点餐的四种沟通方式
去餐厅点餐,你有四种方式让厨房明白你要什么:写在菜单空白处(Query)、桌号贴在单子上(路径参数)、小纸条递进厨房(请求体)、按固定表格勾选(表单)。前端给后端传参,也正好是这四种:
① Query 字符串:数据拼在 URL 的 ? 后面,多个参数用 & 连接。适合搜索、筛选、翻页这类简单参数。
GET /search?keyword=java&page=2
String kw = req.getParameter("keyword"); // "java"
String page = req.getParameter("page"); // "2",注意是字符串
② 路径参数:数据嵌在路径里,天生适合"对某个资源操作"——/user/123 表示"id 为 123 的用户"。Servlet 里没有现成的 API,要手动从 URL 截取:
String path = req.getRequestURI(); // /user/123
String id = path.substring(path.lastIndexOf("/") + 1); // "123"
③ 请求体 Body(JSON):数据放在请求体里,适合传复杂大对象(一个用户含多个字段)。用 req.getReader() 读输入流,再用 JSON 库解析:
String json = req.getReader().readLine(); // 读一行 JSON(真实项目要多行拼接)
Student stu = gson.fromJson(json, Student.class); // JSON → 对象
④ 表单(application/x-www-form-urlencoded):浏览器 <form> 默认提交格式,POST + 键值对,编码方式是 URL 编码。它和 Query 一样用 getParameter 取,区别只在于数据放在请求体里而不是 URL 上:
String user = req.getParameter("username"); // 表单字段
String pwd = req.getParameter("password");
四种方式对比:
| 方式 | 数据位置 | Content-Type | 典型场景 | 获取方式 |
|---|---|---|---|---|
| Query | URL ? 后 |
无 | 搜索、翻页 | getParameter |
| 路径参数 | URL 路径里 | 无 | 查/改某资源 | 解析 URL |
| Body(JSON) | 请求体 | application/json |
新增/修改复杂数据 | 读流 + JSON 库 |
| 表单 | 请求体 | x-www-form-urlencoded |
登录、注册 | getParameter |
两个易混点:第一,getParameter 对 Query 和表单都有效,但对 Body 里的 JSON 无效——JSON 必须读流再解析;第二,中文参数要保证服务器设置了 UTF-8 编码,否则乱码。
GET 与 POST 的选择:GET 语义是"查询",参数拼在 URL 上,能被浏览器缓存、能被收藏夹存下来,但也意味着参数会留在历史记录里——不适合传密码。POST 语义是"提交",数据放请求体,更安全也更能传大数据。约定俗成:查询用 GET,会改变数据或传敏感数据用 POST。这个选择和下一节的 REST 理念是一脉相承的。
💡 记忆口诀:简单小数据走 URL,复杂大对象走 Body;查资源爱用路径,筛列表爱用 Query。JSON 走流,表单走
getParameter。
⚠️ 常见错误
getParameter永远返回字符串:page是"2"不是2。为什么:HTTP 传的本来就是文本。怎么避免:要当数字用必须先Integer.parseInt(page),别忘了捕获NumberFormatException。getParameter对 Body 里的 JSON 无效:直接取拿到null。为什么:JSON 在请求体流里,不在参数表里。怎么避免:JSON 一律读getReader()再用库解析。- 路径参数没处理空值:
/user/(末尾没数字)直接substring会越界或得到空串。怎么避免:解析前先判断路径长度,非法参数返回 404 或 400。 - GET 请求带请求体:浏览器 GET 一般不携带 Body,服务器也读不到。怎么避免:有复杂数据要传就用 POST,别指望 GET 发 JSON。
- 参数名拼错:前端传
user_name,后端取username,拿到null。为什么:这是"静默失败",不报错只返空。怎么避免:前后端先对好字段名,排查时先System.out.println把所有参数打出来看一遍。
5.3 REST 风格:用"名词 + 方法"说话 ⭐
图书馆的"借书系统"思路
图书馆管理,你不会说"帮我把借书这件事执行一下",而是说资源(哪本书)、说动作(借/还)。REST 同理:一切看成"资源",资源用名词命名(/books、/users),用 HTTP 方法表达操作。一句话概括:URL 只说是谁,方法只说做什么。
一个资源,四个 HTTP 方法:
| HTTP 方法 | 动作 | 对应 SQL | 例子 |
|---|---|---|---|
| GET | 查 | SELECT | GET /books |
| POST | 增 | INSERT | POST /books |
| PUT | 改 | UPDATE | PUT /books/1 |
| DELETE | 删 | DELETE | DELETE /books/1 |
URL 设计原则:只用名词不用动词(/deleteBook ❌,/books/1 + DELETE ✅);复数名词表示集合(/books、/users);层级用 / 表达从属(/users/5/orders = 用户 5 的订单);查询条件放 Query(/books?author=鲁迅);每个资源配一个 id(/books/1)。
幂等性概念:同一个操作做一次和做一百次,结果一样就叫幂等。GET、PUT、DELETE 是幂等的,POST 不是——所以新增用 POST(每次都会多一条),覆盖更新用 PUT(反复执行结果相同)。这也是为什么"更新"用 PUT 而不用 POST 的原因之一,设计题里常考。
接口设计小建议:列表接口一般带分页,GET /books?page=1&size=10,返回 {"total": 100, "list": [...]},前端才好做分页条;查询具体资源用路径参数 GET /books/1;创建资源的接口成功后返回 201 和新建资源的 id,前端拿到 id 才好跳转详情页。这些细节是设计题里能拿分的点,别只会写四个方法名。
Servlet 里的对应:doGet 对应查、doPost 对应增、doPut 对应改、doDelete 对应删。同一个路径 /books/* 用一个 Servlet 写四个方法,浏览器按 HTTP 方法路由到对应方法,就是一套 REST 接口。
💡 记忆口诀:URL 只有名词,动作全交给方法;
GET不改变数据,POST/PUT/DELETE才动数据。幂等四兄弟:GET、PUT、DELETE 幂等,POST 不幂等。
5.4 一个返回 JSON 的接口示例 ⭐
后端把"原料"打包递出来
RESTful 接口规范(常考设计题):URL 用名词表达资源、用 HTTP 方法表达动作、返回 JSON、配合合理的状态码。状态码是"接口的暗号",前端看到暗号就知道结果对不对:
| 状态码 | 含义 | 场景 |
|---|---|---|
| 200 | 成功 | 查询/更新成功 |
| 201 | 创建成功 | POST 新增完成 |
| 400 | 请求有误 | 参数不对、缺字段 |
| 401 | 未认证 | 没登录就访问受保护接口 |
| 404 | 资源不存在 | 查了不存在的 id |
| 500 | 服务器内部错误 | 代码异常 |
统一的响应格式:好的接口不只返回裸数据,而是包一层统一的壳——code(业务码)、message(提示)、data(数据)。前端拿到先看 code,等于 0 才取 data,异常时能拿到统一格式的错误信息,不用为每个接口单独写解析逻辑。
@WebServlet("/api/users")
public class UserApiServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
resp.setContentType("application/json;charset=UTF-8"); // 声明返回 JSON
Map<String, Object> result = new LinkedHashMap<>();
result.put("code", 200);
result.put("data", new Student(1, "国睿", 20)); // 模拟查库
resp.getWriter().write(new Gson().toJson(result));
}
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
try {
String json = req.getReader().readLine(); // 读请求体
Student stu = gson.fromJson(json, Student.class); // 反序列化
resp.setStatus(201); // 新增成功用 201
resp.getWriter().write("{\"code\":201,\"message\":\"创建成功\"}");
} catch (Exception e) {
resp.setStatus(400); // 参数有问题
resp.getWriter().write("{\"code\":400,\"message\":\"参数格式错误\"}");
}
}
}
这个例子展示了三个要点:doGet 返回统一格式 JSON;doPost 读请求体再入库,成功用 201;异常时给 400 而不是 500——前端能分辨"是你传错还是我服务器炸了"。
为什么用 DTO 而不是实体类:实体类往往带自增主键、密码哈希、创建时间等字段,直接序列化就把"家底"全亮给前端了。DTO(Data Transfer Object)是专门为接口定制的一层壳,只放要返回的字段,还能组合多个表的数据。分层清晰的项目里,Entity → DTO 的转换是标准流程,接口设计题里提到 DTO 能明显提升方案的完整度(这是工程素养,不是考试投机)。
💡 记忆口诀:接口三件套 = URL(资源)+ 方法(动作)+ 状态码(结果)。返回统一
code/message/data,前端只认这一种格式。
⚠️ 常见错误
- 忘写
setContentType("application/json"):不声明,浏览器把 JSON 当纯文本乱码显示,前端JSON.parse也失败。怎么避免:所有返回 JSON 的接口统一先设application/json;charset=UTF-8。 - 中文乱码:返回中文必须
charset=UTF-8,漏了就是???。为什么:默认编码不是 UTF-8。怎么避免:setContentType里把编码带上,一劳永逸。 - 状态码永远 200:业务失败也返回 200 + 错误数据,前端只能靠猜。怎么避免:HTTP 状态码表达"请求本身对不对",业务 code 表达"业务成没成",两层一起用。
- URL 用动词:
/deleteBook?id=3是 RPC 思维不是 REST。为什么:REST 要求资源是名词、动作交给方法。怎么避免:改成DELETE /books/3。 - 把不该暴露的字段序列化返回:
toJson(整个对象)会把密码哈希、内部字段也发出去。怎么避免:用 DTO(只含要返回字段的对象)而不是直接序列化实体类。
5.5 CORS:浏览器的"小区门卫" ⭐
为什么跨域请求被拦
浏览器是小区门卫:你住 A 区想去 B 区串门,门卫先要看 B 区物业给没给你"通行证"。浏览器执行同源策略(same-origin policy):A 网站的页面默认只能访问 A 网站的接口。"同源"= 协议 + 域名 + 端口三个全一样,任一不同即跨域,浏览器拦截响应。
https://shengxia.dev:443/api/user
协议 https 域名 shengxia.dev 端口 443
三个全一样才算同源,有一个不同就是跨域
为什么拦截:不拦的话,你在恶意网站打开的页面,就能偷偷请求你银行网站的接口——浏览器会自动带上银行的 Cookie,攻击者就能冒充你的身份操作转账。拦截是浏览器的安全机制,不是服务器的锅,更不是后端配置错了。
跨域怎么解决(服务器加响应头放行):
resp.setHeader("Access-Control-Allow-Origin", "https://my-site.com"); // 允许指定源
// 用 * 表示允许所有来源(注意:需要带 Cookie 时不能是 *,必须写具体域名)
简单请求 vs 预检请求:GET、POST 且不带自定义头、Content-Type 是基本类型的是简单请求,浏览器直接发;一旦用了 PUT/DELETE、自定义头、Content-Type: application/json,浏览器会先发一个 OPTIONS 预检请求探路——服务器必须对 OPTIONS 正确响应(返回允许的方法和头),真正的请求才发出。所以"JSON 请求跨域失败"往往卡在预检这一步:
// 处理预检请求:必须回应 OPTIONS,否则浏览器后续请求发不出去
if ("OPTIONS".equals(req.getMethod())) {
resp.setHeader("Access-Control-Allow-Origin", "https://my-site.com");
resp.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
resp.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
resp.setStatus(200);
return;
}
为什么 <script>、<img> 标签不受同源限制:同源策略限制的是"脚本主动发起的请求"(fetch、XHR),而 <img src>、<script src>、<link> 这些标签加载资源天然允许跨域——否则 CDN 就全废了。这个"被动加载"的通道也被人利用过:恶意页面用 <script src="https://银行/api/..."> 偷数据(JSONP 攻击),所以现代接口会配合 X-Content-Type-Options、CSRF Token 等防护。理解"主动请求受限制、被动加载不受限",你就真正吃透了同源策略的边界。
💡 记忆口诀:同源 = 协议+域名+端口;跨域被拦是门卫尽责,解法是服务器发 CORS"通行证"。复杂请求先 OPTIONS 探路,别忘了也给它发证。
⚠️ 常见错误
- 以为是后端没写对接口:跨域报错出现在浏览器控制台,接口其实执行成功了——是浏览器拦截了响应,不是请求没发出。怎么排查:看网络面板,请求已发出且服务器有响应,那就是 CORS 头的问题。
Access-Control-Allow-Origin用*还带 Cookie:*表示任意来源,浏览器不允许同时"任意来源 + 带凭证"。怎么避免:带 Cookie 时写具体域名,并加Access-Control-Allow-Credentials: true。- 只处理 GET 不处理 OPTIONS:用 JSON 的 POST 跨域失败。为什么:
Content-Type: application/json触发预检,服务器没响应 OPTIONS。怎么避免:对OPTIONS返回 200 + 允许方法和头。 - 把 CORS 当安全隐患禁止:CORS 是"服务器主动授权",不授权就保持默认拦截,这是安全的。怎么避免:只在确实需要跨域的接口放行,别全站
*。
5.6 前后端分离:后厨只出菜,前台只管摆盘
一份 JSON,两端分工
后厨把菜做熟(后端出数据),前厅摆盘上桌(前端渲染页面),两者通过"传菜口"(接口)传递标准的菜(JSON)。这就是前后端分离——后端只出 JSON,前端(Vue/React)拿到 JSON 再渲染成页面。后端不再返回整个 HTML,前端也不再依赖 JSP 这类服务端模板。
对比两种模式:
| 对比项 | 传统(JSP 服务端渲染) | 前后端分离 |
|---|---|---|
| 后端返回 | 完整 HTML 页面 | 纯 JSON 数据 |
| 谁渲染页面 | 服务器 | 前端 JS |
| 前端技术 | 依赖 JSP 标签 | Vue / React 独立 |
| 部署方式 | 前后端一起 | 可分开部署 |
| 改一处 | 动整个服务端 | 各改各的 |
好处:前后端可并行开发——只要先定好接口格式(字段名、状态码),两端同时开工互不等待;一套后端接口能同时喂网页、App、小程序,不用为每个端重写接口;前端改版不用动后端。代价:要处理跨域、维护接口文档、增加前后端联调成本——接口没定好,两边扯皮是家常便饭。
后端接口的模式:无论什么业务,后端接口基本是同一套三步走——收参数 → 查数据(或处理业务)→ 返回 JSON。代码里也总是三行:getParameter 收参数、dao 查数据、gson.toJson 返回:
@WebServlet("/api/news")
public class NewsApiServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
resp.setContentType("application/json;charset=UTF-8");
String page = req.getParameter("page"); // 1. 收参数
List<News> list = newsDao.queryByPage(page); // 2. 查数据
resp.getWriter().write(new Gson().toJson(list)); // 3. 返回 JSON
}
}
// 前端: fetch('/api/news?page=2') → 拿到 JSON → 渲染成新闻列表
接口文档很重要:前后端分离后,接口格式就是两边的"契约"。团队里通常用 OpenAPI(Swagger)自动生成接口文档——后端写注释,文档自动更新,前端照着文档调。没有文档的接口,联调时前端猜字段、后端改字段,是最耗时的环节。课设里哪怕手写一份简单的接口说明(每个接口的路径、方法、参数、返回示例),整个项目也会显得完整许多。
💡 记忆口诀:前后端分离 = 后端只出 JSON(菜单上的菜),前端负责摆盘上桌(渲染)。接口格式就是两边的"菜单",先定菜单再开工。
📌 双语术语表(本讲)
| 中文 | English | 记忆点 |
|---|---|---|
| 接口 | API | 前后端通信通道 |
| JSON | JSON | 统一运单,{} 对象 [] 数组 |
| 序列化 / 反序列化 | serialize / deserialize | 对象 ↔ JSON |
| 查询参数 | query parameter | URL ? 后面 |
| 路径参数 | path parameter | /user/123 |
| 请求体 | request body | 数据在 Body 里 |
| 表单编码 | form-urlencoded | 表单默认格式 |
| 资源 | resource | URL 里的名词 |
| 幂等 | idempotent | 做一次和做多次结果一样 |
| 状态码 | status code | 200 / 404 / 500 |
| 同源策略 | same-origin policy | 协议+域名+端口 |
| 跨域 | cross-origin | 浏览器拦截 |
| 预检请求 | preflight request | OPTIONS 探路 |
| CORS | CORS | 服务器发的通行证 |
| 前后端分离 | front-back separation | 后端只出数据 |
⭐ 本讲考点清单
- JSON:
{}对象、[]数组、键加双引号、可嵌套 - Gson:
toJson序列化、fromJson反序列化 - Query 参数用
getParameter取,返回字符串要转换 - 路径参数手动解析 URL:
/user/123截出123 - Body 传 JSON 读
getReader()再解析 - 表单提交格式是
application/x-www-form-urlencoded - REST:资源用名词,GET 查 / POST 增 / PUT 改 / DELETE 删
- 幂等:GET、PUT、DELETE 幂等,POST 不幂等
- 返回 JSON 要
setContentType("application/json") - 状态码:200 成功、404 不存在、500 服务端错误
- 同源策略:协议+域名+端口不同即跨域,浏览器拦截;CORS 响应头放行、复杂请求先发 OPTIONS
- 前后端分离:后端只出 JSON,前端负责渲染页面