Spring Boot REST API 开发常见的 7 个“坑”,你都避开了吗?

Spring Boot 是当下开发 REST API 最火的 Java 框架之一。但是,我们在写代码的时候常常会犯一些错误,这些错误可能会影响 API 的质量、可维护性性能

我在这里列出了我们在 Spring Boot 开发中常犯的 7 个常见错误,以及如何避免它们

(我的文章对所有人免费开放。非 Medium 会员可以点这里免费阅读全文。)

1. HTTP 方法使用不当
这是我们在创建 API 时最常犯的错误之一。
REST API 应该遵循恰当的语义来保持代码的清晰度和一致性。

  • • 错误姿势:
    // 用 POST 来更新用户?语义不对
    @PostMapping("/users/{id}")
    public User updateUser(@PathVariable Long id, @RequestBody User user) {
        return userService.updateUser(id, user);
    }
    
    // 用 GET 来创建用户?大错特错!GET 不该有请求体,且应该是幂等的
    @GetMapping("/users/create")
    public User createUser(@RequestBody User user) {
        return userService.createUser(user);
    }
  • • 正确姿势:
    // 使用 PUT 更新(通常是全量替换)
    @PutMapping("/users/{id}")
    public User updateUser(@PathVariable Long id, @RequestBody User user) {
        return userService.updateUser(id, user);
    }
    
    // 使用 POST 创建新用户
    @PostMapping("/users")
    public User createUser(@RequestBody User user) {
        return userService.createUser(user);
    }
  • • HTTP 方法的正确用法:

    • • GET:用于获取数据。

    • • POST:用于创建新资源。

    • • PUT:用于更新(通常是替换)已存在的资源。

    • • DELETE:用于删除资源。

    • • PATCH:用于对资源进行部分更新

2. 异常处理不当
不恰当的异常处理,或者干脆不处理异常,会给公司和客户带来一堆麻烦。
不清晰的错误信息让调试问题变得异常困难,而且还可能暴露潜在的安全漏洞。

  • • 错误姿势:
    @GetMapping("/users/{id}")
    public User getUser(@PathVariable Long id) {
        try {
            return userService.getUser(id); // 假设 getUser 可能抛异常
        } catch (Exception e) {
            // 抓住异常后返回 null?这绝对是坏习惯!调用方无法区分是真没找到还是出错了。
            return null;
        }
    }
  • • 正确姿势: (使用全局异常处理器 @ControllerAdvice)
    // 全局异常处理类
    @ControllerAdvice
    publicclassGlobalExceptionHandlerextendsResponseEntityExceptionHandler {
    
        // 处理特定的业务异常,比如用户未找到
        @ExceptionHandler(UserNotFoundException.class)
        public ResponseEntity<ErrorResponse> handleUserNotFoundException(UserNotFoundException ex) {
            ErrorResponseerror=newErrorResponse(
                HttpStatus.NOT_FOUND.value(), // 404 状态码
                ex.getMessage(), // 使用异常中的消息
                LocalDateTime.now()
            );
            returnnewResponseEntity<>(error, HttpStatus.NOT_FOUND);
        }
    
        // 处理通用的校验异常
        @ExceptionHandler(ValidationException.class)// 假设有 ValidationException
        public ResponseEntity<ErrorResponse> handleValidationException(ValidationException ex) {
            ErrorResponseerror=newErrorResponse(
                HttpStatus.BAD_REQUEST.value(), // 400 状态码
                ex.getMessage(),
                LocalDateTime.now()
            );
            returnnewResponseEntity<>(error, HttpStatus.BAD_REQUEST);
        }
        // 可以继续添加处理其他类型异常的方法...
    }
    
    // 用于封装错误信息的 DTO
    @Getter
    @AllArgsConstructor
    publicclassErrorResponse {
        privateint status;
        private String message;
        private LocalDateTime timestamp;
    }
    (使用 @ControllerAdvice 可以集中处理异常,返回规范的错误响应,让 Controller 代码更干净)

3. 输入校验缺失或不足
不对用户的输入进行校验,可能导致数据损坏,甚至引发安全漏洞(如 SQL 注入、XSS 攻击等)。

  • • 错误姿势:
    // 直接接收 User 对象,没做任何校验
    @PostMapping("/users")
    public User createUser(@RequestBody User user) {
        return userService.createUser(user);
    }
    
    public class User {
        private String email; // email 格式对吗?
        private String password; // 密码强度够吗?
        private String phoneNumber; // 电话号码格式对吗?
        // ... (getter/setter)
    }
  • • 正确姿势: (使用 Jakarta Bean Validation 注解)
    @PostMapping("/users")
    // 在 @RequestBody 前加上 @Valid 注解,触发校验
    public User createUser(@Valid @RequestBody User user) {
        return userService.createUser(user);
    }
    
    publicclassUser {
        @Email(message = "邮箱格式不正确")// 校验邮箱格式
        @NotNull(message = "邮箱不能为空")// 不能为空
        private String email;
    
        @Size(min = 8, message = "密码长度至少需要 8 位")// 最小长度校验
        // 使用正则表达式校验密码复杂度
        @Pattern(regexp = "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=]).*$",
                message = "密码必须包含至少一个数字、一个小写字母、一个大写字母和一个特殊字符")
        private String password;
    
        // 使用正则表达式校验电话号码格式(示例,可能需要根据实际情况调整)
        @Pattern(regexp = "^\\+?[1-9]\\d{1,14}$", message = "无效的电话号码格式")
        private String phoneNumber;
        // ... (getter/setter)
    }
    (在 DTO 或 Entity 类上添加校验注解,并在 Controller 方法参数上使用 @Valid,可以方便地实现输入校验。如果校验失败,可以配合全局异常处理器返回 400 Bad Request 响应)

4. 命名规范不一致
混乱的命名(包括 URL 路径、方法名等)会让代码难以理解和使用。

  • • 错误姿势:
    @RestController
    public class UserController {
        // URL 动词开头?大小写混合?
        @GetMapping("/getUsers")
        public List<User> getUsers() { /* ... */ }
    
        // URL 里包含动词 "createNew"?
        @PostMapping("/createNewUser")
        public User createNewUser(@RequestBody User user) { /* ... */ }
    
        // URL 冗长,动词开头,路径变量名不规范
        @PutMapping("/updateUserDetails/{userId}")
        public User updateUserDetails(@PathVariable Long userId) { /* ... */ }
    }
  • • 正确姿势: (遵循 RESTful 风格)
    @RestController
    @RequestMapping("/api/v1/users")// 统一资源路径前缀,带版本号
    publicclassUserController {
        // GET /api/v1/users 获取用户列表
        @GetMapping
        public List<User> getUsers() { /* ... */ }
    
        // POST /api/v1/users 创建用户
        @PostMapping
        public User createUser(@RequestBody User user) { /* ... */ }
    
        // PUT /api/v1/users/{id} 更新指定 ID 的用户
        @PutMapping("/{id}")// 使用路径变量 id
        public User updateUser(@PathVariable Long id) { /* ... */ }
    
        // DELETE /api/v1/users/{id} 删除指定 ID 的用户
        // @DeleteMapping("/{id}")
        // ...
    }
    (推荐使用名词复数表示资源集合,HTTP 方法表示操作,URL 路径简洁明了)

5. 不实现分页查询
如果你的 API 可能返回大量数据(比如几千上万条用户列表),那么实现分页至关重要。不分页会导致严重的性能问题和糟糕的用户体验。

  • • 错误姿势:
    @GetMapping("/users")
    public List<User> getAllUsers() {
        // 一次性加载所有用户?如果用户量大,服务器和客户端都可能崩!
        return userRepository.findAll();
    }
  • • 正确姿势: (使用 Spring Data JPA 的分页功能)
    @GetMapping("/users")
    public Page<User> getUsers(
            // 接收分页参数,提供默认值
            @RequestParam(defaultValue = "0") int page,    // 页码,从 0 开始
            @RequestParam(defaultValue = "20") int size,   // 每页大小
            @RequestParam(defaultValue = "id") String sortBy // 排序字段
    ) {
        // 创建 Pageable 对象
        Pageablepageable= PageRequest.of(page, size, Sort.by(sortBy));
        // 调用支持分页的查询方法
        return userRepository.findAll(pageable);
    }
    
    // Repository 需要继承 PagingAndSortingRepository 或 JpaRepository
    publicinterfaceUserRepositoryextendsPagingAndSortingRepository<User, Long> {
        // 也可以定义自己的分页查询方法
        Page<User> findByLastName(String lastName, Pageable pageable);
    }
    (返回 Page<User> 对象,它不仅包含当前页的数据,还包含总页数、总记录数等分页信息)

6. 暴露敏感信息
在代码中,我们经常需要在日志中记录数据,或者将数据序列化后通过 API 返回。在这些场景下,必须隐藏用户的敏感信息(如密码、身份证号等),以防安全泄露。

  • • 错误姿势:
    @Entity
    public class User {
        private Long id;
        private String username;
        private String password;  // 密码直接暴露在 API 响应中!非常危险!
        private String ssn;       // 社会安全号码也暴露了!
        // Getters and setters
    }
    // Controller 直接返回 User 实体
  • • 正确姿势: (使用 @JsonIgnore 或 DTO)
    @Entity
    publicclassUser {
        private Long id;
        private String username;
    
        @JsonIgnore// Jackson 注解,序列化时忽略此字段
        private String password;
    
        @JsonIgnore// 同样忽略 SSN
        private String ssn;
    
        // Getters and setters
    }
    
    // 或者(更推荐)使用 DTO 来控制 API 返回的数据结构
    @Data// Lombok 注解简化代码
    publicclassUserDTO {
        private Long id;
        private String username;
        private LocalDateTime createdAt; // 可以选择性地暴露一些非敏感信息
    
        // 提供一个静态工厂方法或使用 MapStruct 等工具进行转换
        publicstatic UserDTO fromEntity(User user) {
            UserDTOdto=newUserDTO();
            dto.setId(user.getId());
            dto.setUsername(user.getUsername());
            // dto.setCreatedAt(user.getCreatedAt()); // 假设 User 实体有 createdAt 字段
            return dto;
        }
    }
    // Controller 方法返回 UserDTO 而不是 User 实体
    (优先推荐使用 DTO 模式,更灵活地控制输入输出的数据结构,同时避免暴露内部实体细节)

7. 响应状态码使用不当
错误地使用 HTTP 响应状态码是一个非常普遍的问题。这会让你的 API 难以理解,给调用方带来困扰。

  • • 错误姿势:
    @PostMapping("/users")
    public User createUser(@RequestBody User user) {
        // 创建成功应该返回 201 Created,而不是默认的 200 OK
        return userService.createUser(user);
    }
    
    @GetMapping("/users/{id}")
    public User getUser(@PathVariable Long id) {
        Useruser= userService.findById(id);
        if (user == null) {
            // 找不到用户时,返回一个空对象?调用方怎么知道是没找到?应该返回 404 Not Found
            returnnewUser();
        }
        return user;
    }
  • • 正确姿势: (使用 ResponseEntity 控制状态码)
    @PostMapping("/users")
    public ResponseEntity<User> createUser(@RequestBody User user) {
        UsercreatedUser= userService.createUser(user);
        // 创建成功,返回 201 Created 状态码和创建的用户信息
        returnnewResponseEntity<>(createdUser, HttpStatus.CREATED);
    }
    
    @GetMapping("/users/{id}")
    public ResponseEntity<User> getUser(@PathVariable Long id) {
        // 假设 service.findById 返回 Optional<User>
        return userService.findById(id)
                .map(user -> ResponseEntity.ok(user)) // 如果找到,返回 200 OK 和用户信息
                .orElse(ResponseEntity.notFound().build()); // 如果没找到,返回 404 Not Found
    }
    (使用 ResponseEntity 可以精确地控制返回的 HTTP 状态码、响应头和响应体)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值