一、PATCH 请求方式
1-1、PATCH 是什么
PATCH 是一种 HTTP 请求方法 ,用于对服务器上已有资源进行部分修改(partial update)。
-
全名 :
PATCH
-
作用 :修改资源的一部分字段,而不是像
PUT
那样替换整个资源。 -
典型场景:
-
更新用户资料中的单个字段(如只改邮箱或昵称)。
-
更新订单状态,而不改变订单的其它信息。
-
修改文档的某一部分数据。
-
与其他方法对比
方法 | 含义 | 数据传递 | 是否需要传递完整资源 |
---|---|---|---|
GET | 获取资源 | 查询参数 / 路径 | 否 |
POST | 新增资源 | 请求体 | 否 |
PUT | 完整更新资源(全量替换) | 请求体 | ✅ 必须提供整个资源 |
PATCH | 部分更新资源 | 请求体 | ❌ 只需提供要修改的部分 |
DELETE | 删除资源 | 路径 | 否 |
关键差异 :
PUT
→ 整个资源被替换;
PATCH
→ 只更新提交的字段,其它保持不变。
1-2、PATCH 请求示例
假设后端有一个用户资源:
GET /users/123
返回的数据是:
{
"id": 123,
"name": "Alice",
"email": "alice@example.com",
"age": 25
}
1、用 PATCH 只修改邮箱
PATCH /users/123
Content-Type: application/json
{
"email": "alice_new@example.com"
}
- 服务器只会把
email
字段更新为新值,name
、age
等其他字段保持不变。
2、用 PUT 替换整个对象(对比用法)
PUT /users/123
Content-Type: application/json
{
"id": 123,
"name": "Alice",
"email": "alice_new@example.com",
"age": 25
}
- 这里必须把完整对象都发过去,否则服务器可能会把没传的字段覆盖成
null
或默认值。
1-3、PATCH 常用内容类型
Content-Type | 用途 | 示例 |
---|---|---|
application/json |
最常见,直接传要修改的字段 | { "email": "new@example.com" } |
application/merge-patch+json |
RFC 7396 标准的 JSON Merge Patch | { "email": "new@example.com" } |
application/json-patch+json |
RFC 6902 JSON Patch,描述操作列表 | [ { "op": "replace", "path": "/email", "value": "new@example.com" } ] |
大多数 REST API(如 GitHub API)使用 JSON Merge Patch (和普通 JSON 基本一样)。
如果是要更复杂的操作(比如增删数组元素),有些会用 JSON Patch 格式。
1-4、何时用 PATCH vs PUT
用 PATCH 的情况
-
只更新一个或几个字段;
-
服务器支持部分更新;
-
不想每次都传整个资源,节省流量。
不用 PUT 的情况
-
后端 API 不支持 PATCH;
-
需要替换整个对象;
-
资源结构简单,替换成本不高。
1-5、实际开发注意事项
-
幂等性:
-
PATCH
通常不保证幂等性(多次 PATCH 请求可能产生不同结果),但如果只做简单字段替换,也可以是幂等的。 -
PUT
一般是幂等的(多次发送相同 PUT 请求,结果一样)。
-
-
字段缺失:PATCH 只改传入的字段,没传的字段不会被删除;PUT 如果没传,可能会被覆盖为空。
-
接口设计:API 文档要明确写清楚支持哪些方法,否则客户端要先确认服务端是否允许 PATCH。
1-6、springboot+springmvc+mybatis 示例
在 Spring Boot + Spring MVC + MyBatis 里,PUT
/PATCH
的"全量替换 vs 局部更新"完全取决于你在 Controller/Service 层的写法 和 MyBatis 的 SQL。框架不会替你决定"未传字段是否清空"。
常见实现方式
1) 路由区分
java
@PutMapping("/users/{id}") // 语义:全量替换
@PatchMapping("/users/{id}") // 语义:部分更新
2) DTO 反序列化策略(Jackson)
-
PUT
:用 完整 DTO,配合 Bean 校验保证字段齐全javapublic record UserPutDTO( @NotNull String name, @NotNull String email, Integer age ) {}
若缺字段,直接 400;这样就符合"全量替换"的预期。
-
PATCH
:用 部分 DTO 或Map<String,Object>
javapublic record UserPatchDTO(String name, String email, Integer age) {}
仅对 非 null 字段做合并更新(见下文 Service 层)。
想要更精细地区分"字段缺失" vs "显式传 null",可用
JsonNode
或JsonMergePatch
/JsonPatch
。
3) Service 层合并逻辑(PATCH 核心)
java
public void patchUser(Long id, UserPatchDTO patch) {
User db = userMapper.selectById(id);
if (db == null) throw new NotFoundException();
if (patch.name() != null) db.setName(patch.name());
if (patch.email() != null) db.setEmail(patch.email());
if (patch.age() != null) db.setAge(patch.age());
userMapper.updateSelective(db); // 见下文:只更新非空列
}
4) MyBatis 动态 SQL("只更新非空列")
XML
<update id="updateSelective" parameterType="User">
UPDATE user
<set>
<if test="name != null">name = #{name},</if>
<if test="email != null">email = #{email},</if>
<if test="age != null">age = #{age},</if>
</set>
WHERE id = #{id}
</update>
-
这就是最常见的 "选择性更新(Selective Update)"。
-
注意:这样会导致无法把某字段更新为 NULL (因为
null
被过滤掉了)。若需要"把字段置空",要另做一条 SQL 或通过Map<String,Object>
显式标记"要置空的字段"。
5) PUT
的全量替换
做法 A(严格):要求客户端传完整 DTO(用 @NotNull
),然后:
-
先把数据库对象取出;
-
用 新的 DTO 全量覆盖(包括置空);
-
用 非选择性更新 执行:
XML<update id="updateFull" parameterType="User"> UPDATE user SET name = #{name}, email = #{email}, age = #{age} WHERE id = #{id} </update>
做法 B(宽松):仍用 updateSelective
,但这就把 PUT
弄成了"看起来像 PATCH",不推荐。
一句话指南
-
想要 PUT=全量替换 :用完整 DTO + 非选择性
updateFull
,缺字段就 400。 -
想要 PATCH=只改传入字段 :Service 里按非 null 合并,MyBatis 用
updateSelective
。 -
需要"把字段置空":不要用"忽略 null"的 update;要么提供专门接口,要么用 JSON Merge Patch/JSON Patch 明确表达。
小结
@PatchMapping
和 @PutMapping
在 Spring MVC 里主要是"语义和请求方法"上的区别,框架本身不会自动改变你的更新逻辑。
-
作用:
-
@PutMapping
只是告诉 Spring MVC:这个方法处理 HTTP PUT 请求。 -
@PatchMapping
只是告诉 Spring MVC:这个方法处理 HTTP PATCH 请求。
-
-
不会自动实现部分更新 :
Spring 不会因为用了
@PatchMapping
就帮你做"只改传入字段,缺失字段保持不变";也不会因为用了@PutMapping
就自动替换整条记录。
你必须自己在 Service/Mapper 层写全量更新或选择性更新的逻辑。 -
常用做法:
-
PUT
→ 在 Controller 接收完整 DTO,要求字段齐全,然后用非选择性 SQL(全部字段都更新)。 -
PATCH
→ 在 Controller 接收部分 DTO/Map,Service 层只对非 null 字段合并,再用 MyBatis 动态 SQL 只更新有值的列。
-
所以,@PatchMapping
/@PutMapping
只是声明接口方法的 HTTP 动词,实际更新行为(全量/局部、如何处理 null)完全由你在代码里决定。
二、回顾:@NotEmpty的使用
2-1、常用 Bean Validation 注解适用范围
注解 | 作用 | 常用于字段类型 |
---|---|---|
@NotNull |
不能为 null ,但允许空字符串/空集合 |
任何对象类型 |
@NotEmpty |
不能为 null 且不能为空(长度 > 0) |
String 、Collection 、Map 、Array |
@NotBlank |
不能为 null 且去掉空格后长度 > 0 |
只用于 String |
@Size |
限制集合、字符串、数组长度 | 字符串、集合、数组 |
⚠️
@NotEmpty
不能用在原始类型(如int
),因为它们不可能为null
;如果是字符串最好用
@NotBlank
(会自动去掉前后空格再判断)。
2-2、在 Controller 方法参数上使用
1、用在 请求体 DTO 字段上(最常见)
java
import jakarta.validation.constraints.NotEmpty;
public class UserPatchDTO {
@NotEmpty(message = "用户名不能为空")
private String name;
@NotEmpty(message = "邮箱不能为空")
private String email;
// getter & setter
}
java
@RestController
@RequestMapping("/users")
public class UserController {
@PostMapping
public ResponseEntity<Void> create(@Valid @RequestBody UserPatchDTO dto) {
// dto 中的字段会自动校验
return ResponseEntity.ok().build();
}
}
-
需要在 Controller 类或方法参数前加上
@Valid
(或@Validated
)。 -
如果不加
@Valid
,注解不会触发校验。
2、用在 方法参数(如请求参数) 上
java
@GetMapping("/search")
public ResponseEntity<?> search(@RequestParam @NotEmpty String keyword) {
// keyword 不能为空或null
return ResponseEntity.ok().build();
}
-
可以直接放在
@RequestParam
后面。 -
注意 :如果你用的是
@PathVariable
,也可以加@NotEmpty
,同样需要启用校验。
3、启用校验的关键
-
Spring Boot 3.x / 2.x 默认已集成
jakarta.validation
(Hibernate Validator),只要在pom.xml
有spring-boot-starter-validation
。 -
Controller 方法里要有
@Valid
或类上有@Validated
才会触发。
java
@RestController
@Validated // 类级别开启
public class UserController {
@GetMapping("/find")
public User find(@RequestParam @NotEmpty String id) { ... }
}
4、建议用法总结
场景 | 推荐注解 |
---|---|
Body 里对象字段 | @NotEmpty (集合/字符串) / @NotBlank (字符串) / @NotNull (一般对象) |
Query 参数 / Path 变量 | @NotEmpty 或 @NotBlank |
数字参数 | @NotNull + @Min / @Max |
2-3、@NotEmpty
vs @NotBlank
对比
注解 | 检查内容 | 允许值示例 |
---|---|---|
@NotEmpty |
不能为 null,并且长度必须 > 0 | "abc" ✅,"" ❌,null ❌," " ✅(因为长度是3) |
@NotBlank |
不能为 null,去掉前后空格后长度必须 > 0 | "abc" ✅,"" ❌,null ❌," " ❌ |
关键区别:
@NotBlank
会去掉空格再判断是否为空 ;@NotEmpty
不会去掉空格。
实际影响
-
如果你的字符串允许只包含空格(例如用户可以输入
" "
作为合法值),用@NotEmpty
就可以。 -
如果你不希望
" "
这种全空格的字符串通过验证,应该用@NotBlank
。
例如:
java
class UserDTO {
@NotEmpty
private String nickname;
@NotBlank
private String realName;
}
输入值 | @NotEmpty |
@NotBlank |
---|---|---|
null |
❌ | ❌ |
"" |
❌ | ❌ |
" " |
✅ | ❌ |
"abc" |
✅ | ✅ |
总结
-
@NotEmpty
= 不允许null
和空串,但允许全空格。 -
@NotBlank
= 不允许null
、空串和全空格字符串。 -
对大多数用户输入场景(如姓名、邮箱、标题),更推荐
@NotBlank
。 -
对集合、数组等用
@NotEmpty
更合适,因为@NotBlank
只支持字符串。
所以:如果你的需求是"必须有非空白字符",要用
@NotBlank
;如果只需要"不是 null、长度不为 0",那
@NotEmpty
就够了。
三、更新用户头像的场景
3-1、常见的头像存储架构
1、应用服务器只保存头像 URL 或文件路径
- 数据库表(如
user
)中一般只有一个字段,例如:
sql
ALTER TABLE user ADD COLUMN avatar_url VARCHAR(255);
- 保存的只是 云存储返回的访问地址 (如
https://cdn.xxx.com/avatar/123.jpg
)或相对路径(如/avatar/123.jpg
)。
2、真正的图片文件存放在对象存储或 CDN 上
常见方案:
-
阿里云 OSS、腾讯云 COS、AWS S3、七牛云 Kodo
-
静态资源服务器(Nginx + 本地磁盘)
-
搭配 CDN 加速(Cloudflare、阿里云 CDN)
应用服务器本身通常不直接存储和分发大文件,减少压力和带宽成本。
3-2、上传流程(常见两种)
方式 A:客户端直传云存储(推荐)
-
客户端(Web/APP)向后端请求上传凭证(如 S3 pre-signed URL、OSS STS token)。
-
客户端直接用该凭证将文件传到云存储。
-
云存储返回文件 URL,客户端再把这个 URL 提交给后端更新用户信息。
优点:后端不用承受文件流量和大带宽;更省成本,上传速度快。
方式 B:客户端先传到后端,再由后端转存云存储
-
客户端发一个
multipart/form-data
请求到后端。 -
后端接收文件后,用 SDK 上传到云存储。
-
得到文件 URL 后更新数据库。
优点 :后端可统一做图片处理(压缩、裁剪、水印、鉴黄等);
缺点:占用服务器带宽和磁盘 IO,成本较高。
3-3、为什么很少把图片直接存在数据库或本地磁盘
-
数据库 :可以用
BLOB
存,但会严重拖慢数据库性能和备份速度。 -
本地磁盘:如果是多机部署或容器环境(K8s、Docker),会难以共享和扩展,迁移也麻烦。
-
云存储:高可用、便宜、可用 CDN 加速、便于权限控制,是业界最佳实践。
3-4、总结
大多数系统:
数据库只存头像 URL;
实际图片存云对象存储(OSS/S3/COS)或 CDN 静态资源服务器;
上传时可以选择"直传云存储"或"后端中转"。
四、请求参数类型是querystring是什么意思
这是在说 HTTP 请求里参数的传递方式 。
如果一个接口的 请求参数类型是 query string
,意思是:
参数通过 URL 问号
?
后面的键值对传递,而不是放在请求体(body)里。
4-1、结构
https://api.example.com/users?name=alice&age=18
-
?
之后的部分就是 query string(查询字符串)。 -
每个参数是
key=value
的形式,用&
连接多个。 -
上例中:
-
name=alice
-
age=18
-
4-2、使用方式
1. GET 请求
最常见:GET 请求参数一般用 query string
GET /search?keyword=apple&page=2 HTTP/1.1
Host: example.com
2. POST 请求也可以带 query string
虽然 POST 的主要数据放在 body,但 URL 也可以附带 query string:
POST /order/create?source=app HTTP/1.1
Host: example.com
Content-Type: application/json
{ "itemId":123, "count":2 }
4-3、对比其他参数位置
参数位置 | 形式示例 | 特点 |
---|---|---|
Query String | /users?name=alice&age=18 |
最常见,适合 GET 查询条件;暴露在 URL |
Path Variable | /users/123 |
路径中直接表示资源 ID |
Request Body | POST /users + JSON |
用于创建/修改资源,数据放 body |
Header | Authorization: Bearer xxx |
元信息、token、安全数据 |
总结
-
Query string = URL 中 ? 后面的键值对参数。
-
常用在 GET 请求或需要在 URL 上直观看到的参数。
-
与 body 不同:query string 总是明文出现在 URL 上,适合搜索条件、分页参数等。
如果接口文档写"请求参数类型:querystring",就是让你把参数拼在 URL 后面。
五、controller层接受json格式的传参的参数格式可以有哪些
在 Spring Boot + Spring MVC 的 Controller 里,
"接收 JSON 格式参数"最常见的几种写法可以分成两类:
- 接收整个 JSON 对象
- 接收 JSON 中的局部字段。
5-1、接收整个 JSON 对象
(1)用 JavaBean / DTO
最常用、推荐方式
java
@Data
public class UserDTO {
private String name;
private Integer age;
private String avatarUrl;
}
java
@RestController
@RequestMapping("/users")
public class UserController {
@PostMapping
public String createUser(@RequestBody UserDTO user) {
return "name=" + user.getName() + ", age=" + user.getAge();
}
}
-
前端发送:
{
"name": "Alice",
"age": 18,
"avatarUrl": "https://xxx.com/a.jpg"
} -
Spring 会用 Jackson 自动把 JSON 映射到
UserDTO
。
(2)用 Map
灵活,但缺少类型检查,适合动态 JSON。
java
@PostMapping("/map")
public String saveUser(@RequestBody Map<String, Object> json) {
return "name=" + json.get("name");
}
请求:
{ "name": "Bob", "age": 22 }
(3)用 JsonNode
(Jackson Tree)
适合要处理不固定结构或只取部分字段时。
java
@PostMapping("/tree")
public String saveUser(@RequestBody com.fasterxml.jackson.databind.JsonNode node) {
return "name=" + node.get("name").asText();
}
5-2、接收局部字段(JSON 里的某些值)
(1)直接用多个基本参数(不常用)
java
@PostMapping("/partial")
public String partial(@RequestBody Map<String, Object> json) {
String name = (String) json.get("name");
Integer age = (Integer) json.get("age");
return name + "-" + age;
}
⚠️ 这种方式要自己从 Map 里取值,类型安全差。
spring validation在此时是失效的!需要手动校验参数。
(2)用 @RequestParam
(不适合 JSON Body)
- 注意:
@RequestParam
用于 query string 或表单,不是 JSON。
如果请求体是 JSON,不能用@RequestParam
去直接接字段。
java
// ❌ 错误示例
@PostMapping("/wrong")
public String wrong(@RequestParam String name) { ... }
5-3、高级用法
1、接收并校验
java
@PostMapping
public String create(@Valid @RequestBody UserDTO user) {
// @NotBlank、@Size 等注解会自动触发
return user.getName();
}
2、接收 List / 嵌套结构
java
@PostMapping("/batch")
public void batch(@RequestBody List<UserDTO> users) {
// 接收 JSON 数组
}
3、接收 Map<String, List> 等复杂结构
java
@PostMapping("/complex")
public void complex(@RequestBody Map<String, List<String>> payload) { ... }
总结
用法 | 适用场景 | 推荐度 |
---|---|---|
@RequestBody + DTO |
结构固定、可做校验 | ⭐⭐⭐⭐✅ |
@RequestBody + Map |
动态字段、简单快速 | ⭐⭐⭐ |
@RequestBody + JsonNode |
不确定结构、手动解析 | ⭐⭐ |
List<DTO> |
批量对象 | ⭐⭐⭐⭐ |
混合:DTO + @Valid | 强类型 + 校验 | ⭐⭐⭐⭐⭐ |
一般来说:
固定接口 → DTO +
@RequestBody
+ 校验注解;动态 JSON → Map 或 JsonNode;
数组 →
List<DTO>
;不要用
@RequestParam
去解析 JSON。