javaSpring
邵预鸿 Lv5

一、SpringBoot 请求与响应

1.1 环境与依赖

pom.xml 关键依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<dependencies>
<!-- Web:内置 Tomcat + SpringMVC + Jackson(JSON 序列化) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- 参数校验:@Valid / @NotBlank / @NotNull 等 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

<!-- Lombok:省掉 getter/setter/构造器 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>

版本注意:Spring Boot 3.x 校验注解包是 jakarta.validation.*,2.x 是 javax.validation.*,其余用法完全一致。

推荐项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
src/main/java/com/example/demo/
├── DemoApplication.java # 启动类(必须在最外层包)
├── controller/
│ └── UserController.java # 接收请求、返回响应
├── entity/
│ └── User.java # 数据库实体
├── dto/
│ ├── UserQueryDTO.java # 接收请求参数的对象
│ └── UserCreateDTO.java
├── vo/
│ └── UserVO.java # 返回给前端的对象
└── common/
├── R.java # 统一响应体
├── GlobalExceptionHandler.java # 全局异常处理
└── LoginInterceptor.java # 拦截器

启动类

1
2
3
4
5
6
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}

坑:启动类必须放在所有业务包的上层目录(如 com.example.demo),否则 controller、service 扫描不到,接口 404。


1.2 控制器基础

最小可用控制器

1
2
3
4
5
6
7
8
9
10
@RestController                 // = @Controller + @ResponseBody(返回值直接写 JSON)
@RequestMapping("/api/users") // 类级别公共路径
public class UserController {

// GET http://localhost:8080/api/users/1
@GetMapping("/{id}")
public UserVO getById(@PathVariable Long id) {
return new UserVO(id, "张三", 25);
}
}

请求映射注解对照表

注解 等价写法 用途
@GetMapping @RequestMapping(method = GET) 查询
@PostMapping @RequestMapping(method = POST) 新增
@PutMapping @RequestMapping(method = PUT) 全量更新
@PatchMapping @RequestMapping(method = PATCH) 局部更新
@DeleteMapping @RequestMapping(method = DELETE) 删除
@RequestMapping 可指定 path / method / params / headers 需要精细化匹配时使用

精细化映射(限定参数/请求头)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RestController
@RequestMapping("/api/orders")
public class OrderController {

// 仅当存在 type=app 参数时才匹配:GET /api/orders/search?type=app
@GetMapping(value = "/search", params = "type=app")
public List<OrderVO> searchFromApp(@RequestParam String keyword) {
return orderService.search(keyword);
}

// 仅当 Content-Type=application/json 时才匹配
@PostMapping(value = "/create", consumes = "application/json")
public R<Long> create(@RequestBody OrderCreateDTO dto) {
return R.ok(orderService.create(dto));
}

// 指定响应类型(解决部分老浏览器 JSON 被当文件下载)
@GetMapping(value = "/export", produces = "application/json;charset=UTF-8")
public R<List<OrderVO>> export() {
return R.ok(orderService.listAll());
}
}

1.3 HttpServletRequest / HttpServletResponse(前置了解)

一句话:SpringMVC 里所有参数注解(@PathVariable、@RequestParam、@RequestBody…)都是 Tomcat 把 HTTP 报文解析成 HttpServletRequest 之后,从里面取值再填到你的方法参数上。看懂原生对象,后面 9 种参数接收方式就不用死记硬背。

1.3.1 一次 HTTP 请求的完整链路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
浏览器发起 HTTP 请求
↓
Tomcat 解析报文 → 封装成 HttpServletRequest + HttpServletResponse
↓
Filter 过滤器链(编码 / 跨域 / 全局日志)
↓
DispatcherServlet(前端控制器,统一入口)
↓
HandlerMapping → 根据 URL 找到对应的 Controller 方法
↓
HandlerAdapter → 参数解析器把 Request 里的数据绑定到方法参数上
↓
Controller 方法执行(业务处理)
↓
返回值 → HttpMessageConverter(Jackson)转成 JSON
↓
写回 HttpServletResponse → Tomcat 组装 HTTP 响应 → 浏览器

核心结论:Controller 方法的每个参数,都是由 HandlerMethodArgumentResolver(参数解析器)从 HttpServletRequest 里解析出来并做类型转换的。注解的作用只是告诉它——去哪儿取、怎么转、要不要必填。

1.3.2 HttpServletRequest 常用 API

方法 作用 示例返回值
getMethod() 请求方式 GET / POST
getRequestURI() 请求路径(不含域名) /api/users/1
getRequestURL() 完整 URL http://localhost:8080/api/users/1
getQueryString() URL 上的查询串 page=1&size=10
getParameter(name) 取请求参数(URL 或表单) 1
getParameterValues(name) 取同名多值参数 ["1","2","3"]
getParameterMap() 所有参数 {page=["1"], size=["10"]}
getHeader(name) 取请求头 Bearer xxx
getHeaderNames() 所有请求头名 枚举
getCookies() 取所有 Cookie Cookie[]
getInputStream() / getReader() 读请求体(JSON 原始报文) 只能读一次
getContentType() 请求体类型 application/json
getRemoteAddr() 客户端 IP 127.0.0.1
getSession() 获取 Session HttpSession
getAttribute(name) 取请求域属性(配合拦截器传值) setAttribute 存进去的东西
getServletContext() 应用上下文 ServletContext

1.3.3 HttpServletResponse 常用 API

方法 作用
setStatus(int) 设置状态码(如 401/404)
setContentType(String) 设置响应类型
setCharacterEncoding(String) 设置编码
setHeader(name, value) 设置响应头
addHeader(name, value) 追加响应头(可重复)
getWriter() 字符输出流(写文本/JSON)
getOutputStream() 字节输出流(写文件/图片)
sendRedirect(url) 重定向
addCookie(Cookie) 写 Cookie 给浏览器
sendError(404) 直接返回错误页

1.3.4 不用任何注解,手写接口

理解了原生对象,下面这段代码就是 SpringMVC 帮你做的事的”展开版”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@RestController
@RequestMapping("/api/raw")
public class RawController {

/**
* GET /api/raw/user?id=1
* 完全不用 @RequestParam,自己从 request 里取
*/
@GetMapping("/user")
public void getUser(HttpServletRequest request,
HttpServletResponse response) throws IOException {

// 1. 取参数(Spring 的 @RequestParam 底层就是它)
String idStr = request.getParameter("id");

// 2. 手动做类型转换(Spring 的 Converter 帮你做了)
Long id = Long.valueOf(idStr);

// 3. 业务处理
UserVO user = userService.getById(id);

// 4. 手动设置响应(Spring 的 @ResponseBody 帮你做了)
response.setStatus(HttpServletResponse.SC_OK); // 200
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(JSON.toJSONString(R.ok(user)));
}

/**
* POST /api/raw/create
* Body: {"username":"zhangsan","age":25}
* 手动读请求体,等价于 @RequestBody
*/
@PostMapping("/create")
public void createUser(HttpServletRequest request,
HttpServletResponse response) throws IOException {

// 读请求体原始 JSON(注意:只能读一次!@RequestBody 读完后这里再读就是空)
String body = request.getReader().lines()
.collect(Collectors.joining(System.lineSeparator()));

UserCreateDTO dto = JSON.parseObject(body, UserCreateDTO.class);
Long id = userService.create(dto);

response.setStatus(HttpServletResponse.SC_CREATED); // 201
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(JSON.toJSONString(R.ok(id)));
}
}

1.3.5 注解 ↔ 原生 API 对照表

Spring 注解 等价于原生写法
@RequestParam("id") Long id request.getParameter("id") + 类型转换
@PathVariable("id") Long id 从 request.getRequestURI() 里截取路径段
@RequestBody UserDTO dto request.getReader() 读流 + Jackson 反序列化
@RequestHeader("Authorization") request.getHeader("Authorization")
@CookieValue("JSESSIONID") 遍历 request.getCookies() 找同名 Cookie
UserQueryDTO query(无注解) 遍历 request.getParameterMap() 按属性名反射赋值
@RequestParam List<Long> ids request.getParameterValues("ids") + 逐个转换
@RequestParam MultipartFile file request.getParts() 取上传的二进制部分
@ResponseBody response.getWriter().write(JSON.toJSONString(obj))
@Valid 手动 if (dto.getUsername() == null) { ... }

1.3.6 什么时候必须用原生对象

日常 CRUD 优先用注解,原生对象只在以下场景兜底:

  1. 文件下载 / 验证码 / 图片:需要直接操作 response.getOutputStream()
  2. 写 Cookie:response.addCookie(new Cookie("token", token))
  3. 拦截器与 Controller 传值:request.setAttribute("userId", 1L) → Controller 里 request.getAttribute("userId")
  4. 获取真实 IP:经过 Nginx 代理时要读 X-Forwarded-For 头
  5. 接口验签:需要读取原始请求体做签名比对(此时不能用 @RequestBody,否则流已被消费)
  6. 导出 Excel / CSV:直接写输出流,避免大文件撑爆内存

1.3.7 两个必知的坑

1
2
3
4
5
6
7
8
// 坑 1:请求体只能读一次
// @RequestBody 读过之后,request.getReader() 再读就是空字符串
// 解决:用 ContentCachingRequestWrapper 包装 request(配合 Filter 使用)

// 坑 2:getWriter() 和 getOutputStream() 二选一,不能同时用
// IllegalStateException: getWriter() has already been called
response.getWriter().write("xxx");
// response.getOutputStream().write(bytes); // 报错!

1.4 接收请求参数(9 种常见场景)

建议先看 1.3 HttpServletRequest(前置了解)。下面这些注解本质上都是从原生 Request 里取值,理解底层后下面的写法就不需要死记。

1.4.1 @PathVariable 路径参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// GET /api/users/100
@GetMapping("/{id}")
public R<UserVO> getById(@PathVariable("id") Long id) {
return R.ok(userService.getById(id));
}

// 参数名与占位符一致时,可省略 ("id")
@GetMapping("/{id}")
public R<UserVO> getById(@PathVariable Long id) { ... }

// 多个路径参数:GET /api/users/100/orders/2024
@GetMapping("/{userId}/orders/{year}")
public R<List<OrderVO>> userOrders(@PathVariable Long userId,
@PathVariable Integer year) {
return R.ok(orderService.listByUserAndYear(userId, year));
}

// Map 一次性接全部:/api/dept/10/user/20
@GetMapping("/dept/{deptId}/user/{userId}")
public R<Map<String, String>> test(@PathVariable Map<String, String> pathVars) {
// pathVars = {deptId=10, userId=20}
return R.ok(pathVars);
}

1.4.2 @RequestParam 查询参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// GET /api/users?page=1&size=10
@GetMapping("url")
public R<List<UserVO>> list(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size) {
return R.ok(userService.page(page, size));
}

// 参数封装为对象
// User是引入的封装对象类 user->接收的参数名
// 如 ?name=xiaoming&age=22
@GetMapping("url")
public void list(User user) {
/**
user: {name: xiaoming, age: 22}
**/
}

// 默认为普通数组, 当参数需要转为复杂对象时如List,使用注释
// User是引入的封装对象类 user->接收的参数名 见-----------------> 1.4.6
// 如 ?name=xiaoming&name=xiaohong
@GetMapping("url")
public void list(@RequestParam List<String> list) {

}


// 当前端传的name , 后端要改为userName时?
// User是引入的封装对象类 user->接收的参数名
@GetMapping("url")
public void list(@RequestParam(name="name") String userName) {

}

// required = false:参数可选,不传为 null
// GET /api/users/search?keyword=张
@GetMapping("/search")
public R<List<UserVO>> search(@RequestParam(required = false) String keyword) {
return R.ok(userService.search(keyword));
}


// 时间类, 前端传yyyy-mm-dd HH:mm:ss 见----------------> 1.4.7

@GetMapping("url")
public void list(@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") LocalDataTime ldt) {
ldt->就是LocalDataTime的日期类型
}


// 前端post请求传json类 见 --------> 1.4.3 @RequestBody JSON 请求体
@GetMapping("url")
public void list(@RequestBody User user) {

}


// 路径参数
// 如/set/xiaoming
@GetMapping("/set/{id}/{name}")
public void list(@PathVarivable Integer id, @PathVariable ) {

}

@RequestParam 三个属性:

属性 说明
value/name 绑定的请求参数名(与变量名一致时可省略)
required 是否必传,默认 true,不传抛 400
defaultValue 默认值,设置后 required 自动失效为 false

1.4.3 @RequestBody JSON 请求体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Data
public class UserCreateDTO {
@NotBlank(message = "用户名不能为空")
private String username;

@NotBlank(message = "密码不能为空")
@Size(min = 6, max = 20, message = "密码长度 6-20 位")
private String password;

@Email(message = "邮箱格式不正确")
private String email;

private Integer age;
}

// POST /api/users
// Content-Type: application/json
// {"username":"zhangsan","password":"123456","email":"zs@test.com","age":25}
@PostMapping
public R<Long> create(@RequestBody UserCreateDTO dto) {
return R.ok(userService.create(dto));
}

规则:

  1. 一个方法只能有一个 @RequestBody
  2. 前端必须带 Content-Type: application/json,否则报 415
  3. 接收数组:@RequestBody List<Long> ids
1
2
3
4
5
6
// POST /api/users/batch-delete   Body: [1,2,3]
@PostMapping("/batch-delete")
public R<Void> batchDelete(@RequestBody List<Long> ids) {
userService.removeByIds(ids);
return R.ok();
}

1.4.4 无注解:表单 / 对象自动绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Data
public class UserQueryDTO {
private String keyword;
private Integer status;
private Integer page = 1;
private Integer size = 10;
}

// GET /api/users/list?keyword=张&status=1&page=1&size=10
// 或 application/x-www-form-urlencoded 表单提交,效果一样
@GetMapping("/list")
public R<List<UserVO>> list(UserQueryDTO query) {
return R.ok(userService.list(query));
}

Spring 会把请求参数按属性名自动塞进对象。这是最省事的写法,查询类接口优先用它。

1.4.5 @RequestHeader / @CookieValue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 取请求头
@GetMapping("/header")
public R<String> header(@RequestHeader("Authorization") String token) {
return R.ok(token);
}

// 有默认值,不传不报错
@GetMapping("/ua")
public R<String> ua(@RequestHeader(value = "User-Agent", defaultValue = "unknown") String ua) {
return R.ok(ua);
}

// 一次性拿全部请求头
@GetMapping("/all-headers")
public R<Map<String, String>> allHeaders(@RequestHeader Map<String, String> headers) {
return R.ok(headers);
}

// 取 Cookie
@GetMapping("/cookie")
public R<String> cookie(@CookieValue(value = "JSESSIONID", required = false) String jsessionId) {
return R.ok(jsessionId);
}

1.4.6 数组 / 集合参数

1
2
3
4
5
6
7
8
9
10
11
// GET /api/users/ids?ids=1&ids=2&ids=3
@GetMapping("/ids")
public R<List<UserVO>> byIds(@RequestParam List<Long> ids) {
return R.ok(userService.listByIds(ids));
}

// GET /api/users/ids2?ids=1,2,3 (逗号分隔,自动转数组)
@GetMapping("/ids2")
public R<List<UserVO>> byIds2(@RequestParam Long[] ids) {
return R.ok(userService.listByIds(Arrays.asList(ids)));
}

1.4.7 日期时间参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Data
public class ReportQueryDTO {
// 前端传 "2026-10-15" 才能绑定成功,否则 400
@DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate startDate;

// 前端传 "2026-10-15 14:30:00"
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime endDate;
}

@GetMapping("/report")
public R<List<ReportVO>> report(ReportQueryDTO query) {
return R.ok(reportService.list(query));
}

全局配置(免得每个字段都写注解):

1
2
3
4
5
spring:
mvc:
format:
date: yyyy-MM-dd
date-time: yyyy-MM-dd HH:mm:ss

1.4.8 文件上传 MultipartFile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@RestController
@RequestMapping("/api/files")
public class FileController {

// 单文件:POST /api/files/upload (form-data: key=file)
@PostMapping("/upload")
public R<String> upload(@RequestParam("file") MultipartFile file) throws IOException {
if (file.isEmpty()) {
return R.fail("文件为空");
}

String originalName = file.getOriginalFilename(); // a.jpg
long size = file.getSize(); // 字节数
String ext = originalName.substring(originalName.lastIndexOf("."));
String newName = UUID.randomUUID() + ext;

Path target = Paths.get("D:/uploads/" + newName);
Files.createDirectories(target.getParent());
file.transferTo(target); // 保存

return R.ok("/files/" + newName);
}

// 多文件 + 附带普通字段
@PostMapping("/upload-batch")
public R<List<String>> uploadBatch(@RequestParam("files") List<MultipartFile> files,
@RequestParam String bizType) throws IOException {
List<String> urls = new ArrayList<>();
for (MultipartFile f : files) {
String url = storageService.save(f, bizType);
urls.add(url);
}
return R.ok(urls);
}
}

上传大小限制:

1
2
3
4
5
spring:
servlet:
multipart:
max-file-size: 10MB # 单个文件
max-request-size: 100MB # 单次请求总量

1.4.9 原生 HttpServletRequest / HttpServletResponse

完整 API 说明见 1.3,这里只列最常用的取法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@GetMapping("/raw")
public void raw(HttpServletRequest request, HttpServletResponse response) throws IOException {
String method = request.getMethod(); // GET
String uri = request.getRequestURI(); // /api/users/raw
String ip = request.getRemoteAddr(); // 客户端 IP
String ua = request.getHeader("User-Agent");
String token = request.getParameter("token");

response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":0,\"msg\":\"ok\"}");
}

// 取真实 IP(经过 Nginx 代理时)
public static String getIpAddr(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("X-Real-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
// X-Forwarded-For 可能是 "client, proxy1, proxy2"
return ip != null && ip.contains(",") ? ip.split(",")[0].trim() : ip;
}

参数接收方式速查表

场景 写法 前端传参方式
/users/1 @PathVariable Long id URL 路径
?page=1 @RequestParam Integer page URL 查询串 / 表单
{"name":"x"} @RequestBody XxxDTO dto JSON 请求体
?a=1&b=2 多参数 XxxDTO dto(无注解) 查询串 / 表单自动绑定
请求头 @RequestHeader("Authorization") Header
Cookie @CookieValue("JSESSIONID") Cookie
逗号分隔 / 重复 key @RequestParam List<Long> ids ids=1,2,3
日期 @DateTimeFormat(pattern=...) 2026-10-15
文件 @RequestParam MultipartFile file multipart/form-data
拿原生对象 HttpServletRequest request —

1.5 参数校验 @Valid

常用校验注解

注解 作用 适用类型
@NotNull 不能为 null 任意
@NotEmpty 不能为 null 且长度 > 0 字符串/集合
@NotBlank 不能为 null 且去空格后长度 > 0 字符串
@Size 长度范围 字符串/集合
@Min / @Max 数值范围 数字
@Pattern 正则 字符串
@Email 邮箱格式 字符串
@Past / @Future 时间必须过去 / 将来 日期
@Valid 嵌套对象级联校验 对象

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
@Data
public class UserCreateDTO {

@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 20, message = "用户名长度 2-20")
private String username;

@NotBlank(message = "密码不能为空")
@Pattern(regexp = "^(?=.*[A-Za-z])(?=.*\\d)[A-Za-z\\d]{6,20}$",
message = "密码需 6-20 位且包含字母和数字")
private String password;

@NotNull(message = "年龄不能为空")
@Min(value = 1, message = "年龄最小 1 岁")
@Max(value = 150, message = "年龄最大 150 岁")
private Integer age;

@NotEmpty(message = "角色不能为空")
private List<Long> roleIds;

// 嵌套对象:必须加 @Valid 才会级联校验
@Valid
@NotNull(message = "地址不能为空")
private AddressDTO address;
}

@Data
public class AddressDTO {
@NotBlank(message = "城市不能为空")
private String city;
}

// Controller:@RequestBody 前加 @Valid 或 @Validated
@PostMapping
public R<Long> create(@Valid @RequestBody UserCreateDTO dto) {
return R.ok(userService.create(dto));
}

// 非 JSON 的对象绑定用 @Validated
@GetMapping("/list")
public R<List<UserVO>> list(@Validated UserQueryDTO query) { ... }

分组校验(新增/编辑用同一 DTO)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ValidationGroups {
public interface Create {}
public interface Update {}
}

@Data
public class UserDTO {
@Null(groups = Create.class, message = "新增时 id 必须为空")
@NotNull(groups = Update.class, message = "编辑时 id 不能为空")
private Long id;

@NotBlank(groups = {Create.class, Update.class}, message = "用户名不能为空")
private String username;
}

@PostMapping
public R<Long> create(@Validated(ValidationGroups.Create.class) @RequestBody UserDTO dto) { ... }

@PutMapping
public R<Void> update(@Validated(ValidationGroups.Update.class) @RequestBody UserDTO dto) { ... }

1.6 响应数据

1.6.1 直接返回对象(自动转 JSON)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Data
@AllArgsConstructor
public class UserVO {
private Long id;
private String name;
private Integer age;

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime createTime;
}

@GetMapping("/{id}")
public UserVO getById(@PathVariable Long id) {
return userService.getById(id);
}
// 响应:{"id":1,"name":"张三","age":25,"createTime":"2026-10-15 10:30:00"}

1.6.2 ResponseEntity(控制状态码和响应头)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 自定义状态码
@PostMapping
public ResponseEntity<UserVO> create(@RequestBody UserCreateDTO dto) {
UserVO vo = userService.create(dto);
return ResponseEntity.status(HttpStatus.CREATED).body(vo); // 201
}

// 自定义响应头 + 下载文件
@GetMapping("/download/{fileName}")
public ResponseEntity<byte[]> download(@PathVariable String fileName) throws IOException {
Path path = Paths.get("D:/uploads/" + fileName);
byte[] data = Files.readAllBytes(path);

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
headers.setContentDispositionFormData("attachment",
URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()));

return new ResponseEntity<>(data, headers, HttpStatus.OK);
}

// 无响应体
@DeleteMapping("/{id}")
public ResponseEntity<Void> delete(@PathVariable Long id) {
userService.removeById(id);
return ResponseEntity.noContent().build(); // 204
}

1.6.3 统一响应体(项目必配)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Data
@NoArgsConstructor
@AllArgsConstructor
public class R<T> implements Serializable {
private Integer code; // 0=成功,其他=失败
private String message;
private T data;
private Long timestamp = System.currentTimeMillis();

public static <T> R<T> ok() {
return new R<>(0, "success", null, System.currentTimeMillis());
}

public static <T> R<T> ok(T data) {
return new R<>(0, "success", data, System.currentTimeMillis());
}

public static <T> R<T> fail(String message) {
return new R<>(500, message, null, System.currentTimeMillis());
}

public static <T> R<T> fail(Integer code, String message) {
return new R<>(code, message, null, System.currentTimeMillis());
}
}

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@RestController
@RequestMapping("/api/users")
public class UserController {

@GetMapping("/{id}")
public R<UserVO> getById(@PathVariable Long id) {
return R.ok(userService.getById(id));
}

@GetMapping("/page")
public R<PageResult<UserVO>> page(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size) {
return R.ok(userService.page(page, size));
}

@PostMapping
public R<Long> create(@Valid @RequestBody UserCreateDTO dto) {
return R.ok(userService.create(dto));
}

@DeleteMapping("/{id}")
public R<Void> delete(@PathVariable Long id) {
userService.removeById(id);
return R.ok();
}
}

前端拿到的统一结构:

1
2
3
4
5
6
{
"code": 0,
"message": "success",
"data": { "id": 1, "name": "张三", "age": 25 },
"timestamp": 1760600000000
}

1.6.4 响应包装的偷懒方案:ResponseBodyAdvice

不想每个方法都手写 R.ok(...),用这个自动包装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@RestControllerAdvice(basePackages = "com.example.demo.controller")
public class GlobalResponseAdvice implements ResponseBodyAdvice<Object> {

@Override
public boolean supports(MethodParameter returnType, Class<?> converterType) {
// 已经是 R 类型、或返回 void/String(如文件下载),不重复包装
return !returnType.getParameterType().isAssignableFrom(R.class)
&& !returnType.getParameterType().isAssignableFrom(ResponseEntity.class);
}

@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType,
MediaType selectedContentType, Class<?> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
if (body instanceof String) {
// String 类型要用 StringHttpMessageConverter,必须手动转 JSON
try {
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
return new ObjectMapper().writeValueAsString(R.ok(body));
} catch (JsonProcessingException e) {
return R.fail(e.getMessage());
}
}
return R.ok(body);
}
}

这样 Controller 可以直接返回业务对象:

1
2
3
4
@GetMapping("/{id}")
public UserVO getById(@PathVariable Long id) {
return userService.getById(id); // 自动包装成 {"code":0,"data":{...}}
}

1.7 全局异常处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
@RestControllerAdvice   // = @ControllerAdvice + @ResponseBody
@Slf4j
public class GlobalExceptionHandler {

/** 参数校验失败(@Valid) */
@ExceptionHandler(MethodArgumentNotValidException.class)
public R<Void> handleValidException(MethodArgumentNotValidException e) {
String msg = e.getBindingResult().getFieldErrors().stream()
.map(f -> f.getField() + ": " + f.getDefaultMessage())
.collect(Collectors.joining(", "));
log.warn("参数校验失败: {}", msg);
return R.fail(400, msg);
}

/** 单参数校验失败(@Validated 直接标在参数上) */
@ExceptionHandler(ConstraintViolationException.class)
public R<Void> handleConstraintViolation(ConstraintViolationException e) {
String msg = e.getConstraintViolations().stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.joining(", "));
return R.fail(400, msg);
}

/** 参数类型错误:/users/abc 传给 Long id */
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public R<Void> handleTypeMismatch(MethodArgumentTypeMismatchException e) {
return R.fail(400, "参数 " + e.getName() + " 类型错误");
}

/** 请求方式不支持 */
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public R<Void> handleMethodNotSupported(HttpRequestMethodNotSupportedException e) {
return R.fail(405, "不支持的请求方法: " + e.getMethod());
}

/** JSON 格式错误 */
@ExceptionHandler(HttpMessageNotReadableException.class)
public R<Void> handleNotReadable(HttpMessageNotReadableException e) {
return R.fail(400, "请求体 JSON 格式错误");
}

/** 缺少必填参数 */
@ExceptionHandler(MissingServletRequestParameterException.class)
public R<Void> handleMissingParam(MissingServletRequestParameterException e) {
return R.fail(400, "缺少必填参数: " + e.getParameterName());
}

/** 文件上传超限 */
@ExceptionHandler(MaxUploadSizeExceededException.class)
public R<Void> handleMaxUpload(MaxUploadSizeExceededException e) {
return R.fail(400, "文件大小超出限制");
}

/** 业务异常 */
@ExceptionHandler(BizException.class)
public R<Void> handleBizException(BizException e) {
log.error("业务异常: ", e);
return R.fail(e.getCode(), e.getMessage());
}

/** 兜底 */
@ExceptionHandler(Exception.class)
public R<Void> handleException(Exception e) {
log.error("系统异常: ", e);
return R.fail(500, "服务器开小差了,请稍后重试");
}
}

自定义业务异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Data
@EqualsAndHashCode(callSuper = true)
public class BizException extends RuntimeException {
private Integer code;

public BizException(String message) {
super(message);
this.code = 500;
}

public BizException(Integer code, String message) {
super(message);
this.code = code;
}
}

// 业务代码里直接抛
if (user == null) {
throw new BizException(404, "用户不存在");
}

1.8 拦截器与过滤器

拦截器 HandlerInterceptor(最常用:登录校验)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Component
public class LoginInterceptor implements HandlerInterceptor {

/** Controller 方法执行前 */
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("Authorization");
if (token == null || !TokenUtil.verify(token)) {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("{\"code\":401,\"message\":\"未登录\"}");
return false; // false = 中断请求
}
// 把用户信息放进 request,Controller 里可取
request.setAttribute("userId", TokenUtil.getUserId(token));
return true;
}

/** Controller 方法执行后、视图渲染前 */
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response,
Object handler, ModelAndView modelAndView) {
}

/** 请求完成后(一定会执行,可做资源清理、日志) */
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
}
}

注册拦截器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration
public class WebConfig implements WebMvcConfigurer {

@Autowired
private LoginInterceptor loginInterceptor;

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/api/**") // 拦截
.excludePathPatterns( // 放行
"/api/auth/login",
"/api/auth/register",
"/api/files/**",
"/error"
);
}
}

过滤器 Filter(更底层,处理编码、跨域日志)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Component
@Order(1) // 数字越小越先执行
public class LogFilter implements Filter {

@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
long start = System.currentTimeMillis();
try {
chain.doFilter(req, res); // 放行
} finally {
long cost = System.currentTimeMillis() - start;
System.out.printf("%s %s 耗时 %dms%n",
request.getMethod(), request.getRequestURI(), cost);
}
}
}

拦截器 vs 过滤器

对比项 Filter Interceptor
归属 Servlet 规范 SpringMVC
生效范围 所有请求(含静态资源) 只拦截 Controller
拿到的对象 原生 Request/Response 还能拿到 Handler 方法信息
依赖注入 支持(注册为 Bean) 支持
典型用途 编码、跨域、全局日志、XSS 过滤 登录鉴权、权限、接口耗时统计

1.9 跨域 CORS

方案一:配置类(推荐)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class CorsConfig implements WebMvcConfigurer {

@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*") // Spring Boot 2.4+ 支持通配符+凭证
.allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS")
.allowedHeaders("*")
.exposedHeaders("Authorization", "Content-Disposition") // 前端能读到的响应头
.allowCredentials(true)
.maxAge(3600); // 预检请求缓存 1 小时
}
}

方案二:单接口注解

1
2
3
@CrossOrigin(origins = "http://localhost:5173", allowCredentials = "true")
@GetMapping("/{id}")
public R<UserVO> getById(@PathVariable Long id) { ... }

坑:allowCredentials(true) 时 allowedOrigins **不能写 "*"**,要用 allowedOriginPatterns("*")。


1.10 Jackson 常用注解(控制 JSON 字段)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Data
public class UserVO {

// 重命名:Java 是 userName,JSON 是 user_name
@JsonProperty("user_name")
private String userName;

// 不返回给前端(密码、盐值等)
@JsonIgnore
private String password;

// null 不返回(也可全局配置)
@JsonInclude(JsonInclude.Include.NON_NULL)
private String remark;

// 日期格式化
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime createTime;

// 数字保留 2 位:1234.567 → "1234.57"
@JsonSerialize(using = BigDecimalSerializer.class)
private BigDecimal amount;

// 枚举返回描述而非名称:{"code":1,"desc":"启用"}
@JsonFormat(shape = JsonFormat.Shape.OBJECT)
private StatusEnum status;
}

全局配置:

1
2
3
4
5
6
7
spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
default-property-inclusion: non_null # 全局 null 字段不返回
deserialization:
fail-on-unknown-properties: false # JSON 有多余字段时不报错

1.11 常见坑与排查

现象 原因 解决
接口 404 ① 启动类包路径太深 ② 路径拼写错 ③ 没加 @RestController 启动类放最外层包;检查 @RequestMapping 拼接结果
@RequestBody 报 415 前端没带 Content-Type: application/json axios 设置 headers: {'Content-Type':'application/json'}
日期参数 400 字符串无法转 LocalDate 加 @DateTimeFormat 或全局 spring.mvc.format
Long 传到前端精度丢失 JS 数字最大安全值 2^53-1,id 太长 全局把 Long 序列化为 String,或字段改 String
中文乱码 响应未指定 UTF-8 produces = "application/json;charset=UTF-8"
参数接收到 null ① 名字不一致 ② 用了 @RequestBody 但传的是表单 核对参数名;表单提交去掉 @RequestBody
OPTIONS 预检失败 跨域未放行 OPTIONS CORS 配置里加上 OPTIONS 方法
@Valid 不生效 忘了加 @Valid / 嵌套对象没加 @Valid @RequestBody;嵌套字段加 @Valid
拦截器不生效 没注册到 WebMvcConfigurer,或被 excludePathPatterns 排除了 检查注册代码和路径匹配规则

Long 精度丢失全局处理

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
public class JacksonConfig {

@Bean
public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() {
return builder -> {
// Long / 大整数 → String,避免前端精度丢失
builder.serializerByType(Long.class, ToStringSerializer.instance);
builder.serializerByType(Long.TYPE, ToStringSerializer.instance);
builder.serializerByType(BigInteger.class, ToStringSerializer.instance);
};
}
}

1.12 完整示例:用户模块接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
@RestController
@RequestMapping("/api/users")
@Validated
public class UserController {

@Autowired
private UserService userService;

/** 分页查询:GET /api/users?page=1&size=10&keyword=张 */
@GetMapping
public R<PageResult<UserVO>> page(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size,
@RequestParam(required = false) String keyword) {
return R.ok(userService.page(page, size, keyword));
}

/** 详情:GET /api/users/1 */
@GetMapping("/{id}")
public R<UserVO> getById(@PathVariable Long id) {
UserVO vo = userService.getById(id);
if (vo == null) {
throw new BizException(404, "用户不存在");
}
return R.ok(vo);
}

/** 新增:POST /api/users */
@PostMapping
public R<Long> create(@Valid @RequestBody UserCreateDTO dto) {
return R.ok(userService.create(dto));
}

/** 全量更新:PUT /api/users/1 */
@PutMapping("/{id}")
public R<Void> update(@PathVariable Long id,
@Valid @RequestBody UserUpdateDTO dto) {
userService.update(id, dto);
return R.ok();
}

/** 局部改状态:PATCH /api/users/1/status?status=1 */
@PatchMapping("/{id}/status")
public R<Void> updateStatus(@PathVariable Long id,
@RequestParam Integer status) {
userService.updateStatus(id, status);
return R.ok();
}

/** 删除:DELETE /api/users/1 */
@DeleteMapping("/{id}")
public R<Void> delete(@PathVariable Long id) {
userService.removeById(id);
return R.ok();
}

/** 批量删除:POST /api/users/batch-delete Body: [1,2,3] */
@PostMapping("/batch-delete")
public R<Void> batchDelete(@RequestBody List<Long> ids) {
userService.removeByIds(ids);
return R.ok();
}

/** 导出:GET /api/users/export */
@GetMapping("/export")
public void export(HttpServletResponse response) throws IOException {
List<UserVO> list = userService.listAll();
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader("Content-Disposition",
"attachment; filename=" + URLEncoder.encode("用户列表.xlsx", "UTF-8"));
userService.exportExcel(list, response.getOutputStream());
}
}

配套 DTO / VO:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
/** 接收新增请求 */
@Data
public class UserCreateDTO {
@NotBlank(message = "用户名不能为空")
private String username;

@NotBlank(message = "密码不能为空")
@Size(min = 6, max = 20, message = "密码 6-20 位")
private String password;

@Email(message = "邮箱格式不正确")
private String email;

@NotNull(message = "年龄不能为空")
@Min(1) @Max(150)
private Integer age;
}

/** 返回给前端(不带密码) */
@Data
public class UserVO {
private String id; // Long 转 String,防精度丢失
private String username;
private String email;
private Integer age;
private String statusDesc;

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime createTime;
}

/** 分页结果 */
@Data
@AllArgsConstructor
public class PageResult<T> {
private List<T> records;
private Long total;
private Integer page;
private Integer size;

public static <T> PageResult<T> of(List<T> records, Long total, Integer page, Integer size) {
return new PageResult<>(records, total, page, size);
}
}
  • 本文标题:javaSpring
  • 本文作者:邵预鸿
  • 创建时间:2026-10-15 18:33:20
  • 本文链接:/images/logo.jpg2026/10/15/javaSpring/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!