15个Spring Boot常见编程误区解析与代码优化建议

2025/06/14 SpringBoot

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 安全 💾 妥善处理数据库和资源 🧪 永远要进行测试!

Show Disqus Comments

Search

    Post Directory