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 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 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)
}
@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 getUsers() { /* ... */ }
// URL 里包含动词 "createNew"?
@PostMapping("/createNewUser")
public User createNewUser(@RequestBody User user) { /* ... */ }
// URL 冗长,动词开头,路径变量名不规范
@PutMapping("/updateUserDetails/{userId}")
public User updateUserDetails(@PathVariable Long userId) { /* ... */ }
}
@RestController
@RequestMapping("/api/v1/users")// 统一资源路径前缀,带版本号
publicclassUserController {
// GET /api/v1/users 获取用户列表
@GetMapping
public List 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 getAllUsers() {
// 一次性加载所有用户?如果用户量大,服务器和客户端都可能崩!
return userRepository.findAll();
}
@GetMapping("/users")
public Page 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 {
// 也可以定义自己的分页查询方法
Page findByLastName(String lastName, Pageable pageable);
}
(返回 Page
对象,它不仅包含当前页的数据,还包含总页数、总记录数等分页信息)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 createUser(@RequestBody User user) {
UsercreatedUser= userService.createUser(user);
// 创建成功,返回 201 Created 状态码和创建的用户信息
returnnewResponseEntity<>(createdUser, HttpStatus.CREATED);
}
@GetMapping("/users/{id}")
public ResponseEntity getUser(@PathVariable Long id) {
// 假设 service.findById 返回 Optional
return userService.findById(id)
.map(user -> ResponseEntity.ok(user)) // 如果找到,返回 200 OK 和用户信息
.orElse(ResponseEntity.notFound().build()); // 如果没找到,返回 404 Not Found
}
(使用 ResponseEntity
可以精确地控制返回的 HTTP 状态码、响应头和响应体)