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
publicclassUserService {
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
publicclassUserController {
@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 中违反了分层架构的原则,使得代码难以测试和维护。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
publicclassProductController {
@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// 声明为全局异常处理器
publicclassGlobalExceptionHandler {
@ExceptionHandler(NoSuchElementException.class)// 只处理 NoSuchElementException
public ResponseEntity handleNoSuchElement(NoSuchElementException ex) {
// 返回一个带有 404 NOT_FOUND 状态码和友好消息的响应
returnnewResponseEntity<>("用户未找到", HttpStatus.NOT_FOUND);
}
// 可以添加更多针对不同异常的处理器...
}
5. ❌ 暴露实体类 (Entity) 而不是使用 DTO (数据传输对象)
@GetMapping("/users")
public List getUsers() {
// 直接返回 JPA 实体列表
// 这会暴露数据库模型,包括可能存在的敏感字段(如密码哈希)
return userRepository.findAll();
}
// 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
publicclassUserService {
@Autowired
private UserRepository userRepository;
public List getAllUserDTOs() {
// 将实体列表映射为 DTO 列表
return userRepository.findAll()
.stream()
.map(user -> newUserDTO(user.getName(), user.getEmail()))
.collect(Collectors.toList());
}
}
Controller: @GetMapping("/users")
public List getUsers() {
// 返回 DTO 列表,而不是实体列表
return userService.getAllUserDTOs();
}
6. ❌ (如果不使用 Spring Data) 不关闭资源
Connection
, Statement
, ResultSet
)被正确关闭,即使发生异常也要关闭。try-with-resources 是最佳实践。 import javax.sql.DataSource;
import java.sql.*;
// ...
@Autowired
private DataSource dataSource; // 假设注入了数据源
publicvoidfetchData() {
// 使用 try-with-resources 语句
try (Connectionconn= dataSource.getConnection();
Statementstmt= conn.createStatement();
ResultSetrs= stmt.executeQuery("SELECT * FROM users")) {
while (rs.next()) {
System.out.println(rs.getString("name"));
}
// conn, stmt, rs 会在这里被自动关闭
} catch (SQLException e) {
// 处理异常
e.printStackTrace();
}
}
7. ❌ 对输入数据不进行校验
import jakarta.validation.constraints.*; // 或 javax.validation.constraints.*
publicclassRegisterRequest {
@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 register(@Valid @RequestBody RegisterRequest request) {
// @Valid 注解会触发对 RegisterRequest 对象的校验
// 如果校验失败,Spring Boot 会抛出 MethodArgumentNotValidException,
// 可以通过全局异常处理器捕获并返回 400 Bad Request
return ResponseEntity.ok("用户注册成功!");
}
8. ❌ 安全配置不当
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
publicclassSecurityConfigextendsWebSecurityConfigurerAdapter { // 注意: 在新版Spring Security中,推荐使用SecurityFilterChain Bean的方式
@Override
protectedvoidconfigure(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
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. ❌ 不使用缓存
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
publicclassBookService {
@Cacheable("books")// 结果会被缓存到名为 "books" 的缓存中
public List getBooks() {
// 只有当缓存中没有数据时,这个方法体才会执行
simulateDelay(); // 模拟耗时的数据库查询
return List.of(newBook("Java 101"), newBook("Spring Boot Mastery"));
}
privatevoidsimulateDelay() {
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// 启用缓存功能
publicclassAppStarter {
publicstaticvoidmain(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
publicclassOrderService {
@Autowired
private OrderRepository orderRepository;
@Transactional// 用在 public 方法上
publicvoidplaceOrder(Order order) {
orderRepository.save(order);
// ...其他数据库操作...
}
}
12. ❌ 不记录日志(或记录不当)
System.out.println
是不够的。应该使用专业的日志框架。 import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
@Service
publicclassLogService {
privatestaticfinalLoggerlogger= LoggerFactory.getLogger(LogService.class);
publicvoidlogExample() {
// 使用参数化日志,比字符串拼接更高效、更安全
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 getUsers(@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size) {
// 使用 PageRequest 创建分页请求
return userRepository.findAll(PageRequest.of(page, size));
}
// UserRepository 需要继承 JpaRepository 或 PagingAndSortingRepository
14. ❌ 不进行单元或集成测试
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;
importstatic org.mockito.Mockito.*;
importstatic org.junit.jupiter.api.Assertions.*;
@ExtendWith(MockitoExtension.class)// 启用 Mockito 扩展
publicclassUserServiceTest {
@Mock// 创建一个 Mock 对象
private UserRepository userRepository;
@InjectMocks// 创建一个 UserService 实例,并将 @Mock 对象注入其中
private UserService userService;
@Test
voidtestSaveUser() {
// Arrange (准备)
UseruserToSave=newUser("John", "[email protected]");
when(userRepository.save(any(User.class))).thenReturn(userToSave);
// Act (执行)
UsersavedUser= 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 安全
妥善处理数据库和资源
永远要进行测试!