Controller 开发指南
框架为 Controller 层提供了完整的开发支持,核心包括:
- ✅ 统一响应封装:
R+ VO 体系,规范化 API 返回格式 - ✅ 分页支持:
Pageable接口 +ListPage/SetPage实现 - ✅ 全局异常处理:自动捕获并转换为统一响应(详见异常处理)
- ✅ 请求日志 AOP:自动记录请求信息(详见 AOP与日志)
- ✅ 参数校验:Bean Validation 自动校验(详见异常处理)
- ✅ 字符串空格自动清理:
@TrimWhiteSpace注解自动去除首尾空白
本文档侧重介绍 统一响应封装(R 与 VO) 和 字符串空格处理 的使用方式。
统一响应封装
核心类概览
框架提供了 R 工具类配合多种 VO 类型,实现统一的响应格式:
| VO 类型 | 适用场景 | 包含字段 |
|---|---|---|
BaseVO | 无数据返回(如操作成功提示) | errorCode, errorMessage, userTip |
SingleVO<T> | 单个对象返回 | 继承 BaseVO + data (单个对象) |
MultiVO<T> | 多条数据返回(无分页) | 继承 BaseVO + data (集合) |
PageVO<T> | 分页数据返回 | 继承 BaseVO + detail (分页信息) + data (集合) |
所有 VO 类型统一包含:
- errorCode:错误码(成功时为
"00000") - errorMessage:错误信息(成功时为
"OK") - userTip:用户友好提示(可选,默认
"OK")
使用场景与示例
场景1:操作成功提示(BaseVO)
适用于增删改等不需要返回数据的操作。
@RestController
@RequestMapping("/user")
public class UserController {
@PostMapping("/delete")
public BaseVO deleteUser(@RequestParam Long userId) {
userService.deleteById(userId);
return R.ok("删除成功");
}
@PostMapping("/update")
public BaseVO updateUser(@RequestBody UpdateUserRequest request) {
userService.update(request);
return R.ok(); // 默认返回 "OK"
}
}响应示例:
{
"errorCode": "00000",
"errorMessage": "OK",
"userTip": "删除成功"
}场景2:返回单个对象(SingleVO)
适用于查询详情、创建后返回实体等场景。
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("/{id}")
public SingleVO<UserDTO> getUserById(@PathVariable Long id) {
UserDTO user = userService.getById(id);
return R.okSingle(user);
}
@PostMapping("/create")
public SingleVO<UserDTO> createUser(@RequestBody CreateUserRequest request) {
UserDTO user = userService.create(request);
return R.okSingle("创建成功", user);
}
}响应示例:
{
"errorCode": "00000",
"errorMessage": "OK",
"userTip": "创建成功",
"data": {
"id": 1001,
"username": "张三",
"email": "zhangsan@example.com"
}
}场景3:返回列表数据(MultiVO)
适用于查询列表、批量操作返回等无分页场景。
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("/list")
public MultiVO<UserDTO> getUserList(@RequestParam String department) {
List<UserDTO> users = userService.listByDepartment(department);
return R.okMulti(users);
}
@GetMapping("/active")
public MultiVO<UserDTO> getActiveUsers() {
List<UserDTO> activeUsers = userService.listActive();
return R.okMulti("查询成功", activeUsers);
}
}响应示例:
{
"errorCode": "00000",
"errorMessage": "OK",
"userTip": "查询成功",
"data": [
{
"id": 1,
"username": "张三"
},
{
"id": 2,
"username": "李四"
}
]
}场景4:分页查询(PageVO)
分页查询是最常见的场景,框架提供了完整的分页支持。
步骤1:定义分页查询参数
import host.springboot.framework3.core.page.query.PageQuery;
@Data
public class UserPageQuery extends PageQuery {
private String keyword; // 关键词搜索
private String department; // 部门筛选
private Integer status; // 状态筛选
}步骤2:Service 层返回 Pageable
import host.springboot.framework3.core.page.ListPage;
import host.springboot.framework3.core.page.Pageable;
@Service
public class UserService {
public Pageable<List<UserDTO>> pageUsers(UserPageQuery query) {
// 使用 MyBatis Plus 或其他 ORM 查询
long totalCount = userMapper.selectCount(buildQueryWrapper(query));
List<UserDTO> records = userMapper.selectPage(query, buildQueryWrapper(query));
// 封装为 ListPage
return new ListPage<>(query, totalCount, records);
}
}步骤3:Controller 返回 PageVO
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("/page")
public PageVO<UserDTO> pageUsers(UserPageQuery query) {
Pageable<List<UserDTO>> page = userService.pageUsers(query);
return R.okPage(page);
}
}请求示例:
GET /user/page?pageNo=1&pageSize=10&keyword=张&department=技术部响应示例:
{
"errorCode": "00000",
"errorMessage": "OK",
"userTip": "OK",
"detail": {
"pageNo": 1,
"pageSize": 10,
"totalCount": 25
},
"data": [
{
"id": 1,
"username": "张三",
"department": "技术部"
},
{
"id": 2,
"username": "张四",
"department": "技术部"
}
]
}分页详解
Pageable 接口
Pageable 是框架的标准分页接口,提供了丰富的分页信息:
| 方法 | 说明 | 示例 |
|---|---|---|
getPageNo() | 当前页码 | 1 |
getPageSize() | 每页数量 | 10 |
getTotalCount() | 数据总条数 | 25 |
getTotalPage() | 总页数 | 3 |
isFirstPage() | 是否第一页 | true |
isLastPage() | 是否最后一页 | false |
getNextPage() | 下一页页码 | 2 |
getPrePage() | 上一页页码 | 0 |
getRecords() | 分页数据集合 | List/Set |
getOffset() | 数据库查询偏移量 | 0 |
ListPage vs SetPage
框架提供了两种分页实现:
ListPage<T>:分页数据为List<T>,适用于大多数场景SetPage<T>:分页数据为Set<T>,适用于需要去重的场景
// 使用 ListPage(推荐)
Pageable<List<UserDTO>> listPage = new ListPage<>(query, totalCount, userList);
// 使用 SetPage(去重场景)
Pageable<Set<UserDTO>> setPage = new SetPage<>(query, totalCount, userSet);分页查询参数(PageQuery)
PageQuery 是框架提供的分页查询基类,包含:
public class PageQuery {
private Long pageNo = 1L; // 页码,默认第 1 页
private Long pageSize = 10L; // 每页数量,默认 10 条
// ... getter/setter
}业务查询对象继承 PageQuery 即可获得分页能力:
@Data
public class OrderPageQuery extends PageQuery {
private Long userId;
private String status;
private LocalDateTime startTime;
private LocalDateTime endTime;
}自定义响应码
除了标准的成功响应,还可以使用自定义错误码:
@GetMapping("/check")
public BaseVO checkInventory(@RequestParam Long productId) {
Product product = productService.getById(productId);
if (product.getStock() < 10) {
// 自定义警告码
return R.base("W0001", "库存预警", "库存不足,请及时补货");
}
return R.ok("库存充足");
}响应示例:
{
"errorCode": "W0001",
"errorMessage": "库存预警",
"userTip": "库存不足,请及时补货"
}与 MyBatis Plus 集成
如果使用 krismile-boot3-starter-mybatisplus 模块,可以更简洁地实现分页:
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@Service
public class UserService extends BaseServiceImpl<UserMapper, User> {
public Pageable<List<UserDTO>> pageUsers(UserPageQuery query) {
// MyBatis Plus 分页对象
Page<User> page = new Page<>(query.getPageNo(), query.getPageSize());
// 执行分页查询
IPage<User> result = this.page(page, buildQueryWrapper(query));
// 转换为 DTO
List<UserDTO> dtoList = result.getRecords().stream()
.map(this::convertToDTO)
.toList();
// 封装为框架的 ListPage
return new ListPage<>(query, result.getTotal(), dtoList);
}
}详细的 MyBatis Plus 集成说明,请参考 MyBatis-Plus集成。
字符串空格自动清理
@TrimWhiteSpace 注解
@TrimWhiteSpace 是框架提供的字符串空白字符自动清理注解,在 JSON 反序列化时自动去除字符串字段的前后空白字符。
核心特性:
- 自动清理字符串首尾空格,无需手动调用
trim() - 只处理标记了
@TrimWhiteSpace的字段 - 只对
String类型字段生效 - 基于 Jackson 反序列化器实现
快速开始
import host.springboot.framework.context.mvc.annotation.TrimWhiteSpace;
@Data
public class UserRegisterRequest {
@TrimWhiteSpace
@NotBlank(message = "用户名不能为空")
private String username; // 自动去除首尾空格
@TrimWhiteSpace
@Email(message = "邮箱格式不正确")
private String email; // 自动去除首尾空格
@NotBlank(message = "密码不能为空")
private String password; // 密码不处理(可能包含有意义的空格)
}请求示例:
{
"username": " zhangsan ",
"email": " zhangsan@example.com ",
"password": " mypassword "
}处理结果:
userRegisterRequest.getUsername(); // "zhangsan" (已清理)
userRegisterRequest.getEmail(); // "zhangsan@example.com" (已清理)
userRegisterRequest.getPassword(); // " mypassword " (保留原样)常见使用场景
- 用户输入字段:用户名、邮箱、手机号等
- 搜索关键词:清理搜索框输入的多余空格
- 数据导入:清理 Excel/CSV 中的冗余空格
- 与 Bean Validation 组合:避免"空格字符串"通过
@NotBlank校验
注意事项
- ⚠️ 只处理首尾空格:字符串中间的空格会保留
- ⚠️ 只对 JSON 请求体生效:URL 查询参数需要手动
trim() - ⚠️ 只支持 String 类型:标记在其他类型字段上不会生效
- ⚠️ 密码字段不推荐使用:密码可能包含有意义的空格
最佳实践
✅ 统一响应封装推荐做法
统一使用 R 工具类返回
javareturn R.okSingle(user); // ✅ 推荐为关键操作添加 userTip
javareturn R.ok("保存成功"); // ✅ 用户友好分页查询继承 PageQuery
javapublic class OrderPageQuery extends PageQuery { } // ✅ 规范Service 层返回 Pageable
javapublic Pageable<List<UserDTO>> pageUsers(UserPageQuery query); // ✅ 清晰
❌ 不推荐做法
❌ 直接返回实体对象
javareturn user; // ❌ 不规范,缺少错误码和提示❌ 手动拼装响应对象
javaMap<String, Object> result = new HashMap<>(); result.put("code", "00000"); result.put("data", user); return result; // ❌ 不规范,不统一❌ 分页直接返回 List
javareturn userList; // ❌ 缺少分页信息
✅ @TrimWhiteSpace 推荐做法
- 用户输入字段(用户名、邮箱、手机号)
- 搜索和查询字段
- 数据导入场景(清理 Excel/CSV 数据)
- 与
@NotBlank、@Size等校验注解组合使用
❌ @TrimWhiteSpace 不推荐做法
- 密码字段(可能包含有意义的空格)
- 富文本/代码/脚本字段(可能破坏格式)
- 非 String 类型字段(不会生效)
常见问题 FAQ
Q: BaseVO、SingleVO、MultiVO、PageVO 如何选择?
A:
- 无数据返回(增删改)→
BaseVO - 单个对象(详情查询、创建返回)→
SingleVO<T> - 列表数据无分页(全量查询、下拉选项)→
MultiVO<T> - 列表数据有分页(列表查询)→
PageVO<T>
Q: 分页数据总条数必须传吗?
A: 不是必须,但强烈建议传入 totalCount。如果不传,前端无法计算总页数,影响分页组件显示。
Q: 可以自定义 VO 类型吗?
A: 可以,但不推荐。框架提供的 VO 体系已覆盖绝大多数场景,自定义可能导致响应格式不统一。如有特殊需求,建议在 data 字段中封装自定义结构。
Q: 如何处理异常响应?
A: 异常响应由全局异常处理器自动处理,开发者只需抛出 ApplicationException 即可,详见异常处理。
Q: @TrimWhiteSpace 会去除字符串中间的空格吗?
A: 不会。@TrimWhiteSpace 只去除首尾空白字符,中间的空格会保留:
// 输入: " hello world "
// 输出: "hello world"Q: 为什么 URL 查询参数不会被清理?
A: @TrimWhiteSpace 基于 Jackson 反序列化器实现,只对 JSON 请求体生效。URL 查询参数绑定使用的是 Spring MVC 的参数解析器,不经过 Jackson。
解决方案:手动清理查询参数
@GetMapping("/search")
public MultiVO<ProductDTO> search(@RequestParam String keyword) {
String trimmedKeyword = keyword != null ? keyword.trim() : null;
// ...
}Q: @TrimWhiteSpace 与 @NotBlank 的执行顺序?
A: @TrimWhiteSpace 先执行,执行顺序为:
- JSON 反序列化(
@TrimWhiteSpace生效) - Bean Validation 校验(
@NotBlank、@Size等) - 进入 Controller 方法
这意味着 @NotBlank 校验的是清理后的字符串。
Q: 为什么密码字段不推荐使用 @TrimWhiteSpace?
A: 密码可能包含有意义的空格(用户有意设置的强密码策略)。清理空格可能导致:
- 用户设置的密码是
" mypass " - 存储时被清理为
"mypass" - 用户下次登录输入
" mypass "时登录失败
推荐做法:密码字段不使用 @TrimWhiteSpace,保持用户输入的原样。
扩展阅读
- 异常处理:了解全局异常处理机制
- AOP与日志:了解请求日志自动记录
- MyBatis-Plus集成:了解 ORM 层最佳实践
- 参数校验:了解 Bean Validation 集成
- Jackson序列化配置:了解更多序列化定制选项
