DDD项目分层结构

// Controller:参数校验、路由、返回包装,不写业务逻辑

// Application Service:事务边界、业务编排,Dubbo 调用远端服务

// Domain:纯业务逻辑,无技术框架依赖,无 Dubbo 引用

// Repository / Infrastructure:数据持久化、Redis、MQ、RPC 通信

下面用一个"优惠券发放"业务场景,把 Dubbo Provider / Consumer 在四层中的位置都演示清楚。


场景说明

Consumer(订单服务)调用 Provider(优惠券服务)查询用户可用券,再由订单服务自己完成扣减逻辑。


一、Provider 侧——优惠券服务

1. Domain 层(纯业务,零框架依赖)

java

复制

// domain/model/Coupon.java
public class Coupon {
    private Long   id;
    private Long   userId;
    private String code;
    private BigDecimal discountAmount;
    private CouponStatus status;   // AVAILABLE / USED / EXPIRED

    /** 核心业务规则:只有可用状态才能被使用 */
    public void markUsed() {
        if (this.status != CouponStatus.AVAILABLE) {
            throw new DomainException("券 [" + code + "] 当前不可用,状态=" + status);
        }
        this.status = CouponStatus.USED;
    }
}

// domain/service/CouponDomainService.java
// 纯业务计算,无任何框架注解
public class CouponDomainService {
    public List<Coupon> filterAvailable(List<Coupon> coupons) {
        return coupons.stream()
                .filter(c -> c.getStatus() == CouponStatus.AVAILABLE)
                .collect(Collectors.toList());
    }
}

这一层没有 @Service、没有 @DubboService可以单独单元测试


2. Repository / Infrastructure 层(持久化 + RPC 通信)

java

复制

// infrastructure/repository/CouponRepositoryImpl.java
@Repository   // ← Spring 的,不是 Dubbo 的
public class CouponRepositoryImpl implements CouponRepository {

    @Autowired
    private CouponMapper couponMapper;   // MyBatis Mapper

    @Override
    public List<Coupon> findAvailableByUserId(Long userId) {
        return couponMapper.selectAvailableByUserId(userId)
                           .stream()
                           .map(CouponConverter::toDomain)
                           .collect(Collectors.toList());
    }

    @Override
    public void save(Coupon coupon) {
        couponMapper.updateStatus(coupon.getId(), coupon.getStatus().name());
    }
}

3. Application Service 层(事务边界 + 业务编排)

java

复制

// application/CouponAppService.java
@Service   // Spring Bean,不对外暴露 Dubbo
public class CouponAppService {

    @Autowired
    private CouponRepository     couponRepository;
    @Autowired
    private CouponDomainService  couponDomainService;

    /** 查询:无副作用,无事务 */
    public List<Coupon> queryAvailable(Long userId) {
        List<Coupon> all = couponRepository.findAvailableByUserId(userId);
        return couponDomainService.filterAvailable(all);
    }

    /** 使用券:有状态变更,加事务 */
    @Transactional
    public void useCoupon(Long userId, String couponCode) {
        Coupon coupon = couponRepository.findByUserIdAndCode(userId, couponCode)
                .orElseThrow(() -> new BizException("券不存在"));
        coupon.markUsed();           // Domain 规则校验
        couponRepository.save(coupon);
    }
}

4. Controller / Dubbo Provider 层(对外暴露接口)

java

复制

// api/dto/CouponDTO.java(放在公共 api 模块,Consumer 依赖)
public class CouponDTO implements Serializable {
    private Long   id;
    private String code;
    private BigDecimal discountAmount;
}

// api/CouponFacade.java(接口定义,放公共 api 模块)
public interface CouponFacade {
    List<CouponDTO> queryAvailable(Long userId);
    void            useCoupon(Long userId, String couponCode);
}

// interfaces/dubbo/CouponFacadeImpl.java  ← Dubbo Provider 实现
@DubboService(version = "1.0.0", group = "coupon")   // ← 只在这里出现 Dubbo 注解
public class CouponFacadeImpl implements CouponFacade {

    @Autowired
    private CouponAppService couponAppService;  // 调 AppService,不碰 Domain

    @Override
    public List<CouponDTO> queryAvailable(Long userId) {
        // 1. 参数校验
        Assert.notNull(userId, "userId 不能为空");
        // 2. 调用 AppService
        List<Coupon> coupons = couponAppService.queryAvailable(userId);
        // 3. 转 DTO 返回(不把领域对象透传出去)
        return CouponConverter.toDTOList(coupons);
    }

    @Override
    public void useCoupon(Long userId, String couponCode) {
        Assert.notNull(userId,     "userId 不能为空");
        Assert.hasText(couponCode, "couponCode 不能为空");
        couponAppService.useCoupon(userId, couponCode);
    }
}

二、Consumer 侧——订单服务

1. Infrastructure 层(RPC 通信,引用远端服务)

java

复制

// infrastructure/rpc/CouponRpcClient.java
@Component
public class CouponRpcClient {

    // @DubboReference 只在 Infrastructure 层出现
    @DubboReference(version = "1.0.0", group = "coupon",
                    timeout = 3000, retries = 2,
                    check = false)
    private CouponFacade couponFacade;   // 引用公共 api 模块的接口

    public List<CouponDTO> getAvailableCoupons(Long userId) {
        try {
            return couponFacade.queryAvailable(userId);
        } catch (RpcException e) {
            // 降级:返回空券列表,不阻断主流程
            log.warn("查询优惠券 RPC 失败,降级返回空列表 userId={}", userId, e);
            return Collections.emptyList();
        }
    }

    public void consumeCoupon(Long userId, String couponCode) {
        couponFacade.useCoupon(userId, couponCode);
    }
}

2. Application Service 层(业务编排,调 RPC Client)

java

复制

// application/OrderAppService.java
@Service
public class OrderAppService {

    @Autowired
    private OrderRepository  orderRepository;
    @Autowired
    private CouponRpcClient  couponRpcClient;   // 通过 Infrastructure 层调用,不直接用 Facade

    @Transactional
    public OrderResult createOrder(CreateOrderCmd cmd) {
        // 1. 查询可用券(RPC 调用在 Infrastructure 层)
        List<CouponDTO> coupons = couponRpcClient.getAvailableCoupons(cmd.getUserId());

        // 2. 匹配用户指定的券
        CouponDTO chosen = coupons.stream()
                .filter(c -> c.getCode().equals(cmd.getCouponCode()))
                .findFirst()
                .orElseThrow(() -> new BizException("券不可用"));

        // 3. 计算最终金额(Domain 层计算)
        Order order = OrderFactory.create(cmd, chosen);

        // 4. 持久化
        orderRepository.save(order);

        // 5. 核销券(RPC,跨服务写操作)
        couponRpcClient.consumeCoupon(cmd.getUserId(), cmd.getCouponCode());

        return OrderConverter.toResult(order);
    }
}

3. Controller 层(HTTP 入口,不含业务逻辑)

java

复制

// interfaces/http/OrderController.java
@RestController
@RequestMapping("/orders")
public class OrderController {

    @Autowired
    private OrderAppService orderAppService;

    @PostMapping
    public Result<OrderResult> create(@RequestBody @Validated CreateOrderRequest req) {
        CreateOrderCmd cmd = OrderConverter.toCmd(req);   // DTO → Command
        OrderResult    result = orderAppService.createOrder(cmd);
        return Result.ok(result);
    }
}

三、依赖关系图

┌─────────────────────────────────────────┐
│  公共 api 模块(coupon-api)             │
│  CouponFacade 接口 + CouponDTO           │
└───────────────┬─────────────────────────┘
                │ maven dependency
    ┌───────────┴──────────┐
    │                      │
┌───▼──────────┐    ┌──────▼────────────┐
│ 优惠券服务    │    │  订单服务          │
│ (Provider)   │    │  (Consumer)        │
│              │    │                    │
│ @DubboService│    │ @DubboReference    │
│ 只在 interfaces│  │ 只在 infrastructure│
│ /dubbo/ 层   │    │ /rpc/ 层           │
└──────────────┘    └────────────────────┘

四、关键原则总结

层次 Dubbo 注解 职责
Controller / @DubboService Provider 唯一出口 参数校验、DTO 转换、调 AppService
Application Service ❌ 无 Dubbo 事务边界、业务编排、调 RpcClient
Domain ❌ 无任何框架 纯业务规则、聚合根、领域服务
Infrastructure / @DubboReference Consumer 唯一入口 RPC 调用 + 降级兜底

一句话原则@DubboService 只在 Provider 的 interfaces 层;@DubboReference 只在 Consumer 的 infrastructure 层——Domain 和 AppService 永远不感知 Dubbo 的存在。

Application Service 层不直接调用facade,要封装一层RpcClient的四层用意:

一、最核心:依赖倒置,Application Service 不感知 RPC 框架

java

复制

// 如果 AppService 直接用 @DubboReference:
@Service
public class OrderAppService {
    @DubboReference(version="1.0.0", group="coupon", timeout=3000)
    private CouponFacade couponFacade;   // ← AppService 直接依赖 Dubbo

    // 问题:换成 gRPC/Feign/HTTP 时,AppService 代码必须改
}

// 封装 RpcClient 后:
@Service
public class OrderAppService {
    @Autowired
    private CouponRpcClient couponRpcClient;   // ← 只依赖自己定义的接口

    // 换框架只改 CouponRpcClient,AppService 零改动
}

这就是依赖倒置原则(DIP):AppService 依赖"自己定义的抽象",而不是"Dubbo 的具体实现"。


二、降级兜底逻辑的归宿

降级属于"技术容错",不是业务逻辑,不该放在 AppService

java

复制

// ❌ 错误做法:降级逻辑污染 AppService
public OrderResult createOrder(CreateOrderCmd cmd) {
    List<CouponDTO> coupons;
    try {
        coupons = couponFacade.queryAvailable(cmd.getUserId());  // Dubbo 调用
    } catch (RpcException e) {
        coupons = Collections.emptyList();   // 这行不该在 AppService 里
    }
    // 继续业务编排...
}

// ✅ 正确做法:降级封在 RpcClient,AppService 看不见
public OrderResult createOrder(CreateOrderCmd cmd) {
    // 直接用,不管 RPC 是否失败——失败已在 RpcClient 里降级了
    List<CouponDTO> coupons = couponRpcClient.getAvailableCoupons(cmd.getUserId());
    // 继续业务编排...
}

三、参数拼装、超时重试等技术细节的收口

RPC 调用往往有很多技术参数:

java

复制

// CouponRpcClient 统一管理这些技术关注点
@DubboReference(version = "1.0.0", group = "coupon",
                timeout = 3000, retries = 2,       // ← 超时、重试
                loadbalance = "roundrobin",         // ← 负载均衡
                check = false)                      // ← 启动不检查
private CouponFacade couponFacade;

public List<CouponDTO> getAvailableCoupons(Long userId) {
    // 参数转换、日志打点、链路追踪 tag 也在这里统一做
    RpcContext.getContext().setAttachment("traceId", MDC.get("traceId"));
    return couponFacade.queryAvailable(userId);
}

这些如果散落在 AppService 里,每个调用点都要重复写,且改动时要找所有散落点。


四、单元测试的可 Mock 性

java

复制

// AppService 的单元测试,直接 Mock RpcClient
@ExtendWith(MockitoExtension.class)
class OrderAppServiceTest {

    @Mock
    private CouponRpcClient couponRpcClient;   // ← 轻松 Mock,无需启动 Dubbo
    @InjectMocks
    private OrderAppService orderAppService;

    @Test
    void createOrder_withValidCoupon_success() {
        when(couponRpcClient.getAvailableCoupons(anyLong()))
            .thenReturn(List.of(mockCoupon()));
        // 直接测业务逻辑,不依赖真实 RPC 环境
    }
}

// 如果 AppService 直接用 @DubboReference,
// 单元测试就必须启动 Dubbo 容器,变成集成测试,极慢

总结

用意 封装 RpcClient 解决了什么
依赖倒置 AppService 不绑定 Dubbo,换框架零改动
降级归宿 try/catch/fallback 收口在 Infrastructure,不污染业务层
技术参数收口 timeout/retry/traceId 统一管理,不散落各处
可测试性 AppService 单测只需 Mock 接口,无需启动 RPC 容器

一句话:RpcClient 是 Infrastructure 层的"防腐层"——它把 Dubbo 的技术细节全部挡在外面,让 AppService 只看见"我需要一个查券的能力",而不是"我在用 Dubbo 调用一个版本号 1.0.0 的远端服务"。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐