java
@RestController
@RequestMapping("/api/students")
public class StudentController {
private final StudentService service;
public StudentController(StudentService service) {
this.service = service;
}
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public StudentResponse create(@RequestBody CreateStudentRequest request) {
return StudentResponse.from(service.createStudent(
request.name(), request.grade(), request.notes()));
}
@GetMapping
public List<StudentResponse> list() {
return service.listStudents().stream()
.map(StudentResponse::from).toList();
}
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, String> invalidInput(IllegalArgumentException exception) {
return Map.of("message", exception.getMessage());
}
@ExceptionHandler(HttpMessageNotReadableException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, String> invalidJson() {
return Map.of("message", "请求体必须是有效的学生 JSON 对象");
}
public record CreateStudentRequest(
String name, String grade, String notes) {}
public record StudentResponse(
Long id, String name, String grade, String notes) {
static StudentResponse from(Student student) {
return new StudentResponse(
student.getId(), student.getName(),
student.getGrade(), student.getNotes());
}
}
}
首先看第一段代码:
java
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public StudentResponse create(@RequestBody CreateStudentRequest request) {
return StudentResponse.from(service.createStudent(
request.name(), request.grade(), request.notes()));
}
@RestController表示这个类是一个 Spring MVC 的 REST 控制器,可以接受HTTP请求
@RequestMapping("/api/students")表示这个 Controller 里的所有接口,都统一以/api/students
开头。
@PostMapping表示接收 POST /api/students 的请求
首先在类中使用record构造了两个类:
CreateStudentRequest
用来便于操作网页端传来的json字符串。
StudentResponse
用于将数据库的数据有选择的封装响应对象返回给前端。
为什么要有选择,因为它是专门用来返回给前端的响应对象。实体类Student中的某些字段不方便返回给前端,实体类和接口返回结构最好不要强耦合。如果强耦合,直接返回Student实体类,会将数据库内部结构暴漏出去。这个类控制了前端能看到什么。
这个类中存在的静态方法
java
static StudentResponse from(Student student)
就是实现了把 Student 实体转换成 StudentResponse,进行有选择的数据封装。
java
@RequestBody CreateStudentRequest request
表示把 HTTP 请求体里的 JSON,自动转换成一个 CreateStudentRequest 对象。
java
@ResponseStatus(HttpStatus.CREATED)
表示这个接口成功后返回 HTTP 状态码:201 Created
每次 HTTP 请求最终都要有状态码;成功走正常响应,失败走异常处理或默认错误处理。
java
return StudentResponse.from(
service.createStudent(
request.name(),
request.grade(),
request.notes()
)
);
可以拆成3步:第一步先让 Service 创建学生
java
Student student = service.createStudent(
request.name(),
request.grade(),
request.notes()
);
第二步把实体对象转换成响应对象
java
StudentResponse response =
StudentResponse.from(student);
第三步将响应对象返回给前端。
接着看第二段代码:
java
@GetMapping
public List<StudentResponse> list() {
return service.listStudents().stream()
.map(StudentResponse::from).toList();
}
@GetMapping表示接收 Get /api/students 的请求
Service返回的是List<Student>,但 Controller 最终要返回List<StudentResponse>,所以需要进行类型转换。
java
.map(StudentResponse::from)
是方法引用。
它等价于:
.map(student -> StudentResponse.from(student))
意思就是:
Student1 → StudentResponse1
Student2 → StudentResponse2
Student3 → StudentResponse3
接着看第三段代码:
java
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, String> invalidInput(IllegalArgumentException exception) {
return Map.of("message", exception.getMessage());
}
这是一个异常处理。
对应StudentSercice里的
java
@Transactional
public Student createStudent(String name, String grade, String notes) {
String normalizedName = name == null ? null : name.strip();
if (normalizedName == null || normalizedName.isEmpty() || normalizedName.length() > 20) {
throw new IllegalArgumentException("Student name must contain 1 to 20 characters");
}
if (notes != null && notes.length() > 200) {
throw new IllegalArgumentException("Student notes must not exceed 200 characters");
}
String normalizedGrade = grade == null || grade.isBlank() ? null : grade.strip();
return repository.save(new Student(normalizedName, normalizedGrade, notes));
}
Service 抛出:
IllegalArgumentException
Controller 捕获到以后:
@ExceptionHandler(IllegalArgumentException.class)
就会调用:
invalidInput(...)
然后返回:
{
"message": "Student name must contain 1 to 20 characters"
}
HTTP 状态码:
400 Bad Request
接着看第四段代码:
java
@ExceptionHandler(HttpMessageNotReadableException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Map<String, String> invalidJson() {
return Map.of("message", "请求体必须是有效的学生 JSON 对象");
}
这个处理的是:
前端发来的 JSON 根本解析不了。
例如发:
{name: 张三
这种非法 JSON。
Spring 在进入:
create(...)
之前就可能解析失败。
于是抛:
HttpMessageNotReadableException
这里捕获之后返回:
{
"message": "请求体必须是有效的学生 JSON 对象"
}
也是:
400 Bad Request
| 注解 | 请求含义 | 常见用途 |
|---|---|---|
@GetMapping |
获取 | 查询数据 |
@PostMapping |
提交 | 新增数据 |
@PutMapping |
更新 | 整体修改数据 |
@PatchMapping |
更新 | 部分修改数据 |
@DeleteMapping |
删除 | 删除数据 |
GET、POST、PUT、DELETE 都是客户端发给服务器的不同类型请求;服务器处理后都会返回响应。
关于record
record 是 Java 专门用来表示只承载数据的简单对象的一种语法。
java
public record CreateStudentRequest(
String name,
String grade,
String notes) {
}
如果不用 record,你以前可能会写:
java
public class CreateStudentRequest {
private String name;
private String grade;
private String notes;
public String getName() {
return name;
}
...
}
但 record 可以直接简化成:
java
record CreateStudentRequest(
String name,
String grade,
String notes
) {}
Java 会自动帮你生成:
- 构造器
name()grade()notes()equals()hashCode()toString()
所以这里:
java
request.name()
request.grade()
request.notes()
其实就是在取 record 里面的数据。
整份Controller流程
java
StudentController
1. POST /api/students
↓
接收创建学生 JSON
↓
调 StudentService
↓
返回 StudentResponse
2. GET /api/students
↓
查询所有学生
↓
Student 转 StudentResponse
↓
返回列表
3. IllegalArgumentException
↓
返回 400 + 错误信息
4. 非法 JSON
↓
返回 400 + JSON 格式错误信息