在Java后端开发的三层架构实践中,Service层作为业务逻辑的核心载体,经常需要与其他模块进行交互。这种交互方式的选择——是直接注入Mapper操作数据,还是通过其他Service复用业务逻辑——看似是一个简单的技术选型问题,实则关系到整个系统的可维护性和架构健康度。
我经历过一个电商项目重构,最初团队为了"保持一致性",强制要求所有跨模块调用必须通过Service层。结果导致:
后来我们调整策略,严格区分"数据访问"和"业务复用"场景,系统复杂度立即下降了40%。这个案例让我深刻认识到:依赖注入的选择本质上是对模块边界的定义。
经典三层架构的核心价值在于关注点分离:
这种分层带来的直接好处是:
健康的依赖关系应该遵循:
实际案例:在支付系统中,支付核心服务(稳定)不应依赖优惠券服务(易变),而应通过抽象接口交互。
以下情况应优先考虑注入Mapper:
在秒杀系统中,我们通过直接注入Mapper获得了显著性能提升:
java复制@Repository
public interface SeckillMapper {
@Update("UPDATE stock SET count = count - 1 WHERE product_id = #{productId} AND count > 0")
int reduceStock(@Param("productId") Long productId);
}
@Service
public class SeckillService {
private final SeckillMapper seckillMapper;
public boolean trySeckill(Long productId) {
return seckillMapper.reduceStock(productId) > 0;
}
}
这种写法相比通过库存服务调用:
即使直接使用Mapper,仍可通过声明式事务保证一致性:
java复制@Transactional
public void processOrder(Order order) {
orderMapper.insert(order);
// 直接操作日志表Mapper
logMapper.insert(new OperationLog("CREATE_ORDER", order.getId()));
// 更新统计信息
statsMapper.incrementOrderCount(order.getUserId());
}
关键点:
@Transactional注解统一管理事务当遇到以下业务需求时,应该选择注入Service:
在供应链系统中,我们采用领域服务封装核心逻辑:
java复制@Service
public class InventoryService {
@Transactional
public void allocateStock(AllocationCommand command) {
// 检查库存可用性
Inventory inventory = inventoryRepository.findById(command.productId());
if (!inventory.canAllocate(command.quantity())) {
throw new BusinessException("库存不足");
}
// 执行分配
inventory.allocate(command.quantity());
inventoryRepository.save(inventory);
// 记录分配日志
allocationLogService.logAllocation(command);
// 触发补货检查
if (inventory.needReplenishment()) {
replenishmentService.requestReplenishment(command.productId());
}
}
}
这种封装保证了:
循环依赖解决方案:
@Lazy延迟注入事务传播行为选择:
REQUIRED(默认):加入当前事务REQUIRES_NEW:新建独立事务NOT_SUPPORTED:非事务方式执行性能优化建议:
@Transactional(readOnly = true)PROPAGATION_NESTED根据业务复杂度和数据访问模式,可以将决策分为四个象限:
| 简单数据访问 | 复杂业务逻辑 | |
|---|---|---|
| 读操作 | 直接使用Mapper | 使用Service |
| 写操作 | 评估事务需求 | 必须使用Service |
在高并发场景下,可以采用折中方案:
java复制public class OrderService {
private final OrderMapper orderMapper;
private final InventoryClient inventoryClient; // 防腐层
public void createOrder(Order order) {
// 快速路径:直接检查库存可用性
if (inventoryMapper.getAvailableStock(order.getProductId()) < order.getQuantity()) {
throw new BusinessException("库存不足");
}
// 慢速路径:通过服务完成最终预留
inventoryClient.reserveStock(order.getProductId(), order.getQuantity());
orderMapper.insert(order);
}
}
java复制class OrderServiceTest {
@Mock
private OrderMapper orderMapper;
@Mock
private UserMapper userMapper;
@Test
void shouldCreateOrderWhenUserExists() {
when(userMapper.selectById(any())).thenReturn(new User());
when(orderMapper.insert(any())).thenReturn(1);
service.createOrder(new CreateOrderDto(1L, 1001L));
verify(orderMapper).insert(any());
}
}
特点:
java复制class PaymentServiceTest {
@Mock
private OrderService orderService;
@Mock
private AccountingService accountingService;
@Test
void shouldProcessPaymentSuccessfully() {
when(orderService.lockOrder(any())).thenReturn(new Order());
when(accountingService.debit(any(), any())).thenReturn("TX123");
PaymentResult result = service.processPayment(new PaymentRequest());
assertThat(result.getStatus()).isEqualTo(SUCCESS);
verify(orderService).completeOrder(any());
}
}
注意点:
对于大型系统,建议采用:
java复制// 领域模块内部
class OrderService {
private final OrderRepository orderRepository; // 聚合根仓库
public void cancelOrder(Long orderId) {
Order order = orderRepository.findById(orderId);
order.cancel(); // 领域行为
orderRepository.save(order);
}
}
// 跨模块交互
class OrderFacade {
private final OrderService orderService;
private final InventoryClient inventoryClient;
@Transactional
public void cancelOrderWithRestock(Long orderId) {
Order order = orderService.cancelOrder(orderId);
inventoryClient.restock(order.getProductId(), order.getQuantity());
}
}
在实际项目中,我逐渐形成了这样的经验法则:当你在编写Service方法时,如果发现自己在"猜测"另一个模块的内部逻辑,就应该注入它的Service;如果只是需要它的数据,就直接用Mapper。这个简单的判断标准帮助我避免了许多设计错误。