用过 Spring Boot 的开发者大概都有过这样的体验:项目初期代码简洁优雅,迭代几个版本、新增几个功能后,逐渐变得混乱不堪——Controller 臃肿不堪、Service 职责混乱、数据库访问随处可见,性能也跟着一落千丈,最终沦为难以维护的“代码沼泽”。

其实 Spring Boot 本身足够强大,问题从来不在框架,而在我们的开发方式。今天就来梳理一套实用的 Spring Boot 开发规范,避开常见反模式,让项目长期保持清晰架构、良好性能和可扩展性。

一、为什么会陷入“代码沼泽”?

很多团队在项目初期,为了追求开发速度,往往会忽略架构设计,陷入这样的误区:

  • 为了快速交付功能,直接在 Controller 中写所有逻辑;

  • Repository 层接口被到处调用,数据访问逻辑分散;

  • 没有统一的分层架构,代码想到哪写到哪。

短期来看,这种方式确实能加快开发进度,但长期来看,代价惨重:代码可维护性急剧下降,新增功能要到处改代码;性能出现瓶颈,排查问题无从下手;代码难以单元测试,迭代风险越来越高;架构逐渐腐化,最后只能推倒重构。

二、Spring Boot 推荐架构:分层清晰,职责单一

一个健康的 Spring Boot 项目,核心是“分层架构”,每一层都有明确的职责,互不越界。推荐的项目目录结构如下(以 com.goldpac.icoderoad 包为例):

/src/main/java/com/goldpac/icoderoad
├── controller  # 处理HTTP请求,接收参数、返回响应
├── service     # 核心业务逻辑层,处理业务规则
├── repository  # 数据访问层,封装数据库操作
├── domain      # 实体类,对应数据库表结构
├── dto         # 数据传输对象,用于前后端数据交互
├── config      # 配置类,全局配置、Bean注册等
├── exception   # 异常相关,全局异常处理、自定义异常
└── util        # 工具类,通用工具方法封装

记住一个核心原则:每一层只做自己的事,不跨层调用、不越权处理逻辑。

三、10个常见反模式及解决方案(附代码示例)

下面梳理项目中最常出现的10个反模式,每个都附上问题表现、分析和规范的代码示例,直接套用即可。

反模式1:Controller 写满业务逻辑

问题表现:Controller 既处理 HTTP 请求,又做参数校验、业务判断、数据库访问,集多种职责于一身。

@PostMapping("/users")
public ResponseEntity<?> create(@RequestBody UserDTO dto) {
    // 违规:Controller 直接做参数校验、数据库操作
    if (dto.getAge() < 18) return ResponseEntity.badRequest().body("用户必须年满18周岁");
    User user = new User();
    user.setName(dto.getName());
    user.setAge(dto.getAge());
    userRepository.save(user); // 直接调用Repository,跨层违规
    return ResponseEntity.ok("用户创建成功");
}

问题分析:违反单一职责原则,Controller 变得越来越胖,难以单元测试,业务逻辑分散,后续修改一处逻辑要动多个地方。

解决方案:Controller 只负责 HTTP 层的逻辑(接收请求、返回响应),所有业务逻辑移至 Service 层。

规范示例

// Controller 层(仅处理HTTP请求,不包含任何业务逻辑)
package com.icoderoad.controller;

import com.icoderoad.dto.UserDTO;
import com.icoderoad.service.UserService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/users")
public class UserController {

    // 构造函数注入Service(遵循反模式3规范)
    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @PostMapping
    public ResponseEntity<String> create(@RequestBody UserDTO dto) {
        // 仅接收请求,调用Service处理业务,返回响应
        userService.createUser(dto);
        return ResponseEntity.ok("用户创建成功");
    }
}
// Service 层(仅处理业务逻辑,通过Repository访问数据)
package com.icoderoad.service;

import com.icoderoad.dto.UserDTO;
import com.icoderoad.domain.User;
import com.icoderoad.repository.UserRepository;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    // 构造函数注入Repository(遵循反模式3规范)
    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    // 专注业务逻辑:参数校验、实体转换、调用Repository
    public void createUser(UserDTO dto) {
        // 业务规则校验(原Controller中的违规逻辑移至此)
        if (dto.getAge() < 18) {
            throw new IllegalArgumentException("用户必须年满18周岁");
        }
        // 实体转换(DTO转Entity,避免直接暴露实体)
        User user = new User();
        user.setName(dto.getName());
        user.setAge(dto.getAge());
        // 调用Repository访问数据库(不直接操作SQL)
        userRepository.save(user);
    }
}

反模式2:Service 直接写 SQL

问题表现:在 Service 层直接使用 JdbcTemplate 写 SQL,数据访问逻辑和业务逻辑混在一起。

@Service
public class UserService {
    // 违规:Service层直接使用JdbcTemplate写SQL,数据访问与业务逻辑混合
    @Autowired
    private JdbcTemplate jdbcTemplate;

    public List<UserDTO> listUsers() {
        // SQL硬编码在Service中,难以维护和复用
        String sql = "SELECT id, name, age FROM users WHERE age >= 18";
        return jdbcTemplate.query(sql, (rs, rowNum) -> {
            UserDTO dto = new UserDTO();
            dto.setName(rs.getString("name"));
            dto.setAge(rs.getInt("age"));
            return dto;
        });
    }
}

问题分析:SQL 语句分散在 Service 层,难以维护和复用;Service 层职责混乱,既要处理业务,又要关心数据库访问细节。

解决方案:使用 Repository 层专门封装数据访问逻辑,Service 层通过调用 Repository 接口获取数据,不直接操作 SQL。

规范示例

// Repository 层(仅负责数据访问,封装所有数据库操作)
package com.icoderoad.repository;

import com.icoderoad.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import java.util.List;

// 继承JpaRepository,自带CRUD方法,无需手动写基础SQL
public interface UserRepository extends JpaRepository<User, Long> {

    // 如需自定义查询,在Repository层统一封装(而非Service层)
    @Query("SELECT u FROM User u WHERE u.age >= :age")
    List<User> findByAgeGreaterThanEqual(@Param("age") Integer age);
}

// 对应Service层规范写法(仅处理业务,不操作SQL)
@Service
public class UserService {
    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public List<UserDTO> listUsers() {
        // 业务逻辑:仅定义规则,调用Repository获取数据
        List<User> users = userRepository.findByAgeGreaterThanEqual(18);
        // 实体转DTO(遵循反模式4规范)
        return users.stream()
                .map(u -> new UserDTO(u.getName(), u.getAge()))
                .toList();
    }
}

反模式3:过度使用 @Autowired

问题表现:使用 @Autowired 注解隐式注入依赖,代码中随处可见。

// 违规:过度使用@Autowired隐式注入,依赖关系不明确
@RestController
@RequestMapping("/users")
public class UserController {
    // 隐式注入,无构造函数,不利于测试和维护
    @Autowired
    private UserService userService;
    @Autowired
    private UserRepository userRepository; // 违规:Controller直接依赖Repository

    @GetMapping
    public List<UserDTO> list() {
        return userService.listUsers();
    }
}

问题分析:隐式依赖不利于单元测试(无法手动注入mock对象);不符合不可变原则,依赖对象可能被篡改;代码可读性差,难以快速找到依赖关系。

解决方案:使用构造函数注入,明确依赖关系,同时保证依赖对象不可变。

规范示例

// 规范:构造函数注入,明确依赖关系,保证依赖不可变
@RestController
@RequestMapping("/users")
public class UserController {
    // final修饰,保证依赖对象不可篡改
    private final UserService userService;

    // 构造函数注入(Spring Boot 4.3+可省略@Autowired)
    // 仅依赖Service,不直接依赖Repository(遵循分层架构)
    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping
    public List<UserDTO> list() {
        return userService.listUsers();
    }
}

// Service层同理,构造函数注入Repository
@Service
public class UserService {
    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

反模式4:实体直接暴露给前端

问题表现:Controller 直接返回数据库实体(Entity)给前端,暴露数据库结构。

// 违规:Controller直接返回数据库实体(Entity),暴露数据库结构
@RestController
@RequestMapping("/users")
public class UserController {
    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping
    public List<User> list() {
        // 直接返回Entity,暴露数据库表所有字段(如id、createTime等敏感/无用字段)
        return userRepository.findAll();
    }
}

问题分析:暴露数据库表结构,存在安全隐患(比如敏感字段泄露);一旦修改实体类字段,会直接影响前端接口,破坏接口兼容性;前端不需要的字段也会被返回,浪费带宽。

解决方案:使用 DTO(Data Transfer Object,数据传输对象)进行前后端数据交互,只返回前端需要的字段。

规范示例

// 规范:DTO类(仅包含前端需要的字段,隐藏数据库结构)
package com.icoderoad.dto;

import lombok.Data; // 实际开发可使用lombok简化getter/setter
@Data
public class UserDTO {
    // 仅暴露前端需要的字段,不包含数据库无关字段(如id、createTime)
    private String name;
    private Integer age;
    // 如需扩展,仅添加前端需要的字段,不影响数据库实体
}
// 规范:Service层负责Entity与DTO的转换,不暴露实体给前端
@Service
public class UserService {
    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public List<UserDTO> listUsers() {
        // 1. 从Repository获取实体(仅Service层可访问Entity)
        List<User> users = userRepository.findAll();
        // 2. 实体转DTO,只返回前端需要的字段
        return users.stream()
                .map(user -> {
                    UserDTO dto = new UserDTO();
                    dto.setName(user.getName());
                    dto.setAge(user.getAge());
                    return dto;
                })
                .toList();
    }
}

// Controller层仅返回DTO
@RestController
@RequestMapping("/users")
public class UserController {
    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @GetMapping
    public List<UserDTO> list() {
        return userService.listUsers(); // 返回DTO,隐藏数据库结构
    }
}

反模式5:滥用 @Transactional

问题表现:在方法上随意添加 @Transactional 注解,将远程调用、非数据库操作也包裹在事务中。

// 违规:事务包裹远程调用,导致锁时间过长
@Service
public class OrderService {
    private final OrderRepository orderRepository;
    private final PaymentApi paymentApi; // 远程调用接口

    public OrderService(OrderRepository orderRepository, PaymentApi paymentApi) {
        this.orderRepository = orderRepository;
        this.paymentApi = paymentApi;
    }

    // 违规:事务包含远程调用(paymentApi.call())
    @Transactional
    public void createOrder(OrderDTO dto) {
        // 远程调用:耗时不确定,导致事务锁持有时间过长
        paymentApi.call(dto.getPaymentId());
        // 数据库操作:本应是事务唯一包含的内容
        Order order = new Order();
        order.setOrderNo(dto.getOrderNo());
        orderRepository.save(order);
    }
}

问题分析:远程调用耗时不确定,会导致事务锁时间过长,影响数据库性能;非数据库操作不需要事务,添加注解会增加不必要的开销。

解决方案:只在包含数据库写入操作(新增、修改、删除)的方法上开启事务,且事务范围尽量最小化,避免包含远程调用、IO操作等。

反模式6:N+1 查询问题

问题表现:查询主表数据后,循环查询关联表数据,导致大量冗余查询。

// 违规:N+1查询问题,循环查询关联数据
@Service
public class UserService {
    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public List<UserDTO> listUsersWithOrders() {
        // 1次查询:获取所有用户(N为用户数量)
        List<User> users = userRepository.findAll();
        // N次查询:循环获取每个用户的订单,性能极差
        users.forEach(user -> {
            // 每次调用getOrders()都会触发一次数据库查询
            user.getOrders().size();
        });
        // 实体转DTO返回
        return users.stream()
                .map(user -> new UserDTO(user.getName(), user.getAge()))
                .toList();
    }
}

问题分析:上述代码会执行 1 次查询获取所有用户,然后执行 N 次查询获取每个用户的订单,查询次数随数据量增加而增多,严重影响性能。

解决方案:使用 JOIN FETCH 关联查询,一次性获取主表和关联表数据,避免冗余查询。

规范示例

// 规范:Repository层使用JOIN FETCH,一次性获取主表+关联表数据
package com.icoderoad.repository;

import com.icoderoad.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.List;

public interface UserRepository extends JpaRepository<User, Long> {
    // JOIN FETCH 关联查询,1次查询获取所有用户及对应订单
    @Query("select u from User u join fetch u.orders")
    List<User> findAllWithOrders();
}

// Service层调用,避免N+1查询
@Service
public class UserService {
    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public List<UserDTO> listUsersWithOrders() {
        // 仅1次查询,获取所有用户及订单
        List<User> users = userRepository.findAllWithOrders();
        return users.stream()
                .map(user -> new UserDTO(user.getName(), user.getAge()))
                .toList();
    }
}

反模式7:没有统一异常处理

问题表现:在 Controller、Service 中到处使用 try-catch 处理异常,异常处理逻辑分散。

// 违规:异常处理分散,每个接口都写try-catch,格式不统一
@RestController
@RequestMapping("/users")
public class UserController {
    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @PostMapping
    public ResponseEntity<?> create(@RequestBody UserDTO dto) {
        try {
            userService.createUser(dto);
            return ResponseEntity.ok("创建成功");
        } catch (IllegalArgumentException e) {
            // 每个接口都要单独处理异常,代码冗余
            return ResponseEntity.badRequest().body(e.getMessage());
        } catch (Exception e) {
            return ResponseEntity.internalServerError().body("系统异常");
        }
    }

    @GetMapping("/{id}")
    public ResponseEntity<?> getById(@PathVariable Long id) {
        try {
            UserDTO dto = userService.getById(id);
            return ResponseEntity.ok(dto);
        } catch (Exception e) {
            // 异常响应格式不统一,前端对接困难
            return ResponseEntity.status(500).body("查询失败");
        }
    }
}

问题分析:代码冗余,相同的异常处理逻辑重复出现;无法统一管理异常响应格式,前端对接困难;异常排查不便,没有统一的异常日志。

解决方案:使用 @RestControllerAdvice 实现全局异常处理,统一捕获所有异常,规范响应格式。

规范示例

// 规范:全局异常处理,统一捕获、统一响应格式
package com.icoderoad.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

// 全局异常处理器,所有Controller的异常都会被捕获
@RestControllerAdvice
public class GlobalExceptionHandler {

    // 自定义异常:参数非法(如年龄小于18)
    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<ErrorDTO> handleIllegalArgException(IllegalArgumentException e) {
        ErrorDTO error = new ErrorDTO(
                HttpStatus.BAD_REQUEST.value(),
                "参数异常",
                e.getMessage()
        );
        return ResponseEntity.badRequest().body(error);
    }

    // 通用异常:系统异常
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorDTO> handleException(Exception e) {
        ErrorDTO error = new ErrorDTO(
                HttpStatus.INTERNAL_SERVER_ERROR.value(),
                "系统异常",
                "服务器内部错误,请联系管理员"
        );
        // 打印异常堆栈,方便排查(遵循反模式10规范)
        log.error("系统异常:", e);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
    }

    // 统一异常响应DTO(规范返回格式)
    public static class ErrorDTO {
        private Integer code;
        private String message;
        private String detail;

        // 构造函数、getter/setter
        public ErrorDTO(Integer code, String message, String detail) {
            this.code = code;
            this.message = message;
            this.detail = detail;
        }
    }
}

反模式8:配置写死在代码里

问题表现:将配置信息(如接口地址、参数阈值)硬编码在代码中,无法灵活调整。

// 违规:配置硬编码,无法根据环境切换,修改需重新部署
@Service
public class PaymentService {
    // 硬编码接口地址,测试/生产环境无法灵活调整
    private final String PAYMENT_API_URL = "http://localhost:8080/api/payment";
    // 硬编码参数阈值,修改需改代码
    private final Integer MAX_RETRY_COUNT = 3;

    public void callPaymentApi(String orderNo) {
        // 直接使用硬编码配置,扩展性差
        RestTemplate restTemplate = new RestTemplate();
        restTemplate.postForObject(PAYMENT_API_URL + "/notify", orderNo, String.class);
    }
}

问题分析:配置硬编码,修改配置需要修改代码、重新部署;无法根据开发、测试、生产等不同环境调整配置,灵活性极差。

解决方案:使用 application.yml 或 application.properties 配置文件管理所有配置,通过 @Value 或 @ConfigurationProperties 读取配置。

规范示例

# 规范:配置文件管理(application.yml),按环境区分配置
# 开发环境:application-dev.yml
# 测试环境:application-test.yml
# 生产环境:application-prod.yml
app:
  payment:
    api-url: http://localhost:8080/api/payment # 接口地址,可按环境修改
  retry:
    max-count: 3 # 重试阈值,灵活调整
  page:
    max-size: 100 # 分页最大条数
// 规范:读取配置文件,不硬编码,支持多环境切换
package com.icoderoad.service;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Service;

@Service
public class PaymentService {

    // 方式1:@Value 读取单个配置(适合少量配置)
    @Value("${app.payment.api-url}")
    private String paymentApiUrl;

    // 方式2:@ConfigurationProperties 批量读取(推荐,适合多个相关配置)
    private final AppConfig appConfig;

    public PaymentService(AppConfig appConfig) {
        this.appConfig = appConfig;
    }

    public void callPaymentApi(String orderNo) {
        // 使用配置文件中的配置,无需修改代码
        RestTemplate restTemplate = new RestTemplate();
        restTemplate.postForObject(paymentApiUrl + "/notify", orderNo, String.class);
        // 使用批量配置
        System.out.println("最大重试次数:" + appConfig.getRetry().getMaxCount());
    }

    // 批量配置类(与配置文件层级对应)
    @Configuration
    @ConfigurationProperties(prefix = "app")
    public static class AppConfig {
        private PaymentConfig payment;
        private RetryConfig retry;
        private PageConfig page;

        // 内部静态类,对应配置层级
        public static class PaymentConfig {
            private String apiUrl;
            // getter/setter
        }
        public static class RetryConfig {
            private Integer maxCount;
            // getter/setter
        }
        public static class PageConfig {
            private Integer maxSize;
            // getter/setter
        }
        // 外部类getter/setter
    }
}

反模式9:没有缓存策略

问题表现:对于高频访问、变化频率低的数据(如字典、商品列表),每次请求都直接查询数据库。

// 违规:高频访问接口无缓存,每次都查询数据库,压力过大
@Service
public class ProductService {
    private final ProductRepository productRepository;

    public ProductService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    // 商品列表为高频访问、低频修改数据,无缓存会频繁查询数据库
    public List<ProductDTO> listProducts() {
        // 每次请求都查询数据库,高并发下性能瓶颈
        List<Product> products = productRepository.findAll();
        return products.stream()
                .map(p -> new ProductDTO(p.getName(), p.getPrice()))
                .toList();
    }
}

问题分析:高并发场景下,大量请求直接访问数据库,会导致数据库压力过大,接口响应变慢,甚至出现数据库瓶颈。

解决方案:使用 Spring Cache 框架,对高频访问的数据进行缓存,减少数据库查询次数。

规范示例

// 规范:使用Spring Cache缓存高频访问数据,减少数据库压力
package com.icoderoad;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

// 1. 启动类添加@EnableCaching,开启缓存功能
@SpringBootApplication
@EnableCaching
public class SpringBootDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootDemoApplication.class, args);
    }
}

// 2. Service层添加缓存注解,控制缓存逻辑
@Service
public class ProductService {
    private final ProductRepository productRepository;

    public ProductService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    // @Cacheable:查询时先查缓存,无缓存再查数据库,结果存入缓存
    @Cacheable(value = "productList", key = "'allProducts'")
    public List<ProductDTO> listProducts() {
        // 仅第一次请求会查询数据库,后续请求直接返回缓存
        List<Product> products = productRepository.findAll();
        return products.stream()
                .map(p -> new ProductDTO(p.getName(), p.getPrice()))
                .toList();
    }

    // @CacheEvict:修改/新增数据后,清除对应缓存,避免缓存脏数据
    @CacheEvict(value = "productList", key = "'allProducts'")
    public void addProduct(ProductDTO dto) {
        Product product = new Product();
        product.setName(dto.getName());
        product.setPrice(dto.getPrice());
        productRepository.save(product);
    }
}

反模式10:日志随便打印

问题表现:使用 System.out.println() 打印日志,日志级别混乱,没有统一规范。

// 违规:日志打印不规范,使用System.out,无级别、无上下文
@Service
public class UserService {
    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void createUser(UserDTO dto) {
        // 不规范1:使用System.out,无法控制日志级别
        System.out.println("用户创建开始");
        try {
            User user = new User();
            user.setName(dto.getName());
            user.setAge(dto.getAge());
            userRepository.save(user);
            // 不规范2:日志无上下文,无法定位具体用户
            System.out.println("用户创建成功");
        } catch (Exception e) {
            // 不规范3:不打印异常堆栈,无法排查问题
            System.out.println("用户创建失败");
        }
    }
}

问题分析:日志输出方式不统一,难以管理;无法控制日志级别(如开发环境打印 debug 日志,生产环境只打印 info 日志);日志没有上下文信息,排查问题困难。

解决方案:使用 SLF4J + Logback (Spring Boot 默认日志框架),统一日志打印规范,按级别输出日志。

规范示例

// 规范:使用SLF4J+Logback,统一日志规范,按级别输出
package com.icoderoad.service;

import com.icoderoad.domain.User;
import com.icoderoad.dto.UserDTO;
import com.icoderoad.repository.UserRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    // 1. 定义日志对象,参数为当前类(便于定位日志来源)
    private static final Logger log = LoggerFactory.getLogger(UserService.class);

    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void createUser(UserDTO dto) {
        // 2. 按级别输出日志,info级别记录业务流程,携带上下文(用户名)
        log.info("用户创建开始,用户名:{},年龄:{}", dto.getName(), dto.getAge());
        try {
            User user = new User();
            user.setName(dto.getName());
            user.setAge(dto.getAge());
            userRepository.save(user);
            log.info("用户创建成功,用户名:{}", dto.getName());
        } catch (Exception e) {
            // 3. error级别记录异常,打印堆栈(便于排查),携带上下文
            log.error("用户创建失败,用户名:{},异常信息:{}", dto.getName(), e.getMessage(), e);
        }
    }
}

// 补充:日志级别规范(从低到高)
// debug:开发调试用,生产环境关闭
// info:业务流程关键节点,如"用户创建开始"
// warn:警告信息,如"参数非必填但为空"
// error:异常信息,如"用户创建失败",必须打印堆栈

四、核心原则总结

其实 Spring Boot 开发规范,本质上是围绕“单一职责”“分层架构”“可维护性”这三个核心展开的。记住以下5个原则,就能避开大部分坑,让项目长期保持优雅:

  1. Controller 只处理 HTTP:接收请求、校验参数、返回响应,不写任何业务逻辑;

  2. Service 只写业务逻辑:专注于业务规则处理,通过 Repository 访问数据,不直接操作数据库;

  3. Repository 只做数据访问:封装数据库操作,提供统一的数据访问接口,不包含业务逻辑;

  4. DTO 不暴露 Entity:用 DTO 做前后端数据传输,保护数据库结构,保证接口兼容性;

  5. 事务只包裹数据库操作:最小化事务范围,避免包含远程调用、IO 操作,减少锁时间。

Spring Boot 的核心优势是“简化开发、提高效率”,但高效不等于随意。遵循规范,才能避免项目从“优雅”沦为“沼泽”,让每一次迭代都更轻松,每一行代码都更可维护。

希望这套规范能帮到正在开发 Spring Boot 项目的你,也欢迎在评论区补充你项目中遇到的反模式和解决方案~

(注:文档部分内容可能由 AI 生成)

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐