05 · 前后端接口(Front-Back Interface)

📅 预计 45 分钟 | ⭐ = 高频考点 | 📌 中英术语见文末
✍️ 配套练习:网页 quiz shengxia.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 必须小写,TrueNULL 都是非法的。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 对象的区别。

⚠️ 常见错误

  1. 键忘了加双引号{name: "国睿"} 不是合法 JSON。为什么:JSON 规范要求键必须是带双引号的字符串,这是它和 JS 对象字面量的关键区别。怎么避免:手写 JSON 时键一律加双引号;用库生成就不会犯这个错。
  2. 字符串用了单引号:JSON 只认双引号。为什么:规范如此,单引号在 JSON 里没有特殊含义。怎么避免:转义时用 \",别用单引号替代。
  3. 末尾多了逗号{"a":1, "b":2,} 最后一项后多了逗号。为什么:JSON 不允许尾逗号,很多解析器直接抛异常。怎么避免:生成时用库;手写时检查最后一项后面不能有逗号。
  4. 类型对不上fromJson 时字段类型与 JSON 不一致(字符串给了数字)会抛异常。为什么:库严格按声明类型转换,转不了就报错。怎么避免:字段类型先核对,日期格式用统一的字符串约定。
  5. 类没写无参构造:很多 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

⚠️ 常见错误

  1. getParameter 永远返回字符串page"2" 不是 2为什么:HTTP 传的本来就是文本。怎么避免:要当数字用必须先 Integer.parseInt(page),别忘了捕获 NumberFormatException
  2. getParameter 对 Body 里的 JSON 无效:直接取拿到 null为什么:JSON 在请求体流里,不在参数表里。怎么避免:JSON 一律读 getReader() 再用库解析。
  3. 路径参数没处理空值/user/(末尾没数字)直接 substring 会越界或得到空串。怎么避免:解析前先判断路径长度,非法参数返回 404 或 400。
  4. GET 请求带请求体:浏览器 GET 一般不携带 Body,服务器也读不到。怎么避免:有复杂数据要传就用 POST,别指望 GET 发 JSON。
  5. 参数名拼错:前端传 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)。

幂等性概念:同一个操作做一次和做一百次,结果一样就叫幂等。GETPUTDELETE 是幂等的,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,前端只认这一种格式。

⚠️ 常见错误

  1. 忘写 setContentType("application/json"):不声明,浏览器把 JSON 当纯文本乱码显示,前端 JSON.parse 也失败。怎么避免:所有返回 JSON 的接口统一先设 application/json;charset=UTF-8
  2. 中文乱码:返回中文必须 charset=UTF-8,漏了就是 ???为什么:默认编码不是 UTF-8。怎么避免setContentType 里把编码带上,一劳永逸。
  3. 状态码永远 200:业务失败也返回 200 + 错误数据,前端只能靠猜。怎么避免:HTTP 状态码表达"请求本身对不对",业务 code 表达"业务成没成",两层一起用。
  4. URL 用动词/deleteBook?id=3 是 RPC 思维不是 REST。为什么:REST 要求资源是名词、动作交给方法。怎么避免:改成 DELETE /books/3
  5. 把不该暴露的字段序列化返回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 探路,别忘了也给它发证。

⚠️ 常见错误

  1. 以为是后端没写对接口:跨域报错出现在浏览器控制台,接口其实执行成功了——是浏览器拦截了响应,不是请求没发出。怎么排查:看网络面板,请求已发出且服务器有响应,那就是 CORS 头的问题。
  2. Access-Control-Allow-Origin* 还带 Cookie* 表示任意来源,浏览器不允许同时"任意来源 + 带凭证"。怎么避免:带 Cookie 时写具体域名,并加 Access-Control-Allow-Credentials: true
  3. 只处理 GET 不处理 OPTIONS:用 JSON 的 POST 跨域失败。为什么Content-Type: application/json 触发预检,服务器没响应 OPTIONS。怎么避免:对 OPTIONS 返回 200 + 允许方法和头。
  4. 把 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 后端只出数据

⭐ 本讲考点清单

  1. JSON:{} 对象、[] 数组、键加双引号、可嵌套
  2. Gson:toJson 序列化、fromJson 反序列化
  3. Query 参数用 getParameter 取,返回字符串要转换
  4. 路径参数手动解析 URL:/user/123 截出 123
  5. Body 传 JSON 读 getReader() 再解析
  6. 表单提交格式是 application/x-www-form-urlencoded
  7. REST:资源用名词,GET 查 / POST 增 / PUT 改 / DELETE 删
  8. 幂等:GET、PUT、DELETE 幂等,POST 不幂等
  9. 返回 JSON 要 setContentType("application/json")
  10. 状态码:200 成功、404 不存在、500 服务端错误
  11. 同源策略:协议+域名+端口不同即跨域,浏览器拦截;CORS 响应头放行、复杂请求先发 OPTIONS
  12. 前后端分离:后端只出 JSON,前端负责渲染页面