Spring Boot 让 Java 开发变得更快、更简单、也更整洁。但即使是经验丰富的开发者,也常常会犯一些错误,这些错误会导致性能瓶瓶颈、Bug 和安全问题。
让我们来探讨一下最常见的 Spring Boot 错误,通过完整的代码示例,学习如何像专家一样避免它们。🧑💻
1. ❌ 未正确使用 @Service
, @Component
或 @Repository
注解
- 👎 糟糕的代码:
// 这个类没有被标记为 Spring 的组件 public class UserService { public String getUserById(Long id) { return "用户: " + id; } }
上面这个类不会被 Spring 扫描到并注册为一个 Bean!因此,你无法在其他地方注入它。
- ✅ 良好的代码:
import org.springframework.stereotype.Service; @Service // 标记为服务层 Bean public class UserService { public String getUserById(Long id) { return "用户: " + id; } } // 在 Controller 中注入并使用 UserService import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; @RestController public class UserController { @Autowired // 注入 UserService private UserService userService; @GetMapping("/user/{id}") public String getUser(@PathVariable Long id) { return userService.getUserById(id); } }
2. ❌
application.properties
配置错误 - 👎 糟糕的配置:
# 同一个键被定义了两次 server.port=8080 server.port=9090
Spring 只会使用最后一个值,这可能导致你的应用运行在错误的端口上,或者产生非预期的行为。
- ✅ 正确的配置:
server.port=8081 spring.datasource.url=jdbc:mysql://localhost:3306/userdb spring.datasource.username=root spring.datasource.password=admin
3. ❌ 编写臃肿的“胖控制器” (Fat Controllers)
- 👎 糟糕的代码:
@RestController public class ProductController { @PostMapping("/product") public String addProduct(@RequestBody Product product) { // 大量的业务逻辑直接写在 Controller 里 System.out.println("正在保存产品: " + product.getName()); // ... 可能还有数据库操作、校验、日志等 ... return "产品已保存!"; } }
将业务逻辑放在 Controller 中违反了分层架构的原则,使得代码难以测试和维护。
- ✅ 良好的代码:
服务层 (Service Layer):
import org.springframework.stereotype.Service; @Service public class ProductService { public String addProduct(Product product) { // 将保存逻辑移到 Service 层 System.out.println("正在保存产品: " + product.getName()); // ... 数据库操作等 ... return "产品已保存!"; } }
控制器 (Controller):
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; @RestController public class ProductController { @Autowired private ProductService productService; // 注入 Service @PostMapping("/product") public String addProduct(@RequestBody Product product) { // Controller 只负责接收请求并委托给 Service 处理 return productService.addProduct(product); } }
4. ❌ 不处理异常 (No Exception Handling)
- 👎 糟糕的代码:
@GetMapping("/users/{id}") public User getUser(@PathVariable Long id) { // 如果 findById 返回空 Optional,调用 .get() 会抛出 NoSuchElementException // 这个异常没有被处理,会导致向客户端返回一个不友好的 500 错误 return userRepository.findById(id).get(); }
- ✅ 良好的代码: (使用全局异常处理器)
import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import java.util.NoSuchElementException; @ControllerAdvice // 声明为全局异常处理器 public class GlobalExceptionHandler { @ExceptionHandler(NoSuchElementException.class) // 只处理 NoSuchElementException public ResponseEntity<String> handleNoSuchElement(NoSuchElementException ex) { // 返回一个带有 404 NOT_FOUND 状态码和友好消息的响应 return new ResponseEntity<>("用户未找到", HttpStatus.NOT_FOUND); } // 可以添加更多针对不同异常的处理器... }
5. ❌ 暴露实体类 (Entity) 而不是使用 DTO (数据传输对象)
- 👎 糟糕的代码:
@GetMapping("/users") public List<User> getUsers() { // 直接返回 JPA 实体列表 // 这会暴露数据库模型,包括可能存在的敏感字段(如密码哈希) return userRepository.findAll(); }
- ✅ 良好的代码:
DTO (Data Transfer Object):
// UserDTO 只包含需要暴露给客户端的字段 public class UserDTO { private String name; private String email; // 构造函数, getters public UserDTO(String name, String email) { this.name = name; this.email = email; } // ... }
Service:
import java.util.stream.Collectors; // ... @Service public class UserService { @Autowired private UserRepository userRepository; public List<UserDTO> getAllUserDTOs() { // 将实体列表映射为 DTO 列表 return userRepository.findAll() .stream() .map(user -> new UserDTO(user.getName(), user.getEmail())) .collect(Collectors.toList()); } }
Controller:
@GetMapping("/users") public List<UserDTO> getUsers() { // 返回 DTO 列表,而不是实体列表 return userService.getAllUserDTOs(); }
6. ❌ (如果不使用 Spring Data) 不关闭资源
- ✅ 正确的 JDBC 资源管理示例:
在不使用像 Spring Data JPA 这样能自动管理资源的框架,而直接使用原生 JDBC 时,必须确保资源(如
Connection
,Statement
,ResultSet
)被正确关闭,即使发生异常也要关闭。try-with-resources 是最佳实践。import javax.sql.DataSource; import java.sql.*; // ... @Autowired private DataSource dataSource; // 假设注入了数据源 public void fetchData() { // 使用 try-with-resources 语句 try (Connection conn = dataSource.getConnection(); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("SELECT * FROM users")) { while (rs.next()) { System.out.println(rs.getString("name")); } // conn, stmt, rs 会在这里被自动关闭 } catch (SQLException e) { // 处理异常 e.printStackTrace(); } }
7. ❌ 对输入数据不进行校验
- ✅ 带校验的 DTO:
不校验用户输入会带来安全风险和数据不一致问题。应始终使用校验注解。
import jakarta.validation.constraints.*; // 或 javax.validation.constraints.* public class RegisterRequest { @NotBlank // 不能为空白字符串 private String username; @Email // 必须是合法的邮箱格式 private String email; @Size(min = 6) // 长度至少为 6 private String password; // getters and setters }
Controller:
import jakarta.validation.Valid; // ... @PostMapping("/register") public ResponseEntity<String> register(@Valid @RequestBody RegisterRequest request) { // @Valid 注解会触发对 RegisterRequest 对象的校验 // 如果校验失败,Spring Boot 会抛出 MethodArgumentNotValidException, // 可以通过全局异常处理器捕获并返回 400 Bad Request return ResponseEntity.ok("用户注册成功!"); }
8. ❌ 安全配置不当
- ✅ 使用角色保护 API 安全:
不配置安全策略的 API 等于在公网上“裸奔”。应使用 Spring Security 保护你的端点。
import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { // 注意: 在新版Spring Security中,推荐使用SecurityFilterChain Bean的方式 @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() // 根据需要禁用CSRF .authorizeRequests() .antMatchers("/admin/**").hasRole("ADMIN") // /admin/** 路径需要 ADMIN 角色 .antMatchers("/api/**").authenticated() // /api/** 路径需要认证 .anyRequest().permitAll() // 其他请求允许所有访问 .and() .formLogin(); // 启用表单登录 } }
9. ❌ 没有为开发/测试/生产环境配置 Profiles
- ✅ 正确用法:
使用 Profile 特定的配置文件来管理不同环境的配置。
application.properties
(主配置文件)# 激活 dev profile spring.profiles.active=dev
application-dev.properties
(开发环境配置)server.port=8080 logging.level.root=DEBUG
application-prod.properties
(生产环境配置)server.port=80 logging.level.root=ERROR
10. ❌ 不使用缓存
- ✅ 使用缓存:
对于读取频繁且不经常变化的数据,缓存是提升性能的关键。
Service:
import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import java.util.List; @Service public class BookService { @Cacheable("books") // 结果会被缓存到名为 "books" 的缓存中 public List<Book> getBooks() { // 只有当缓存中没有数据时,这个方法体才会执行 simulateDelay(); // 模拟耗时的数据库查询 return List.of(new Book("Java 101"), new Book("Spring Boot Mastery")); } private void simulateDelay() { try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } }
主启动类:
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cache.annotation.EnableCaching; @SpringBootApplication @EnableCaching // 启用缓存功能 public class AppStarter { public static void main(String[] args) { SpringApplication.run(AppStarter.class, args); } }
11. ❌ 不正确地使用
@Transactional
- ✅ 正确用法:
@Transactional
注解依赖于 Spring AOP 代理。不要在私有 (private) 方法上使用它——它不会生效! 因为代理无法拦截私有方法的调用。应将其用在公共 (public) 方法上。import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class OrderService { @Autowired private OrderRepository orderRepository; @Transactional // 用在 public 方法上 public void placeOrder(Order order) { orderRepository.save(order); // ...其他数据库操作... } }
12. ❌ 不记录日志(或记录不当)
- ✅ 使用 SLF4J:
在生产环境中,
System.out.println
是不够的。应该使用专业的日志框架。import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; @Service public class LogService { private static final Logger logger = LoggerFactory.getLogger(LogService.class); public void logExample() { // 使用参数化日志,比字符串拼接更高效、更安全 logger.info("应用程序已启动!用户ID: {}", userId); logger.error("处理订单 {} 时发生错误", orderId, exception); } }
13. ❌ 不使用分页获取大量数据
- ✅ 使用分页:
一次性从数据库查询成千上万条记录会导致性能问题和内存溢出。
import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; // ... 在 Controller 中 ... // @Autowired private UserRepository userRepository; @GetMapping("/users") public Page<User> getUsers(@RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int size) { // 使用 PageRequest 创建分页请求 return userRepository.findAll(PageRequest.of(page, size)); } // UserRepository 需要继承 JpaRepository 或 PagingAndSortingRepository
14. ❌ 不进行单元或集成测试
- ✅ 单元测试示例 (JUnit 5 + Mockito):
不写测试的代码是不可靠的。
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import static org.mockito.Mockito.*; import static org.junit.jupiter.api.Assertions.*; @ExtendWith(MockitoExtension.class) // 启用 Mockito 扩展 public class UserServiceTest { @Mock // 创建一个 Mock 对象 private UserRepository userRepository; @InjectMocks // 创建一个 UserService 实例,并将 @Mock 对象注入其中 private UserService userService; @Test void testSaveUser() { // Arrange (准备) User userToSave = new User("John", "john@example.com"); when(userRepository.save(any(User.class))).thenReturn(userToSave); // Act (执行) User savedUser = userService.saveUser(userToSave); // Assert (断言) assertNotNull(savedUser); assertEquals("John", savedUser.getName()); verify(userRepository, times(1)).save(any(User.class)); // 验证 save 方法被调用了一次 } }
15. ❌ 使用字段注入而非构造器注入
- 👎 糟糕的写法:
// 字段注入不推荐,因为它隐藏了依赖关系,且不便于测试 @Autowired private UserService userService;
- ✅ 良好的写法:
import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor // Lombok 会为所有 final 字段自动生成构造函数 public class MyService { private final UserService userService; // 依赖声明为 final // 构造器注入在这里被自动处理了 }
🎯 总结
Spring Boot 功能强大——但能力越大,责任也越大,你需要编写出整洁、安全、高效的代码。
🚫 避免常见的陷阱 ✅ 正确使用分层架构 🔒 保护你的 API 安全 💾 妥善处理数据库和资源 🧪 永远要进行测试!