为什么我们刚开始的 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. 事务管理规范
事务管理不当会导致数据不一致。建议:
- 避免在Controller层开启事务:事务应由Service层管理
- 合理设置事务传播行为:根据业务场景选择合适的传播行为
- 避免大事务:长时间运行的事务会锁住数据库资源
- 统一异常回滚策略:默认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注解,而是要考虑:
- 事务范围要尽量小
- 事务内不要有远程调用、IO操作
- 跨服务调用需要有补偿机制
- 最终一致性往往是更好的选择
三、案例:重构前后的对比
假设我们有一个用户模块,重构前是这样的:
// 重构前:混乱的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);
}
}
}
重构带来的好处:
- 职责分明:注册服务只负责注册,查询服务只负责查询
- 事务边界清晰:只有需要事务的方法才添加@Transactional
- 解耦通知逻辑:使用事件机制将核心业务与辅助功能解耦
- 可测试性强:每个组件职责单一,更容易编写单元测试
- 异常处理统一:依赖全局异常处理器,无需在业务代码中处理异常
四、架构设计的几个关键原则
分享几个我认为特别重要的原则,这些原则指导着我的架构设计:
1. 高内聚低耦合
- 高内聚:一个模块内部的组件高度相关,共同完成单一职责
- 低耦合:模块之间依赖关系简单,修改一个模块不会影响太多其他模块
2. 关注点分离(SoC)
将不同关注点(如业务逻辑、数据访问、安全控制)分离到不同组件中,每个组件只关注自己的职责。
3. 依赖倒置原则
高层模块不应依赖低层模块,两者都应该依赖抽象。抽象不应依赖细节,细节应该依赖抽象。
// 不好的设计
public class OrderService {
private MysqlOrderRepository repository; // 依赖具体实现
}
// 好的设计
public class OrderService {
private OrderRepository repository; // 依赖抽象接口
}
4. 适时重构
不要等到代码烂到无法维护才重构。建议:
- 每次修改代码时,顺手改进相关的结构
- 定期进行代码评审,发现架构问题及时调整
- 使用自动化测试保障重构安全
五、总结
- 模块化组织:按业务功能而非技术层次划分模块
- 统一规范:API响应、异常处理、日志记录等统一规范
- 分层明确:Controller、Service、DAO各司其职
- 解耦设计:使用事件驱动、消息队列等方式解耦核心业务
- 配置管理:合理组织配置,敏感信息分离
本文首发于公众号:程序员大华,专注分享前后端开发的实战笔记。关注我,少走弯路,一起进步!