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 注解)
(在 DTO 或 Entity 类上添加校验注解,并在 Controller 方法参数上使用@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) }
@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 风格)
(推荐使用名词复数表示资源集合,HTTP 方法表示操作,URL 路径简洁明了)@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}") // ... }
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)
(优先推荐使用 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 实体
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 状态码、响应头和响应体)