重构了20个SpringBoot项目后,总结出这套稳定高效的架构设计

1,052 阅读9分钟

为什么我们刚开始的 SpringBoot 项目清爽简洁,但随着业务增长,代码却变得越来越臃肿、难以维护?

其实项目架构的合理性往往决定了后期的开发效率。

一、常踩的坑

举2个典型场景:

1.万能的Service:小李要修改一个订单状态的功能,结果发现OrderService.java已经3800多行了!里面既有订单处理,又有用户积分计算,还夹杂着各种报表导出逻辑。想改一行代码,得先花半天时间理解。

2.异常处理:线上出了个问题,用户支付成功了但订单状态没更新。排查发现:支付模块用try-catch返回错误码,订单模块直接抛异常,而中间的协调层既没处理异常也没记录日志,出了问题都不知道该找谁。

二、一个清晰的架构应该是什么样子

1. 合理的项目结构

首先,让我们看看良好的项目结构应该是什么样的:

src/main/java
└── com
    └── example
        └── project
            ├── common           // 通用组件
            │   ├── annotation   // 自定义注解
            │   ├── config       // 全局配置
            │   ├── constant     // 常量
            │   ├── exception    // 统一异常处理
            │   ├── utils        // 工具类
            │   └── vo           // 通用VO
            ├── module1          // 业务模块1
            │   ├── controller   // 控制器
            │   ├── service      // 服务层
            │   ├── dao          // 数据访问层
            │   ├── entity       // 实体类
            │   ├── dto          // 数据传输对象
            │   └── vo           // 视图对象
            ├── module2          // 业务模块2
            │   └── ...          // 结构同上
            └── ProjectApplication.java  // 启动类

为什么这样设计?

  • 按功能模块划分:而不是按技术层次划分。这样每个模块都是自包含的,职责单一,修改一个模块不会影响到其他模块。
  • 分层清晰:controller负责接收请求,service处理业务逻辑,dao负责数据操作。
  • 通用组件抽取:避免代码重复,一处修改处处生效。

2. 统一的API响应格式

接口返回格式五花八门,前端同事是不是经常找你吐槽?来看看统一响应格式的设计:

/**
 * 统一API响应格式
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
    private Integer code;    // 状态码
    private String message;  // 消息
    private T data;          // 数据
    
    // 成功响应
    public static <T> Result<T> success() {
        return new Result<>(200, "操作成功", null);
    }
    
    public static <T> Result<T> success(T data) {
        return new Result<>(200, "操作成功", data);
    }
    
    // 失败响应
    public static <T> Result<T> error(String message) {
        return new Result<>(500, message, null);
    }
    
    public static <T> Result<T> error(Integer code, String message) {
        return new Result<>(code, message, null);
    }
}

这样,所有接口返回都遵循统一格式:

{
  "code": 200,
  "message": "操作成功",
  "data": {
    "id": 1,
    "name": "示例数据"
  }
}

好处:前端可以统一封装请求拦截器,无需为每个接口单独处理响应格式。

3. 全局异常处理

别再在每个方法里写try-catch了!使用 @ControllerAdvice 实现全局异常处理:

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
    
    // 处理自定义业务异常
    @ExceptionHandler(BusinessException.class)
    public Result<?> handleBusinessException(BusinessException e) {
        log.warn("业务异常: {}", e.getMessage());
        return Result.error(e.getCode(), e.getMessage());
    }
    
    // 处理参数校验异常
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<?> handleValidException(MethodArgumentNotValidException e) {
        String error = e.getBindingResult().getFieldError().getDefaultMessage();
        return Result.error(400, error);
    }
    
    // 处理所有其他异常
    @ExceptionHandler(Exception.class)
    public Result<?> handleException(Exception e) {
        log.error("系统异常: {}", e.getMessage(), e);
        return Result.error(500, "系统繁忙,请稍后再试");
    }
}

为什么这样做

  • 业务代码更清爽,只需关注正常逻辑
  • 异常处理集中,修改方便
  • 可以统一记录异常日志,方便排查问题

4. 配置管理最佳实践

配置文件管理混乱是很多项目的痛点。推荐这样组织:

  • 分环境配置:application.yml(公共配置)+ application-{env}.yml(环境特定配置)
  • 敏感信息外部化:数据库密码等敏感信息不要放在代码仓库,使用配置中心或环境变量
  • 配置类封装:使用@ConfigurationProperties将配置项映射为Java对象
@Component
@ConfigurationProperties(prefix = "app.jwt")
@Data
public class JwtProperties {
    private String secret;
    private long expiration;
    private String header;
}

好处

  • 配置与代码解耦
  • 修改配置无需重新编译
  • 类型安全,IDE能提供自动补全和验证

5. 事务管理规范

事务管理不当会导致数据不一致。建议:

  1. 避免在Controller层开启事务:事务应由Service层管理
  2. 合理设置事务传播行为:根据业务场景选择合适的传播行为
  3. 避免大事务:长时间运行的事务会锁住数据库资源
  4. 统一异常回滚策略:默认RuntimeException会触发回滚,检查型异常不会
@Service
@RequiredArgsConstructor
public class OrderService {
    
    private final OrderRepository orderRepository;
    private final InventoryService inventoryService;
    
    @Transactional(rollbackFor = Exception.class)
    public Order createOrder(OrderRequest request) {
        // 1. 创建订单
        Order order = orderRepository.save(convertToOrder(request));
        
        // 2. 扣减库存(远程调用)
        boolean success = inventoryService.decrease(request.getItems());
        
        if (!success) {
            throw new BusinessException("库存不足");
        }
        
        return order;
    }
}

注意:上面的例子有瑕疵!远程调用不应放在事务中,会导致事务过长。更合理的做法是使用消息队列解耦。

正确的做法:使用可靠事件模式 + 消息队列

@Service
@RequiredArgsConstructor
public class OrderService {
    
    private final OrderRepository orderRepository;
    private final ApplicationEventPublisher eventPublisher;
    private final TransactionTemplate transactionTemplate; // 用于编程式事务
    
    @Transactional
    public Order createOrder(OrderRequest request) {
        // 1. 创建订单(在事务内)
        Order order = new Order();
        order.setStatus(OrderStatus.CREATED);
        order.setItems(request.getItems());
        order.setTotalAmount(calculateTotal(request.getItems()));
        order.setCreateTime(LocalDateTime.now());
        Order savedOrder = orderRepository.save(order);
        
        // 2. 事务提交后再发送事件
        // 使用TransactionSynchronization确保事务提交后才执行
        transactionTemplate.execute(status -> {
            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
                @Override
                public void afterCommit() {
                    // 事务成功提交后,发布扣减库存事件
                    eventPublisher.publishEvent(new OrderCreatedEvent(savedOrder.getId(), request.getItems()));
                }
            });
            return null;
        });
        
        return savedOrder;
    }
}

// 事件监听器(异步处理)
@Component
@RequiredArgsConstructor
@Slf4j
public class InventoryEventListener {
    
    private final InventoryService inventoryService;
    private final OrderRepository orderRepository;
    private final RocketMQTemplate rocketMQTemplate; // 或其他MQ客户端
    
    @Async
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void handleOrderCreated(OrderCreatedEvent event) {
        try {
            // 1. 先记录事件已处理(防重复)
            if (eventLogService.isEventProcessed(event.getOrderId())) {
                return;
            }
            
            // 2. 扣减库存(远程调用)
            boolean success = inventoryService.decrease(event.getItems());
            
            if (!success) {
                // 3. 库存不足,更新订单状态
                orderRepository.updateStatus(event.getOrderId(), OrderStatus.CANCELLED);
                
                // 4. 通知用户
                notificationService.sendStockShortageNotice(event.getOrderId());
            }
            
            // 5. 记录事件处理结果
            eventLogService.markEventAsProcessed(event.getOrderId());
            
        } catch (Exception e) {
            log.error("处理订单[{}]库存扣减失败", event.getOrderId(), e);
            // 5. 重试机制:发送到MQ重试队列
            rocketMQTemplate.convertAndSend("ORDER_INVENTORY_RETRY", event);
        }
    }
}

// 重试处理器(补偿机制)
@Component
@RequiredArgsConstructor
@Slf4j
public class InventoryRetryHandler {
    
    private final InventoryService inventoryService;
    private final OrderRepository orderRepository;
    private final EventLogService eventLogService;
    
    @RocketMQMessageListener(topic = "ORDER_INVENTORY_RETRY", consumerGroup = "inventory-retry-group")
    public void handleRetry(OrderCreatedEvent event) {
        // 1. 检查重试次数
        int retryCount = retryLogService.getRetryCount(event.getOrderId());
        if (retryCount > 3) {
            // 超过最大重试次数,人工介入
            alarmService.sendAlert("库存扣减持续失败,订单ID: " + event.getOrderId());
            return;
        }
        
        try {
            // 2. 再次尝试扣减库存
            boolean success = inventoryService.decrease(event.getItems());
            
            if (success) {
                // 更新订单状态为已确认
                orderRepository.updateStatus(event.getOrderId(), OrderStatus.CONFIRMED);
                // 标记事件已处理
                eventLogService.markEventAsProcessed(event.getOrderId());
            } else if (retryCount >= 2) {
                // 最后一次重试仍失败,取消订单
                orderRepository.updateStatus(event.getOrderId(), OrderStatus.CANCELLED);
                notificationService.sendStockShortageNotice(event.getOrderId());
            }
            
            // 3. 记录重试
            retryLogService.recordRetry(event.getOrderId());
            
        } catch (Exception e) {
            log.error("重试处理订单[{}]库存扣减失败", event.getOrderId(), e);
            retryLogService.recordRetry(event.getOrderId());
            // 可以考虑指数退避算法延迟下次重试
        }
    }
}

事务设计不是简单的加个@Transactional注解,而是要考虑:

  1. 事务范围要尽量小
  2. 事务内不要有远程调用、IO操作
  3. 跨服务调用需要有补偿机制
  4. 最终一致性往往是更好的选择

三、案例:重构前后的对比

假设我们有一个用户模块,重构前是这样的:

// 重构前:混乱的UserService
@Service
public class UserService {
    
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private EmailService emailService;
    
    @Autowired
    private SmsService smsService;
    
    // 包含注册、登录、修改资料、密码重置等多种功能
    public User register(UserRegisterDTO dto) {
        // 1. 参数校验
        if (userRepository.existsByUsername(dto.getUsername())) {
            throw new RuntimeException("用户名已存在");
        }
        
        // 2. 保存用户
        User user = new User();
        user.setUsername(dto.getUsername());
        user.setPassword(encryptPassword(dto.getPassword()));
        user.setEmail(dto.getEmail());
        user.setPhone(dto.getPhone());
        user.setCreateTime(new Date());
        
        User savedUser = userRepository.save(user);
        
        // 3. 发送欢迎邮件
        try {
            emailService.sendWelcomeEmail(user.getEmail());
        } catch (Exception e) {
            log.error("发送欢迎邮件失败", e);
        }
        
        return savedUser;
    }
    
    // 还有2000行其他方法...
}

重构后:

// 重构后:职责清晰的分层架构
// controller/UserController.java
@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {
    private final UserService userService;
    private final UserRegisterService userRegisterService;
    
    @PostMapping("/register")
    public Result<UserVO> register(@Valid @RequestBody UserRegisterDTO dto) {
        User user = userRegisterService.register(dto);
        return Result.success(convertToVO(user));
    }
    
    @GetMapping("/{id}")
    public Result<UserDetailVO> getById(@PathVariable Long id) {
        UserDetail detail = userService.getUserDetail(id);
        return Result.success(convertToDetailVO(detail));
    }
}

// service/UserRegisterService.java
@Service
@RequiredArgsConstructor
@Transactional(rollbackFor = Exception.class)
public class UserRegisterService {
    private final UserRepository userRepository;
    private final UserValidator userValidator;
    private final PasswordEncoder passwordEncoder;
    private final UserEventPublisher eventPublisher;
    
    public User register(UserRegisterDTO dto) {
        // 1. 校验
        userValidator.validateRegistration(dto);
        
        // 2. 创建用户
        User user = createUserFromDto(dto);
        
        // 3. 保存
        User savedUser = userRepository.save(user);
        
        // 4. 发布事件(解耦通知逻辑)
        eventPublisher.publishUserRegisteredEvent(savedUser);
        
        return savedUser;
    }
    
    private User createUserFromDto(UserRegisterDTO dto) {
        User user = new User();
        user.setUsername(dto.getUsername());
        user.setPassword(passwordEncoder.encode(dto.getPassword()));
        user.setEmail(dto.getEmail());
        user.setPhone(dto.getPhone());
        user.setCreateTime(new Date());
        return user;
    }
}

// service/UserService.java
@Service
@RequiredArgsConstructor
public class UserService {
    private final UserRepository userRepository;
    private final UserFactory userFactory;
    
    @Transactional(readOnly = true)
    public UserDetail getUserDetail(Long userId) {
        User user = userRepository.findById(userId)
                .orElseThrow(() -> new BusinessException("用户不存在"));
        
        // 组装详细信息(可能涉及多表查询)
        return userFactory.createUserDetail(user);
    }
}

// event/UserEventPublisher.java
@Component
@RequiredArgsConstructor
public class UserEventPublisher {
    private final ApplicationEventPublisher eventPublisher;
    
    public void publishUserRegisteredEvent(User user) {
        eventPublisher.publishEvent(new UserRegisteredEvent(this, user));
    }
}

// listener/UserRegistrationListener.java
@Component
@RequiredArgsConstructor
public class UserRegistrationListener {
    private final EmailService emailService;
    private final SmsService smsService;
    
    @Async // 异步处理
    @EventListener
    public void handleUserRegistered(UserRegisteredEvent event) {
        User user = event.getUser();
        try {
            emailService.sendWelcomeEmail(user.getEmail());
        } catch (Exception e) {
            log.error("发送欢迎邮件失败,用户ID: {}", user.getId(), e);
        }
        
        try {
            smsService.sendWelcomeSms(user.getPhone());
        } catch (Exception e) {
            log.error("发送欢迎短信失败,用户ID: {}", user.getId(), e);
        }
    }
}

重构带来的好处

  1. 职责分明:注册服务只负责注册,查询服务只负责查询
  2. 事务边界清晰:只有需要事务的方法才添加@Transactional
  3. 解耦通知逻辑:使用事件机制将核心业务与辅助功能解耦
  4. 可测试性强:每个组件职责单一,更容易编写单元测试
  5. 异常处理统一:依赖全局异常处理器,无需在业务代码中处理异常

四、架构设计的几个关键原则

分享几个我认为特别重要的原则,这些原则指导着我的架构设计:

1. 高内聚低耦合

  • 高内聚:一个模块内部的组件高度相关,共同完成单一职责
  • 低耦合:模块之间依赖关系简单,修改一个模块不会影响太多其他模块

2. 关注点分离(SoC)

将不同关注点(如业务逻辑、数据访问、安全控制)分离到不同组件中,每个组件只关注自己的职责。

3. 依赖倒置原则

高层模块不应依赖低层模块,两者都应该依赖抽象。抽象不应依赖细节,细节应该依赖抽象。

// 不好的设计
public class OrderService {
    private MysqlOrderRepository repository; // 依赖具体实现
}

// 好的设计
public class OrderService {
    private OrderRepository repository; // 依赖抽象接口
}

4. 适时重构

不要等到代码烂到无法维护才重构。建议:

  • 每次修改代码时,顺手改进相关的结构
  • 定期进行代码评审,发现架构问题及时调整
  • 使用自动化测试保障重构安全

五、总结

  1. 模块化组织:按业务功能而非技术层次划分模块
  2. 统一规范:API响应、异常处理、日志记录等统一规范
  3. 分层明确:Controller、Service、DAO各司其职
  4. 解耦设计:使用事件驱动、消息队列等方式解耦核心业务
  5. 配置管理:合理组织配置,敏感信息分离

本文首发于公众号:程序员大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!