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

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 中违反了分层架构的原则,使得代码难以测试和维护。
  • • ✅ 良好的代码:
    服务层 (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
    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();
    }
  • • ✅ 良好的代码:
    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
    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) 不关闭资源

  • • ✅ 正确的 JDBC 资源管理示例:
    在不使用像 Spring Data JPA 这样能自动管理资源的框架,而直接使用原生 JDBC 时,必须确保资源(如 ConnectionStatementResultSet)被正确关闭,即使发生异常也要关闭。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. ❌ 对输入数据不进行校验

  • • ✅ 带校验的 DTO:
    不校验用户输入会带来安全风险和数据不一致问题。应始终使用校验注解。
    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. ❌ 安全配置不当

  • • ✅ 使用角色保护 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
    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

  • • ✅ 正确用法:
    使用 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
    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. ❌ 不记录日志(或记录不当)

  • • ✅ 使用 SLF4J:
    在生产环境中,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. ❌ 不进行单元或集成测试

  • • ✅ 单元测试示例 (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;
    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 安全
妥善处理数据库和资源
永远要进行测试!

你可能感兴趣的:(spring,boot,后端,java)