1. 为什么写这个文章
我们经常在各种教程,博客,网站中看到,知乎,掘金等网站中,看到各种各样的文章教程,告诉我们如何定义一个Controller响应体。一般建议的方案是这样
public class CommonResult<T> {
private Integer code;
private String message;
private T data;
// ... getters and setters
}
然后会让你在Controller中使用这个类来定义响应体
@RestController
@RequestMapping("/users")
public class UserController {
@RequestMapping("/get")
public CommonResult<String> get() {
// 模拟从数据库中获取用户
UserDto user = new UserDto();
user.setId(1L);
user.setName("user" + id);
// 构建响应体
CommonResult<UserDto> result = new CommonResult<>();
result.setCode(200);
result.setMessage("success");
result.setData(user);
return result;
}
}
这样每次set可能比较麻烦,我们可以考虑使用构造函数来初始化响应体,或者构造两个静态方法来创建成功和失败的响应体
public class CommonResult<T> {
// 构造两个静态方法来创建成功和失败的响应体
public static <T> CommonResult<T> success(T data) {
return new CommonResult<>(200, "success", data);
}
public static <T> CommonResult<T> fail(Integer code, String message) {
return new CommonResult<>(code, message, null);
}
public CommonResult() {
}
public CommonResult(int code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
private Integer code;
private String message;
private T data;
// ... getters and setters
}
大家都推荐这么做,但真的有必要这么做吗?如果还真的有必要,Spring为什么一直没有提供这么一个类来帮我们实现这个逻辑呢?
答案就是完全没有必要。我们可以直接返回一个对象,spring会自动将其转换为json格式。所以我们可以直接返回一个对象,而不需要定义一个响应体类。
@RestController
@RequestMapping("/users")
public class UserController {
@RequestMapping("/{id}")
public UserDto get(
@PathVariable Long id
) {
// 模拟从数据库中获取用户
UserDto user = new UserDto();
user.setId(1L);
user.setName("user" + id);
return user;
}
}
是的,一切就是这么简单,不需要自定义响应类,不需要区分成功和失败,你不需要做任何跟业务无关的其它工作。
2 如何定义API
有了上面的基础,我们接下来看看如何实现Restful API。包括,新增,修改,删除,获取,分页查询
2.1 定义API
根据Restful API的设计规范,我们接下来看看如何定义API
- POST /users:创建用户
- GET /users/{id}:获取用户
- PUT /users/{id}:修改用户
- DELETE /users/{id}:删除用户
- GET /users:分页查询用户列表
2.1.1 新增用户
新增的接口定义如下,要注意
- 响应体的接收,是一个包含完整用户信息的请求体.
- 在方法上标注@ResponseStatus(HttpStatus.CREATED),表示返回的状态码为201 Created。
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public UserDto save(
@RequestBody UserRequest request
) {
// 模拟保存用户到数据库,将request中的属性保存到存储中,返回保存后的用户
// user = service.save(request);
// 这里的user用了模拟保存后的用户
UserDto user = new UserDto();
user.setId(1L);
user.setName("user");
return user;
}
新增用户时,我们在请求体中传递完整用户信息。例如:
{
"name": "user1"
}
新增用户后,我们可以在响应体中返回新增的用户信息。例如:
{
"id": 1,
"name": "user1"
}
新增的请求和响应区别主要在于是否有id字段。请求体中没有id字段,而响应体中包含id字段。当然还可能包含其它的生成的信息,例如创建时间,更新时间等。
2.1.2 获取用户
获取用户的接口定义如下,要注意
- 路径变量中包含用户id.
- 返回的响应体是一个包含完整用户信息的对象.
@GetMapping("/{id}")
public UserDto get(
@PathVariable Long id
) {
// 模拟从数据库中获取用户
UserDto user = new UserDto();
user.setId(id);
user.setName("user" + id);
return user;
}
请求路径如下
GET /users/{id}
请求体包括完整的用户信息,例如:
{
"id": 1,
"name": "user1"
}
如果用户不存在,我们可以返回一个404 Not Found状态码,并在响应体中返回一个错误信息.例如:
{
"code": 404,
"message": "user not found",
"data": null
}
但我们的代码到当前为止,还不能正常处理这种情况,这种情况在后面会详细介绍.
2.1.3 修改用户
修改用户的接口定义如下,要注意
- 路径变量中包含用户id.
- 请求体中包含完整用户信息.
- 在方法上标注@ResponseStatus(HttpStatus.CREATED),表示返回的状态码为201 Created。
- 返回的响应体是一个包含完整用户信息的对象.
@PutMapping("/{id}")
@ResponseStatus(HttpStatus.CREATED)
public UserDto update(@PathVariable Long id, UserRequest request) {
// 模拟保存用户到数据库,将request中的属性保存到存储中,返回保存后的用户
// user = service.update(id,request);
// 这里的user用了模拟更新后的用户
UserDto user = new UserDto();
user.setId(1L);
user.setName("user");
return user;
}
请求路径如下:
PUT /users/{id}
请求体中包含完整用户信息。例如:
{
"name": "user1"
}
修改用户后,我们应该在响应体中返回修改后的用户信息。例如:
{
"id": 1,
"name": "user1"
}
2.1.4 删除用户
删除用户的接口定义如下,要注意
- 路径变量中包含用户id.
- 在方法上标注@ResponseStatus(HttpStatus.NO_CONTENT),表示返回的状态码为204 No Content。
- 返回的响应体是一个空对象.
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(
@PathVariable Long id
) {
// 模拟删除用户从数据库
// service.delete(id);
}
请求路径如下:
DELETE /users/{id}
请求体为空. 删除用户后,我们可以返回一个204 No Content状态码,表示删除成功.
2.1.5 分页查询用户列表
分页查询用户列表时,我们可以在URL中添加分页参数,例如:/users?page=0&size=10。其中,page表示当前页码,size表示每页数量。 但分页查询时,我们为了适配前端的分页相关的逻辑,通常会在响应体中包含分页相关的信息,例如总页数,总记录数等.例如:
{
"page": 0,
"size": 10,
"totalPages": 1,
"totalElements": 1,
"content": [
{
"id": 1,
"name": "user1"
}
]
}
这种方式不太符合RESTful风格,因为它将分页相关的信息放在了响应体中,而不是放在响应头中.如果我们要符合RESTful风格,我们可以将分页相关的信息放在响应头中.例如:
X-Total-Count: 1
X-Total-Pages: 1
但通常情况下,我们不会这么做.而是直接返回一个包括分页信息的响应体.而且spring为我们的分页请求和响应都提供了相应的对象封装,而且这些对象跟你使用的持久层框架无关. 他们分别是
- Pageable 用户接受分页参数
- Page 分页查询结果对象,在spring boot 3.3以前,我们通常使用Page对象来封装分页查询结果.
- PagedModel 分页查询结果封装对象,在spring 3.3以后,我们通常使用PagedModel对象来封装分页查询结果.
而要使用这些类也非常简单,只需要在你的项目中引入spring-data-commons依赖即可.
以下是一个Spring boot 3.3的分页查询示例
@GetMapping
public PagedModel<UserDto> find(
@RequestParam(required = false) String keyword,
Pageable pageable
) {
// 模拟从数据库中获取用户列表,查询方法通常返回一个page对象
// Page<UserDto> result=service.find(keyword, pageable);
Page<UserDto> result = new PageImpl<>(List.of(new UserDto(1L, "user")), pageable, 1L);
// 模拟从数据库中获取用户列表
return new PagedModel<>(result);
}
spring boot会自动解析分页参数,并将其转换为Pageable对象.我们不需要做任何额外的操作.如果你使用spring data 相关组件, 比如sping-data-jpa,spring-data-mongodb等,你可以直接在你的查询方法中使用Pageable参数,spring boot会自动将其转换为对应的分页查询. 如果你使用的是mybatis-plus等其它持久层框架,你也可以从pageable中获取分页参数,并手动构建分页查询.
2.2、如何处理异常
在实际开发中,我们需要考虑如何处理异常。包括要获取的对象不存在,请求参数错误,数据库操作异常等. 最常见的就是我们需要获取的对象不存在时,我们需要返回一个404 Not Found状态码.但我们的正常业务逻辑中,并不包括相关的代码. 这时,我们就可以借助全局异常处理机制,来实现相关的逻辑.
2.2.1 查询对象不存在时的异常处理
首先,我们需要定义一个自定义的异常类,例如: 注意标注@ResponseStatus(HttpStatus.NOT_FOUND),表示返回的状态码为404 Not Found。
@ResponseStatus(HttpStatus.NOT_FOUND)
public class ObjectNotFoundException extends RuntimeException {
@Serial
private static final long serialVersionUID = -2269660443026133324L;
public ObjectNotFoundException(String message) {
super(message);
}
}
当查询对象不存在时,我们可以抛出这个异常,例如:
@RequestMapping("/{id}")
public UserDto get(
@PathVariable Long id
) {
// 模拟从数据库中获取用户
if (id > 1) {
throw new ObjectNotFoundException("user not found");
} else {
UserDto user = new UserDto();
user.setId(1L);
user.setName("user" + id);
return user;
}
}
通常情况下,我们不需要做任何额外的操作.因为spring boot会自动将这个异常转换为一个404 Not Found状态码. 示例:
查询URL:
GET /users/2
响应状态码:
404 Not Found
响应体如下
{
"timestamp": "2025-11-13T15:43:18.265+00:00",
"status": 404,
"error": "Not Found",
"path": "/users/3"
}
默认情况下,错误的message不会被包含在响应体中,这时,我们可以通过配置来开启这个功能. 例如,在application.properties中添加以下配置:
server.error.include-message=always
修改该配置后,应体如下
{
"timestamp": "2025-11-13T15:43:18.265+00:00",
"status": 404,
"error": "Not Found",
"message": "user not found",
"path": "/users/3"
}
如果我们要进行精细的异常处理,可以定义多个异常处理器,分别处理不同的异常.如下:
- 将类ErrorControllerAdvice标注为@ControllerAdvice,表示这是一个全局异常处理器.
- 定义一个异常处理器方法,标注为@ExceptionHandler(ObjectNotFoundException.class),表示当抛出ObjectNotFoundException异常时,调用这个方法.
- 标注@ResponseStatus(HttpStatus.NOT_FOUND),表示返回的状态码为404 Not Found.
- 标注@ResponseBody,表示返回的响应体为一个Map对象.
- 返回一个Map对象,包含错误码,错误消息,以及其他自定义的信息.
- 定义一个全局异常处理器(这非常重要),因为它能给你的未处理的异常兜底.处理所有未被其他异常处理器捕获的异常.
@ControllerAdvice
public class ErrorControllerAdvice {
@ExceptionHandler(ObjectNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
@ResponseBody
public Map<String, Object> handleException(ObjectNotFoundException ex) {
// 记录日志
log.error("Object not found: {}", ex.getMessage(), ex);
return Map.of(
"code", HttpStatus.NOT_FOUND.value(),
"message", ex.getMessage(),
"other", "other value"
);
}
/**
* 定义一个全局异常处理器,处理所有未被其他异常处理器捕获的异常.
* @param ex 异常信息
*/
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ResponseBody
public Map<String, Object> handleException(Exception ex) {
log.error("Internal server error: {}", ex.getMessage(), ex);
return Map.of(
"code", HttpStatus.INTERNAL_SERVER_ERROR.value(),
"message", ex.getMessage(),
"other", "other value"
);
}
}
我们可以定义多个异常处理器,分别处理不同的异常。这样,在我们的业务代码中,只需要根据业务场景,抛出不同的异常即可实现错误的响应。 而不需要我们在业务代码中进行都多余的处理,增强代码可读性。
总结
- 不要自定义所谓的通用响应体,而是使用spring boot默认的响应体.因为它已经足够。
- 通过抛出异常来处理各种非正常情况。
- 可以通过定义全局异常处理器,处理所有未被其他异常处理器捕获的异常.