SpringBoot请求响应详解

O泡李华 6

SpringBoot 请求响应详解

本章位置:第二阶段 Java 核心框架
前置知识:Servlet、HTTP、JSON、JavaBean、MyBatis
本章内容:SpringBoot Web 请求与响应
下一篇:RESTful
学习目标:理解 SpringBoot 如何接收 HTTP 请求、完成参数绑定、调用 Controller 方法并返回文本、对象、JSON,以及如何排查常见 Web 请求问题。


一、为什么学 SpringBoot 请求响应

前面 Servlet 阶段,我们处理请求要写:

@WebServlet(
        "/student/detail"
)
public class StudentDetailServlet
        extends HttpServlet {

    @Override
    protected void doGet(
            HttpServletRequest request,
            HttpServletResponse response
    )
            throws IOException {

        String idText =
                request.getParameter(
                        "id"
                );

        // 参数转换
        // 业务调用
        // 响应输出
    }
}

现在进入 SpringBoot 后,大量模板代码会被框架接管。

例如:

@GetMapping(
        "/student/detail"
)
public Student detail(
        Long id
) {

    return studentService.findById(
            id
    );
}

SpringBoot 会帮我们:

接收 HTTP 请求

匹配 URL

找到 Controller

读取请求参数

自动类型转换

调用 Java 方法

把返回值转 JSON

写回 HTTP Response

二、SpringBoot Web 请求整体流程

最重要的一张图:

浏览器 / Vue / Postman
↓
HTTP Request
↓
Tomcat
↓
DispatcherServlet
↓
HandlerMapping
↓
Controller
↓
Service
↓
Mapper
↓
Database
↓
Controller 返回结果
↓
HttpMessageConverter
↓
JSON / Text
↓
HTTP Response
↓
客户端

三、SpringBoot 和 Servlet 的关系

SpringBoot Web 并没有“抛弃 Servlet”。

Spring MVC 核心仍然建立在:

Servlet 体系

之上。

核心 Servlet:

DispatcherServlet

它相当于:

Spring MVC 的前端控制器

四、DispatcherServlet 是什么

以前:

每一个 URL
对应一个 Servlet

现在:

大量请求
先进入 DispatcherServlet

然后由 Spring 根据:

@RequestMapping
@GetMapping
@PostMapping

找到具体 Controller 方法。


五、为什么叫前端控制器

这里“前端”不是:

Vue / HTML 前端

而是:

所有请求进入业务 Controller 之前,先经过的统一入口。

也就是:

Front Controller Pattern

六、创建 SpringBoot Web 项目后的基础结构

常见:

src/main/java
└─ com.example.demo
   ├─ DemoApplication.java
   ├─ controller
   │  └─ StudentController.java
   ├─ service
   │  └─ StudentService.java
   ├─ mapper
   │  └─ StudentMapper.java
   └─ entity
      └─ Student.java

七、SpringBoot 启动类

@SpringBootApplication
public class DemoApplication {

    public static void main(
            String[] args
    ) {

        SpringApplication.run(
                DemoApplication.class,
                args
        );
    }
}

八、@SpringBootApplication

它是一个组合注解。

可以简单理解为组合了:

@Configuration

@EnableAutoConfiguration

@ComponentScan

当前重点不是底层细节。

先理解:

它标记 SpringBoot 启动类

九、SpringBoot 默认内嵌 Tomcat

传统项目:

先安装 Tomcat

再部署 WAR

SpringBoot Web 常见:

项目内部已经带内嵌 Tomcat

直接运行:

main()

就可以启动 Web 服务。


十、默认端口

默认:

8080

访问:

http://localhost:8080

十一、修改端口

application.properties:

server.port=8081

或者 application.yml:

server:
  port: 8081

十二、Controller 是什么

Controller:

负责接收 HTTP 请求
调用业务逻辑
返回响应结果

这和以前 Servlet 的 Controller 职责一致。


十三、@RestController

最常见:

@RestController
public class HelloController {

}

表示:

这是一个 Controller

方法返回值直接写入响应体

十四、@RestController 等价理解

可以简单理解:

@Controller
+
@ResponseBody

十五、@Controller

@Controller
public class PageController {

}

更传统地用于:

返回页面 View

例如:

JSP
Thymeleaf

十六、@ResponseBody

如果:

@Controller

的方法加:

@ResponseBody

返回值会直接写入:

HTTP Response Body

十七、第一个接口

@RestController
public class HelloController {

    @GetMapping(
            "/hello"
    )
    public String hello() {

        return "Hello SpringBoot";
    }
}

访问:

GET /hello

响应:

Hello SpringBoot

十八、@GetMapping

@GetMapping(
        "/hello"
)

表示:

只处理 GET 请求

十九、@PostMapping

@PostMapping(
        "/student"
)

表示:

只处理 POST

二十、@PutMapping

@PutMapping(
        "/student"
)

处理:

PUT

二十一、@DeleteMapping

@DeleteMapping(
        "/student/{id}"
)

处理:

DELETE

二十二、@PatchMapping

处理:

PATCH

例如:

@PatchMapping(
        "/student/{id}"
)

二十三、@RequestMapping

最通用:

@RequestMapping(
        "/student"
)

可以放:

类上

方法上

二十四、类级别映射

@RestController
@RequestMapping(
        "/students"
)
public class StudentController {

}

方法:

@GetMapping(
        "/list"
)

最终路径:

/students/list

二十五、推荐的 Controller 路径结构

@RestController
@RequestMapping(
        "/students"
)
public class StudentController {

    @GetMapping
    public List<Student> list() {

    }

    @GetMapping(
            "/{id}"
    )
    public Student detail() {

    }

    @PostMapping
    public Student add() {

    }

    @PutMapping(
            "/{id}"
    )
    public void update() {

    }

    @DeleteMapping(
            "/{id}"
    )
    public void delete() {

    }
}

这会自然过渡到下一篇:

RESTful

二十六、请求参数有哪几类

SpringBoot Web 中常见参数来源:

Query String

Path Variable

Form Data

JSON Body

Header

Cookie

Multipart File

二十七、Query String

例如:

GET /students?pageNum=1&pageSize=10

参数:

pageNum

pageSize

二十八、最简单参数绑定

@GetMapping(
        "/search"
)
public String search(
        String keyword
) {

    return keyword;
}

请求:

/search?keyword=java

Spring 会自动:

keyword=java

绑定给:

String keyword

二十九、参数名必须匹配

请求:

?keyword=java

方法:

String keyword

匹配。

如果参数名完全不同:

就需要显式 @RequestParam

三十、@RequestParam

推荐明确写:

@GetMapping(
        "/search"
)
public String search(
        @RequestParam(
                "keyword"
        )
        String keyword
) {

    return keyword;
}

三十一、@RequestParam required

默认通常:

required = true

如果没传:

可能返回 400

三十二、可选参数

@RequestParam(
        value = "keyword",
        required = false
)
String keyword

这样:

不传也可以

三十三、默认值

@RequestParam(
        value = "pageNum",
        defaultValue = "1"
)
int pageNum

如果用户没传:

pageNum = 1

三十四、分页参数示例

@GetMapping
public String list(
        @RequestParam(
                defaultValue = "1"
        )
        int pageNum,

        @RequestParam(
                defaultValue = "10"
        )
        int pageSize
) {

    return pageNum
            + "-"
            + pageSize;
}

三十五、Spring 自动类型转换

请求:

?pageNum=10

方法参数:

int pageNum

Spring 自动:

String
→
int

三十六、类型转换失败

如果请求:

?pageNum=abc

方法:

int pageNum

Spring 无法转换:

通常返回 400

三十七、包装类型和基本类型

如果参数可能缺失:

Integer age

通常比:

int age

更合适。

因为:

Integer
可以为 null

三十八、PathVariable 是什么

路径参数:

/students/10

其中:

10

就是:

Path Variable

三十九、@PathVariable

@GetMapping(
        "/students/{id}"
)
public String detail(
        @PathVariable
        Long id
) {

    return "id="
            + id;
}

请求:

/students/10

得到:

id = 10

四十、显式指定变量名

@PathVariable(
        "id"
)
Long studentId

这样路径:

{id}

可以绑定到:

studentId

四十一、RequestParam 和 PathVariable 区别

例如查询:

/students?id=10

使用:

@RequestParam

RESTful 风格:

/students/10

使用:

@PathVariable

四十二、表单参数绑定 JavaBean

前端表单:

studentNo=20260001
name=张三
age=20
major=软件工程

JavaBean:

public class Student {

    private String studentNo;

    private String name;

    private Integer age;

    private String major;

    // Getter / Setter
}

Controller:

@PostMapping(
        "/students/form"
)
public Student add(
        Student student
) {

    return student;
}

Spring 可以自动:

请求参数
→
Student

四十三、自动绑定为什么能实现

本质会结合:

JavaBean

反射

内省

类型转换

根据参数名:

name
age
major

找到属性:

setName()
setAge()
setMajor()

四十四、@ModelAttribute

表单/Query 参数绑定对象可以显式写:

@PostMapping(
        "/students/form"
)
public Student add(
        @ModelAttribute
        Student student
) {

    return student;
}

很多时候:

可以省略

四十五、JSON 请求

前后端分离最常见:

POST /students
Content-Type: application/json

Body:

{
    "studentNo": "20260001",
    "name": "张三",
    "age": 20,
    "major": "软件工程"
}

四十六、@RequestBody

Controller:

@PostMapping(
        "/students"
)
public Student add(
        @RequestBody
        Student student
) {

    return student;
}

表示:

读取 HTTP Request Body

按照 JSON 解析

转换成 Student

四十七、为什么 JSON 必须用 @RequestBody

如果只是:

Student student

Spring 更倾向于:

表单 / Query 参数绑定

如果请求体是 JSON:

通常需要 @RequestBody

四十八、@RequestBody 背后是谁转换

Spring MVC 使用:

HttpMessageConverter

把:

JSON
↔
Java Object

四十九、Jackson

SpringBoot Web 默认常见使用:

Jackson

完成:

Java 对象
↔
JSON

转换。


五十、Java 对象返回为什么自动变 JSON

例如:

@GetMapping(
        "/student"
)
public Student getStudent() {

    Student student =
            new Student();

    student.setName(
            "张三"
    );

    student.setAge(
            20
    );

    return student;
}

因为:

@RestController

最终 Spring 会:

Student
↓
Jackson
↓
JSON

响应:

{
    "name": "张三",
    "age": 20
}

五十一、List 也可以自动转 JSON

@GetMapping(
        "/students"
)
public List<Student> list() {

    return studentService.findAll();
}

返回:

[
    {
        "id": 1,
        "name": "张三"
    },
    {
        "id": 2,
        "name": "李四"
    }
]

五十二、Map 响应

@GetMapping(
        "/info"
)
public Map<String, Object> info() {

    Map<String, Object> map =
            new HashMap<>();

    map.put(
            "code",
            200
    );

    map.put(
            "message",
            "success"
    );

    return map;
}

会自动变 JSON。


五十三、为什么不推荐项目里到处返回 Map

Map 优点:

快
灵活

缺点:

字段没有类型约束

容易拼错 key

IDE 不好提示

维护困难

更推荐:

DTO

VO

统一响应对象

五十四、统一响应对象

常见:

public class Result<T> {

    private Integer code;

    private String message;

    private T data;

}

五十五、Result 成功方法

public static <T>
Result<T> success(
        T data
) {

    Result<T> result =
            new Result<>();

    result.setCode(
            200
    );

    result.setMessage(
            "success"
    );

    result.setData(
            data
    );

    return result;
}

五十六、失败方法

public static <T>
Result<T> fail(
        Integer code,
        String message
) {

    Result<T> result =
            new Result<>();

    result.setCode(
            code
    );

    result.setMessage(
            message
    );

    return result;
}

五十七、Controller 使用统一响应

@GetMapping(
        "/students/{id}"
)
public Result<Student> detail(
        @PathVariable
        Long id
) {

    Student student =
            studentService.findById(
                    id
            );

    return Result.success(
            student
    );
}

五十八、统一响应的优点

前端永远可以按:

code

message

data

读取。

减少:

接口格式混乱

五十九、业务 code 和 HTTP 状态码不是一回事

例如:

{
    "code": 404,
    "message": "学生不存在"
}

并不代表 HTTP 一定:

404

很多项目会:

HTTP 200
+
业务 code

也有项目会:

正确使用 HTTP 状态码

两种风格都存在。

更推荐:

HTTP 状态码表达协议层结果
业务 code 表达业务层结果

六十、ResponseEntity

如果需要控制 HTTP 状态码:

@GetMapping(
        "/students/{id}"
)
public ResponseEntity<Student> detail(
        @PathVariable
        Long id
) {

    Student student =
            studentService.findById(
                    id
            );

    if (
        student == null
    ) {

        return ResponseEntity
                .notFound()
                .build();
    }

    return ResponseEntity.ok(
            student
    );
}

六十一、ResponseEntity 能做什么

控制:

状态码

响应头

响应体

六十二、返回 201 Created

新增资源:

return ResponseEntity
        .status(
                HttpStatus.CREATED
        )
        .body(
                student
        );

六十三、返回 204 No Content

删除成功:

return ResponseEntity
        .noContent()
        .build();

六十四、请求头 @RequestHeader

@GetMapping(
        "/header"
)
public String header(
        @RequestHeader(
                "User-Agent"
        )
        String userAgent
) {

    return userAgent;
}

六十五、可选请求头

@RequestHeader(
        value = "X-Token",
        required = false
)
String token

六十六、获取全部 Header

可以使用:

HttpHeaders

或者:

@RequestHeader
Map<String, String> headers

六十七、Cookie 参数

可以:

@CookieValue(
        value = "JSESSIONID",
        required = false
)
String sessionId

六十八、原生 HttpServletRequest 仍然可以使用

Spring MVC 并没有禁止原生 Servlet API。

例如:

@GetMapping(
        "/request-info"
)
public String requestInfo(
        HttpServletRequest request
) {

    return request.getRequestURI();
}

六十九、什么时候用原生 request

当 Spring 已经能自动绑定时:

优先自动绑定

只有特殊场景才直接使用:

HttpServletRequest
HttpServletResponse

七十、为什么优先自动绑定

因为:

代码更少

类型更明确

更容易测试

业务代码不依赖 Servlet API

七十一、多个 Query 参数绑定对象

例如查询:

/students?keyword=java&minAge=18&maxAge=25

定义:

public class StudentQuery {

    private String keyword;

    private Integer minAge;

    private Integer maxAge;

}

Controller:

@GetMapping(
        "/students/search"
)
public List<Student> search(
        StudentQuery query
) {

    return studentService.search(
            query
    );
}

Spring 自动绑定。


七十二、DTO 是什么

DTO:

Data Transfer Object

用于:

请求或响应的数据传输

例如:

public class StudentCreateDTO {

    private String studentNo;

    private String name;

    private Integer age;

}

七十三、为什么不要所有接口都直接用 Entity

数据库实体:

可能包含很多内部字段

例如:

createTime

updateTime

deleted

passwordHash

前端新增时:

根本不应该提交这些字段

所以真实项目常分:

Entity

DTO

VO

七十四、Entity

主要对应:

数据库表

七十五、DTO

主要对应:

客户端提交的数据

七十六、VO

VO:

View Object

常用于:

返回给前端的数据结构

七十七、JSON 字段名和 Java 属性名

JSON:

{
    "studentNo": "20260001"
}

Java:

private String studentNo;

名称匹配:

自动绑定

七十八、JSON 字段名不一样怎么办

Jackson 可以通过:

@JsonProperty(
        "student_no"
)
private String studentNo;

让:

student_no
↔
studentNo

七十九、JSON 日期格式

例如:

private LocalDateTime createTime;

实际项目可能需要配置:

日期序列化格式

例如:

@JsonFormat(
        pattern = "yyyy-MM-dd HH:mm:ss"
)

八十、JSON 忽略字段

例如密码:

@JsonIgnore
private String password;

返回 JSON 时:

不输出

八十一、请求体只能读取一次吗

底层:

HTTP Request Body

本质是输入流。

Spring 会统一处理:

@RequestBody

一般不要自己又:

手动读 InputStream

再让框架读一次。


八十二、Content-Type 必须匹配

请求发送 JSON:

Content-Type: application/json

Controller:

@RequestBody Student student

最匹配。

如果 Content-Type 错误:

可能 415

八十三、415 Unsupported Media Type

常见原因:

接口要求 JSON

客户端却发 form-data

或者 Content-Type 没设置正确

八十四、400 Bad Request

SpringBoot 中常见原因:

参数缺失

类型转换失败

JSON 格式错误

@RequestBody 解析失败

八十五、404 Not Found

常见:

URL 错

Controller 不在扫描范围

@RequestMapping 路径不对

应用启动失败

八十六、405 Method Not Allowed

例如:

接口只允许 POST

客户端发送:

GET

会返回:

405

八十七、500 Internal Server Error

通常:

Controller / Service / Mapper 内部异常

例如:

NullPointerException

SQL 错误

业务异常没处理

八十八、请求参数日志怎么排查

浏览器:

F12
→ Network

Postman:

Params
Body
Headers

SpringBoot 控制台:

看异常堆栈

八十九、Postman 测试 Query 参数

例如:

GET /students/search

Params:

keyword=java
pageNum=1

九十、Postman 测试 JSON

Body:

raw
↓
JSON

内容:

{
    "studentNo": "20260003",
    "name": "王五",
    "age": 20,
    "major": "人工智能"
}

Header:

Content-Type: application/json

九十一、文件上传简单预览

Controller:

@PostMapping(
        "/upload"
)
public String upload(
        @RequestParam(
                "file"
        )
        MultipartFile file
) {

    return file.getOriginalFilename();
}

请求类型:

multipart/form-data

九十二、MultipartFile 常用方法

getOriginalFilename()

getSize()

getContentType()

getInputStream()

transferTo(...)

九十三、文件上传安全注意

不要直接相信:

原始文件名

Content-Type

扩展名

真实项目要校验:

文件大小

文件类型

保存路径

文件名

权限

九十四、多个请求参数集合

例如:

?id=1&id=2&id=3

可以:

@RequestParam(
        "id"
)
List<Long> ids

Spring 可自动绑定集合。


九十五、数组参数

@RequestParam(
        "id"
)
Long[] ids

也可以。


九十六、PathVariable 多参数

@GetMapping(
        "/schools/{schoolId}/students/{studentId}"
)
public String detail(
        @PathVariable
        Long schoolId,

        @PathVariable
        Long studentId
) {

    return schoolId
            + ":"
            + studentId;
}

九十七、请求路径命名建议

推荐:

名词

复数资源

例如:

/students

/users

/orders

而不是大量:

/getStudent

/addStudent

/deleteStudent

下一篇 RESTful 会详细讲。


九十八、Controller 不应该直接写 SQL

错误:

@GetMapping(
        "/students"
)
public List<Student> list() {

    // JDBC
    // SQL
}

推荐:

Controller
↓
Service
↓
Mapper

九十九、Controller 应该负责什么

接收请求

参数绑定

基础参数校验

调用 Service

返回响应

一百、Service 负责什么

业务逻辑

事务

业务校验

多个 Mapper 协调

一百零一、Mapper 负责什么

数据库读写

一百零二、一个完整新增接口

DTO:

public class StudentCreateDTO {

    private String studentNo;

    private String name;

    private Integer age;

    private String major;

    // Getter / Setter
}

Controller:

@PostMapping(
        "/students"
)
public Result<Student> add(
        @RequestBody
        StudentCreateDTO dto
) {

    Student student =
            studentService.create(
                    dto
            );

    return Result.success(
            student
    );
}

一百零三、一个完整详情接口

@GetMapping(
        "/students/{id}"
)
public Result<Student> detail(
        @PathVariable
        Long id
) {

    Student student =
            studentService.findById(
                    id
            );

    if (
        student == null
    ) {

        return Result.fail(
                404,
                "学生不存在"
        );
    }

    return Result.success(
            student
    );
}

一百零四、一个完整分页接口

@GetMapping(
        "/students"
)
public Result<PageResult<Student>>
list(
        @RequestParam(
                defaultValue = "1"
        )
        int pageNum,

        @RequestParam(
                defaultValue = "10"
        )
        int pageSize,

        @RequestParam(
                required = false
        )
        String keyword
) {

    PageResult<Student> page =
            studentService.findPage(
                    keyword,
                    pageNum,
                    pageSize
            );

    return Result.success(
            page
    );
}

一百零五、返回 JSON 的完整结构

可能:

{
    "code": 200,
    "message": "success",
    "data": {
        "records": [],
        "total": 100,
        "pageNum": 1,
        "pageSize": 10
    }
}

一百零六、前端 Vue 为什么喜欢这种格式

因为前端可以统一:

判断 code

显示 message

读取 data

所有接口结构一致。


一百零七、参数校验基础

手工:

if (
    dto.getName() == null
    ||
    dto.getName().isBlank()
) {

    return Result.fail(
            400,
            "姓名不能为空"
    );
}

但真实项目大量手写会:

重复

一百零八、Bean Validation 预览

后面可以使用:

jakarta.validation

例如:

@NotBlank(
        message = "姓名不能为空"
)
private String name;

Controller:

@Valid
@RequestBody
StudentCreateDTO dto

一百零九、SpringBoot 3 使用 jakarta 包

需要注意:

SpringBoot 3 基于:

Jakarta EE

常见:

jakarta.validation.Valid
jakarta.servlet.http.HttpServletRequest

这和你之前 Tomcat 8.5 的:

javax.servlet

属于不同命名空间时代。


一百一十、不要把 javax 和 jakarta 乱混

SpringBoot 3 项目里:

优先使用 jakarta.*

传统 Tomcat 8.5 Servlet 项目:

通常使用 javax.*

一百一十一、返回 String 的含义取决于注解

如果类:

@RestController

返回:

"hello"

通常是:

响应正文

一百一十二、@Controller 返回 String

如果:

@Controller

方法返回:

"student-list"

通常解释为:

View 名称

而不是直接响应字符串。


一百一十三、这就是 @RestController 和 @Controller 的重要区别

@RestController
更适合前后端分离 JSON API

@Controller
更适合服务端页面渲染

一百一十四、produces

可以指定响应类型:

@GetMapping(
        value = "/hello",
        produces = "text/plain;charset=UTF-8"
)

一百一十五、consumes

可以限制请求类型:

@PostMapping(
        value = "/students",
        consumes = "application/json"
)

表示:

只接受 JSON

一百一十六、如果 consumes 不匹配

可能:

415 Unsupported Media Type

一百一十七、URL 编码

Query 参数包含中文:

keyword=软件工程

浏览器通常会进行:

URL Encoding

Spring / Tomcat 再解析。

现代 SpringBoot 默认 UTF-8 处理通常比老 Servlet 项目方便很多。


一百一十八、请求参数和 JSON 不要混淆

例如:

GET /students?id=1

使用:

@RequestParam

JSON:

{
    "id": 1
}

使用:

@RequestBody

一百一十九、Path 参数不要用 RequestParam

请求:

/students/1

应该:

@PathVariable

一百二十、常见错误:忘记 @RequestBody

前端发 JSON:

{
    "name": "张三"
}

Controller:

public void add(
        Student student
)

结果:

对象字段可能全是 null

此时优先检查:

是不是应该加 @RequestBody

一百二十一、常见错误:Content-Type 不对

Postman Body 选择:

raw

但没有正确:

application/json

可能导致:

@RequestBody 无法解析

一百二十二、常见错误:JSON 格式错误

错误:

{
    "name": "张三",
}

最后多一个逗号。

可能:

400
JSON parse error

一百二十三、常见错误:数字字段传文字

Java:

private Integer age;

JSON:

{
    "age": "abc"
}

可能:

400
类型转换失败

一百二十四、常见错误:路径重复

类:

@RequestMapping(
        "/students"
)

方法:

@GetMapping(
        "/students"
)

最终:

/students/students

很多初学者会误以为:

还是 /students

一百二十五、常见错误:Controller 没被扫描

启动类:

com.example.DemoApplication

Controller 却在:

com.other.controller

默认组件扫描可能扫不到。

推荐目录:

启动类放最外层根包

例如:

com.example
├─ DemoApplication
├─ controller
├─ service
└─ mapper

一百二十六、常见错误:404 但应用正常启动

检查:

端口

context-path

类级 RequestMapping

方法级 Mapping

Controller 扫描

一百二十七、server.servlet.context-path

可以配置:

server.servlet.context-path=/api

那么:

/hello

真实访问:

/api/hello

一百二十八、为什么不建议随便设置 context-path

前后端联调时经常造成:

路径多一层
404

学习阶段如果不是必要:

可以保持默认空 context path

一百二十九、常见错误:405

例如:

@PostMapping(
        "/students"
)

却在浏览器地址栏直接访问:

GET /students

会:

405

一百三十、常见错误:415

通常:

Content-Type 与 @RequestBody/consumes 不匹配

一百三十一、常见错误:406

可能是:

客户端 Accept

和服务端可产生响应类型

不匹配。

初学阶段遇到较少。


一百三十二、常见错误:500

查看:

Caused by

可能:

Service 异常

Mapper 异常

SQL 异常

空指针

一百三十三、日志排查顺序

客户端
↓
URL
↓
Method
↓
Headers
↓
Content-Type
↓
Params / Body
↓
Controller 是否进入
↓
Service
↓
Mapper
↓
SQL
↓
Response

一百三十四、开发阶段可以打印简单日志

例如:

System.out.println(
        "进入 add"
);

用来确认:

请求是否进入 Controller

但实际项目更推荐:

日志框架

一百三十五、SpringBoot 默认日志

通常使用:

SLF4J
+
Logback

以后可以写:

private static final Logger log =
        LoggerFactory.getLogger(
                StudentController.class
        );

一百三十六、不要打印密码

日志中不要输出:

密码

Token

身份证

银行卡号

完整敏感数据

一百三十七、Controller 方法参数可以很多吗

技术上可以:

public Result<?> search(
        String keyword,
        Integer minAge,
        Integer maxAge,
        String major,
        Integer pageNum,
        Integer pageSize
)

但参数越来越多时:

建议封装 Query DTO

一百三十八、Query DTO

public class StudentQueryDTO {

    private String keyword;

    private Integer minAge;

    private Integer maxAge;

    private String major;

    private Integer pageNum = 1;

    private Integer pageSize = 10;

}

Controller:

@GetMapping(
        "/students"
)
public Result<?> list(
        StudentQueryDTO query
) {

    return Result.success(
            studentService.findPage(
                    query
            )
    );
}

一百三十九、请求对象和响应对象分离

例如:

StudentCreateDTO
新增请求

StudentUpdateDTO
修改请求

StudentQueryDTO
查询参数

StudentVO
响应对象

这样接口更清晰。


一百四十、为什么修改 DTO 和新增 DTO 可能不同

新增:

id 不需要前端传

修改:

可能需要 id

新增必填字段:

可能比修改多

所以不要强迫:

一个 DTO 走天下

一百四十一、RequestBody 一般配 POST/PUT/PATCH

因为这些请求通常:

带请求体

GET:

通常不推荐依赖 JSON Body

一百四十二、GET 查询条件适合什么

@RequestParam

Query DTO

@PathVariable

一百四十三、接口设计例子

查询列表:

GET /students?pageNum=1&pageSize=10

详情:

GET /students/1

新增:

POST /students
JSON Body

修改:

PUT /students/1
JSON Body

删除:

DELETE /students/1

这就是下一章 RESTful 的核心风格。


一百四十四、Servlet 写法和 SpringBoot 对照

Servlet:

String idText =
        request.getParameter(
                "id"
        );

Long id =
        Long.valueOf(
                idText
        );

SpringBoot:

@RequestParam
Long id

一百四十五、Servlet JSON 输出和 SpringBoot 对照

Servlet:

response.setContentType(
        "application/json;charset=UTF-8"
);

response
        .getWriter()
        .write(
                json
        );

SpringBoot:

@RestController

public Student detail() {

    return student;
}

一百四十六、Servlet 路由和 SpringBoot 对照

Servlet:

@WebServlet(
        "/student/list"
)

SpringBoot:

@GetMapping(
        "/students"
)

一百四十七、Servlet 参数绑定和 SpringBoot 对照

Servlet:

request.getParameter(
        "name"
);

SpringBoot:

@RequestParam
String name

一百四十八、Servlet 路径参数

原生 Servlet:

要自己分析 URI / PathInfo

SpringBoot:

@PathVariable
Long id

非常方便。


一百四十九、Servlet JSON 解析

原生 Servlet:

自己读 InputStream

再调用 Jackson

SpringBoot:

@RequestBody
Student dto

框架自动完成。


一百五十、SpringBoot 为什么代码少很多

因为 Spring MVC 帮我们封装了:

请求路由

参数绑定

类型转换

JSON 序列化

JSON 反序列化

状态码处理

异常处理扩展

文件上传

一百五十一、但底层逻辑没有消失

仍然是:

HTTP
↓
Tomcat
↓
Servlet
↓
Spring MVC
↓
Controller

只是:

框架替你写了模板代码

一百五十二、练习 1:Hello Controller

创建:

@GetMapping(
        "/hello"
)

返回:

Hello SpringBoot

一百五十三、练习 2:RequestParam

接口:

GET /hello?name=张三

返回:

你好,张三

一百五十四、练习 3:默认参数

GET /page

参数:

pageNum 默认 1

pageSize 默认 10

一百五十五、练习 4:PathVariable

接口:

GET /students/10

方法:

@PathVariable
Long id

一百五十六、练习 5:表单绑定

提交:

studentNo
name
age
major

直接绑定:

Student student

一百五十七、练习 6:JSON 请求

Postman:

POST /students

Body:

{
    "studentNo": "20260010",
    "name": "张三",
    "age": 20,
    "major": "软件工程"
}

Controller:

@RequestBody
Student student

一百五十八、练习 7:JSON 响应

Controller 返回:

Student

观察 Postman:

自动 JSON

一百五十九、练习 8:List JSON

返回:

List<Student>

观察:

JSON 数组

一百六十、练习 9:统一 Result

创建:

Result<T>

所有接口统一返回:

code
message
data

一百六十一、练习 10:400

接口参数:

@RequestParam
Integer age

请求:

?age=abc

观察:

400

一百六十二、练习 11:405

接口只:

POST

使用浏览器 GET 访问。

观察:

405

一百六十三、练习 12:415

接口:

@RequestBody Student

Postman 故意:

Content-Type 设错误

观察:

415

一百六十四、练习 13:Query DTO

创建:

StudentQueryDTO

支持:

keyword
minAge
maxAge
pageNum
pageSize

自动绑定。


一百六十五、练习 14:完整学生 CRUD Controller

实现:

GET /students

GET /students/{id}

POST /students

PUT /students/{id}

DELETE /students/{id}

当前先不追求 RESTful 完整规范。

下一章再系统优化。


一百六十六、必须掌握的注解

@RestController

@Controller

@ResponseBody

@RequestMapping

@GetMapping

@PostMapping

@PutMapping

@DeleteMapping

@PatchMapping

@RequestParam

@PathVariable

@RequestBody

@ModelAttribute

@RequestHeader

@CookieValue

一百六十七、必须掌握的请求类型

Query String

Path Variable

Form Data

JSON Body

Header

Cookie

Multipart

一百六十八、必须掌握的响应方式

String

JavaBean

List

Map

Result<T>

ResponseEntity

一百六十九、必须掌握常见状态码

200

201

204

400

404

405

415

500

一百七十、必须回答的问题

学完后应该能回答:

1. SpringBoot Web 请求为什么仍然和 Servlet 有关系?

2. DispatcherServlet 是什么?

3. @RestController 和 @Controller 有什么区别?

4. @GetMapping 和 @PostMapping 有什么区别?

5. @RequestMapping 可以写在哪里?

6. @RequestParam 是什么?

7. @PathVariable 是什么?

8. RequestParam 和 PathVariable 有什么区别?

9. @RequestBody 是什么?

10. 为什么 JSON 请求通常要加 @RequestBody?

11. SpringBoot 为什么能自动把 JSON 转 Java 对象?

12. HttpMessageConverter 是什么?

13. Jackson 是做什么的?

14. JavaBean 为什么能自动接收表单参数?

15. DTO 是什么?

16. Entity、DTO、VO 有什么区别?

17. 为什么不推荐所有接口都直接返回 Map?

18. 为什么要统一响应格式?

19. ResponseEntity 有什么作用?

20. 400、404、405、415、500 分别常见于什么情况?

21. 为什么 SpringBoot 3 使用 jakarta 包?

22. 为什么 JSON 的 Content-Type 应该是 application/json?

23. Controller 应该负责什么?

24. Service 应该负责什么?

25. Mapper 应该负责什么?

一百七十一、SpringBoot 请求响应知识结构

SpringBoot Web
│
├─ 请求入口
│   ├─ Tomcat
│   └─ DispatcherServlet
│
├─ Controller
│   ├─ RestController
│   ├─ RequestMapping
│   ├─ GetMapping
│   ├─ PostMapping
│   └─ DeleteMapping
│
├─ 参数绑定
│   ├─ RequestParam
│   ├─ PathVariable
│   ├─ ModelAttribute
│   ├─ RequestBody
│   ├─ RequestHeader
│   └─ CookieValue
│
├─ 数据转换
│   ├─ Type Conversion
│   ├─ HttpMessageConverter
│   └─ Jackson
│
├─ 响应
│   ├─ String
│   ├─ Object
│   ├─ List
│   ├─ Result<T>
│   └─ ResponseEntity
│
└─ 错误
    ├─ 400
    ├─ 404
    ├─ 405
    ├─ 415
    └─ 500

一百七十二、从 Servlet 到 SpringBoot

以前:

@WebServlet
↓
doGet / doPost
↓
request.getParameter
↓
response.getWriter

现在:

@GetMapping / @PostMapping
↓
Controller 方法
↓
自动参数绑定
↓
直接 return

对照:

Servlet Mapping
→
@RequestMapping

request.getParameter
→
@RequestParam

URI 分析
→
@PathVariable

手工读 JSON
→
@RequestBody

response.getWriter
→
@ResponseBody / RestController

手工 Jackson
→
HttpMessageConverter

一百七十三、本章总结

SpringBoot 请求响应的核心不是:

背注解

而是理解:

HTTP 请求
如何被 Spring MVC
映射成 Java 方法调用

完整流程:

客户端
↓
HTTP
↓
Tomcat
↓
DispatcherServlet
↓
Controller
↓
参数绑定
↓
Service
↓
Mapper
↓
Controller 返回值
↓
HttpMessageConverter
↓
JSON
↓
HTTP Response

最重要参数注解:

@RequestParam
Query 参数

@PathVariable
路径参数

@RequestBody
JSON 请求体

@RequestHeader
请求头

最重要 Controller 注解:

@RestController

@RequestMapping

@GetMapping

@PostMapping

最重要响应方式:

Object
List
Result<T>
ResponseEntity

排错时:

URL
↓
HTTP Method
↓
Content-Type
↓
Query / Path / Body
↓
Controller Mapping
↓
参数绑定
↓
Service
↓
Mapper
↓
Response

到这里,你已经从:

手写 Servlet 请求响应

正式过渡到:

SpringBoot / Spring MVC 请求响应

按照课程表,下一篇就是:

RESTful

会把:

URL 设计
GET
POST
PUT
DELETE
资源
状态码
统一接口规范

系统整理成一套真正的 API 设计方式。