Skip to content

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)

适用于增删改等不需要返回数据的操作。

java
@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"
    }
}

响应示例

json
{
  "errorCode": "00000",
  "errorMessage": "OK",
  "userTip": "删除成功"
}

场景2:返回单个对象(SingleVO)

适用于查询详情、创建后返回实体等场景。

java
@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);
    }
}

响应示例

json
{
  "errorCode": "00000",
  "errorMessage": "OK",
  "userTip": "创建成功",
  "data": {
    "id": 1001,
    "username": "张三",
    "email": "zhangsan@example.com"
  }
}

场景3:返回列表数据(MultiVO)

适用于查询列表、批量操作返回等无分页场景。

java
@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);
    }
}

响应示例

json
{
  "errorCode": "00000",
  "errorMessage": "OK",
  "userTip": "查询成功",
  "data": [
    {
      "id": 1,
      "username": "张三"
    },
    {
      "id": 2,
      "username": "李四"
    }
  ]
}

场景4:分页查询(PageVO)

分页查询是最常见的场景,框架提供了完整的分页支持。

步骤1:定义分页查询参数

java
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

java
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

java
@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=技术部

响应示例

json
{
  "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>,适用于需要去重的场景
java
// 使用 ListPage(推荐)
Pageable<List<UserDTO>> listPage = new ListPage<>(query, totalCount, userList);

// 使用 SetPage(去重场景)
Pageable<Set<UserDTO>> setPage = new SetPage<>(query, totalCount, userSet);

分页查询参数(PageQuery)

PageQuery 是框架提供的分页查询基类,包含:

java
public class PageQuery {
    private Long pageNo = 1L;        // 页码,默认第 1 页
    private Long pageSize = 10L;     // 每页数量,默认 10 条
    // ... getter/setter
}

业务查询对象继承 PageQuery 即可获得分页能力:

java
@Data
public class OrderPageQuery extends PageQuery {
    private Long userId;
    private String status;
    private LocalDateTime startTime;
    private LocalDateTime endTime;
}

自定义响应码

除了标准的成功响应,还可以使用自定义错误码:

java
@GetMapping("/check")
public BaseVO checkInventory(@RequestParam Long productId) {
    Product product = productService.getById(productId);
    
    if (product.getStock() < 10) {
        // 自定义警告码
        return R.base("W0001", "库存预警", "库存不足,请及时补货");
    }
    
    return R.ok("库存充足");
}

响应示例

json
{
  "errorCode": "W0001",
  "errorMessage": "库存预警",
  "userTip": "库存不足,请及时补货"
}

与 MyBatis Plus 集成

如果使用 krismile-boot3-starter-mybatisplus 模块,可以更简洁地实现分页:

java
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 反序列化器实现

快速开始

java
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;  // 密码不处理(可能包含有意义的空格)
}

请求示例

json
{
  "username": "  zhangsan  ",
  "email": "  zhangsan@example.com  ",
  "password": "  mypassword  "
}

处理结果

java
userRegisterRequest.getUsername();  // "zhangsan" (已清理)
userRegisterRequest.getEmail();     // "zhangsan@example.com" (已清理)
userRegisterRequest.getPassword();  // "  mypassword  " (保留原样)

常见使用场景

  1. 用户输入字段:用户名、邮箱、手机号等
  2. 搜索关键词:清理搜索框输入的多余空格
  3. 数据导入:清理 Excel/CSV 中的冗余空格
  4. 与 Bean Validation 组合:避免"空格字符串"通过 @NotBlank 校验

注意事项

  • ⚠️ 只处理首尾空格:字符串中间的空格会保留
  • ⚠️ 只对 JSON 请求体生效:URL 查询参数需要手动 trim()
  • ⚠️ 只支持 String 类型:标记在其他类型字段上不会生效
  • ⚠️ 密码字段不推荐使用:密码可能包含有意义的空格

最佳实践

✅ 统一响应封装推荐做法

  1. 统一使用 R 工具类返回

    java
    return R.okSingle(user);      // ✅ 推荐
  2. 为关键操作添加 userTip

    java
    return R.ok("保存成功");      // ✅ 用户友好
  3. 分页查询继承 PageQuery

    java
    public class OrderPageQuery extends PageQuery { }  // ✅ 规范
  4. Service 层返回 Pageable

    java
    public Pageable<List<UserDTO>> pageUsers(UserPageQuery query);  // ✅ 清晰

❌ 不推荐做法

  1. ❌ 直接返回实体对象

    java
    return user;  // ❌ 不规范,缺少错误码和提示
  2. ❌ 手动拼装响应对象

    java
    Map<String, Object> result = new HashMap<>();
    result.put("code", "00000");
    result.put("data", user);
    return result;  // ❌ 不规范,不统一
  3. ❌ 分页直接返回 List

    java
    return 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 只去除首尾空白字符,中间的空格会保留:

java
// 输入: "  hello   world  "
// 输出: "hello   world"

Q: 为什么 URL 查询参数不会被清理?

A: @TrimWhiteSpace 基于 Jackson 反序列化器实现,只对 JSON 请求体生效。URL 查询参数绑定使用的是 Spring MVC 的参数解析器,不经过 Jackson。

解决方案:手动清理查询参数

java
@GetMapping("/search")
public MultiVO<ProductDTO> search(@RequestParam String keyword) {
    String trimmedKeyword = keyword != null ? keyword.trim() : null;
    // ...
}

Q: @TrimWhiteSpace 与 @NotBlank 的执行顺序?

A: @TrimWhiteSpace 先执行,执行顺序为:

  1. JSON 反序列化(@TrimWhiteSpace 生效)
  2. Bean Validation 校验(@NotBlank@Size 等)
  3. 进入 Controller 方法

这意味着 @NotBlank 校验的是清理后的字符串。

Q: 为什么密码字段不推荐使用 @TrimWhiteSpace?

A: 密码可能包含有意义的空格(用户有意设置的强密码策略)。清理空格可能导致:

  • 用户设置的密码是 " mypass "
  • 存储时被清理为 "mypass"
  • 用户下次登录输入 " mypass " 时登录失败

推荐做法:密码字段不使用 @TrimWhiteSpace,保持用户输入的原样。

扩展阅读

Released under the Apache-2.0 License