摘要
文章通过对 MVC 三层架构和 DDD 四层架构的深入分析,结合具体的代码示例和项目结构设计,为 Java 开发人员提供了全面的系统架构分层设计经验。在实际项目中,开发人员应根据项目的规模和业务复杂度选择合适的架构模式,并遵循文中提出的最佳实践建议,以提高代码的可维护性和系统的扩展性。
1. MVC三层架构(Controller-Service-DAO)
MVC(Model-View-Controller)是常见的分层架构,在后端应用中通常演变为三层架构(Controller-Service-DAO)。每一层的主要职责和具体流程如下:
1.1. Controller 层(接收请求、参数校验、封装传输)
主要职责:
- 接收请求(解析
HTTP
请求) - 参数校验(基本数据校验,如
@Valid
、@RequestParam
) - VO(View Object) -> DTO(Data Transfer Object)(将前端传输的数据转换为业务层可用的数据)
- 调用 Service 层
- DTO -> VO(将
Service
处理后的数据转换为前端需要的格式) - 返回
Response
具体流程:
@RestController
@RequestMapping("/user")
@Slf4j // Lombok 自动生成 Logger
public class UserController {@Autowiredprivate UserService userService;/*** 用户注册接口*/@PostMapping("/register")public ResponseEntity<?> register(@RequestBody @Valid UserVO userVO, BindingResult bindingResult) {try {// 1. 参数校验if (bindingResult.hasErrors()) {return ResponseEntity.badRequest().body(bindingResult.getAllErrors());}// 2. 空值检查,防止 NPEif (userVO == null) {return ResponseEntity.badRequest().body("请求数据不能为空");}// 3. VO -> DTOUserDTO userDTO = UserConverter.voToDto(userVO);// 4. 调用 Service 层UserDTO savedUser = userService.register(userDTO);if (savedUser == null) {return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("用户注册失败");}// 5. DTO -> VOUserVO responseVO = UserConverter.dtoToVo(savedUser);// 6. 成功返回return ResponseEntity.ok(responseVO);} catch (RuntimeException e) {log.error("用户注册异常: {}", e.getMessage(), e);return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("系统错误,请稍后重试");}}
}
进一步优化:全局异常处理
以使用 @RestControllerAdvice
统一管理异常:
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {@ExceptionHandler(RuntimeException.class)public ResponseEntity<String> handleRuntimeException(RuntimeException e) {log.error("系统异常: {}", e.getMessage(), e);return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("系统错误,请稍后重试");}
}
这样 Controller
里就不需要 try-catch
了,代码更清爽!
在 Spring Boot
开发中,ResponseEntity
是一个强大的返回封装工具,可以用于返回 HTTP 状态码、响应体和 HTTP 头信息。
设计一个通用的 ResponseEntity
结构,主要目标是:
✅ 统一返回格式(避免不同接口返回不同格式的数据)
✅ 携带状态码和错误信息(方便前端解析)
✅ 支持成功 & 失败(包含数据或错误消息)
1.1.1. 统一响应封装类
我们可以创建一个通用的 Response<T>
结构,封装所有接口的返回值:
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;@Data
@NoArgsConstructor
@AllArgsConstructor
public class ApiResponse<T> {private int code; // 状态码private String message; // 提示信息private T data; // 返回数据// 成功返回public static <T> ApiResponse<T> success(T data) {return new ApiResponse<>(200, "操作成功", data);}// 成功返回(带自定义消息)public static <T> ApiResponse<T> success(String message, T data) {return new ApiResponse<>(200, message, data);}// 失败返回public static <T> ApiResponse<T> fail(int code, String message) {return new ApiResponse<>(code, message, null);}
}
1.1.2. 在Controller
中使用
使用 ResponseEntity<ApiResponse<T>>
进行返回:
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {@Autowiredprivate UserService userService;/*** 用户注册接口*/@PostMapping("/register")public ResponseEntity<ApiResponse<UserVO>> register(@RequestBody @Valid UserVO userVO, BindingResult bindingResult) {try {// 1. 参数校验if (bindingResult.hasErrors()) {return ResponseEntity.badRequest().body(ApiResponse.fail(400, bindingResult.getAllErrors().get(0).getDefaultMessage()));}// 2. 空值检查if (userVO == null) {return ResponseEntity.badRequest().body(ApiResponse.fail(400, "请求数据不能为空"));}// 3. VO -> DTOUserDTO userDTO = UserConverter.voToDto(userVO);// 4. 调用 Service 层UserDTO savedUser = userService.register(userDTO);if (savedUser == null) {return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ApiResponse.fail(500, "用户注册失败"));}// 5. DTO -> VOUserVO responseVO = UserConverter.dtoToVo(savedUser);// 6. 成功返回return ResponseEntity.ok(ApiResponse.success(responseVO));} catch (RuntimeException e) {log.error("用户注册异常: {}", e.getMessage(), e);return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ApiResponse.fail(500, "系统错误,请稍后重试"));}}
}
1.1.3. 统一异常处理(推荐)
可以使用 @RestControllerAdvice
统一管理异常,减少 try-catch
代码:
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {@ExceptionHandler(RuntimeException.class)public ResponseEntity<ApiResponse<String>> handleRuntimeException(RuntimeException e) {log.error("系统异常: {}", e.getMessage(), e);return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ApiResponse.fail(500, "系统错误,请稍后重试"));}@ExceptionHandler(MethodArgumentNotValidException.class)public ResponseEntity<ApiResponse<String>> handleValidationException(MethodArgumentNotValidException e) {String errorMsg = e.getBindingResult().getAllErrors().get(0).getDefaultMessage();return ResponseEntity.badRequest().body(ApiResponse.fail(400, errorMsg));}
}
这样设计的好处
✅ 统一返回格式:前端解析 JSON 结构一致
✅ 简化 Controller 代码:减少 ResponseEntity
代码重复
✅ 支持全局异常处理:自动捕获 RuntimeException
和参数校验异常
✅ HTTP 状态码 & 业务状态码分离:既有 HTTP Status Code
,又有 code
业务状态码
1.1.4. 前端收到的 JSON 示例
成功返回
{"code": 200,"message": "操作成功","data": {"id": 1,"username": "zhangsan"}
}
失败返回
{"code": 400,"message": "用户名不能为空","data": null
}
1.1.5. 响应设计总结
🚀 推荐做法
- 封装
ApiResponse<T>
统一返回格式 - Controller 直接返回
ResponseEntity<ApiResponse<T>>
- 使用
@RestControllerAdvice
统一异常处理
这种方式可读性高、扩展性强,更适用于企业级项目
1.2. Service 层(业务逻辑、事务控制、调用 DAO)
主要职责:
- 业务逻辑处理
- 数据转换(DTO -> DO(Domain Object))
- 事务控制(
@Transactional
) - 调用 DAO 层
具体流程:
@Service
public class UserService {@Autowiredprivate UserDAO userDAO;@Transactionalpublic UserDTO register(UserDTO userDTO) {// 1. DTO -> DO 转换UserDO userDO = UserConverter.dtoToDo(userDTO);// 2. 业务逻辑(如校验用户名是否重复)if (userDAO.findByUsername(userDO.getUsername()) != null) {throw new RuntimeException("用户名已存在");}// 3. 调用 DAO 层存储数据UserDO savedUser = userDAO.save(userDO);// 4. DO -> DTO 转换return UserConverter.doToDto(savedUser);}
}
1.3. DAO 层(数据库操作)
主要职责:
- 封装数据库访问
- 增删改查数据库
- 与 ORM(如 MyBatis、JPA)结合
具体流程:
@Repository
public interface UserDAO extends JpaRepository<UserDO, Long> {UserDO findByUsername(String username);
}
1.4. 三层架构总结
层级 | 主要职责 | 关键步骤 |
Controller | 处理请求,参数校验,VO/DTO转换 | 1. 接收请求 2. 参数校验 3. VO → DTO 4. 调用 Service 5. DTO → VO |
Service | 业务逻辑,事务控制,调用 DAO | 1. DTO → DO 2. 业务校验 3. 调用 DAO 4. DO → DTO |
DAO | 访问数据库 | 1. 增删改查数据库 2. ORM 3. 封装 SQL |
2. DDD 四层架构(接口层-应用层-领域层-基础设施层)
DDD(Domain-Driven Design,领域驱动设计)相比 MVC 三层架构,更加强调业务逻辑和领域模型的分离,通常采用四层架构:
2.1. 接口层(Interface Layer)
职责:
- 处理 HTTP 请求(Controller 层)
- 参数校验
- DTO 转换
- 调用应用层
- DTO -> VO 转换
- 返回
Response
示例代码:
@RestController
@RequestMapping("/order")
public class OrderController {@Autowiredprivate OrderApplicationService orderApplicationService;@PostMapping("/create")public ResponseEntity<OrderVO> createOrder(@RequestBody @Valid OrderVO orderVO) {OrderDTO orderDTO = OrderConverter.voToDto(orderVO);OrderDTO createdOrder = orderApplicationService.createOrder(orderDTO);return ResponseEntity.ok(OrderConverter.dtoToVo(createdOrder));}
}
2.2. 应用层(Application Layer)
职责:
- 编排业务流程(调用多个领域服务)
- 事务管理
- 调用领域层
- DTO 转换
- 不包含具体业务逻辑
示例代码:
@Service
public class OrderApplicationService {@Autowiredprivate OrderDomainService orderDomainService;@Transactionalpublic OrderDTO createOrder(OrderDTO orderDTO) {Order order = OrderConverter.dtoToDomain(orderDTO);Order createdOrder = orderDomainService.createOrder(order);return OrderConverter.domainToDto(createdOrder);}
}
2.3. 领域层(Domain Layer)
职责:
- 核心业务逻辑
- 领域对象(Entity、Value Object、Aggregate)
- 领域服务(Domain Service)
- 业务规则
示例代码:
@Service
public class OrderDomainService {@Autowiredprivate OrderRepository orderRepository;public Order createOrder(Order order) {if (order.getTotalAmount() <= 0) {throw new IllegalArgumentException("订单金额必须大于 0");}return orderRepository.save(order);}
}
2.4. 基础设施层(Infrastructure Layer)
职责:
- 数据库访问
- 外部 API 调用
- 缓存、消息队列
示例代码:
@Repository
public interface OrderRepository extends JpaRepository<Order, Long> {
}
2.5. DDD 四层架构总结
层级 | 主要职责 | 关键步骤 |
接口层(Interface) | 处理 HTTP 请求,参数校验,VO/DTO转换 | 1. 接收请求 2. 参数校验 3. VO → DTO 4. 调用应用层 5. DTO → VO |
应用层(Application) | 业务流程编排,调用领域层 | 1. DTO → 领域对象 2. 事务管理 3. 调用领域层 4. 领域对象 → DTO |
领域层(Domain) | 业务逻辑,领域模型,业务规则 | 1. 领域对象封装 2. 业务校验 3. 领域服务 |
基础设施层(Infrastructure) | 访问数据库,缓存,外部 API | 1. 数据持久化 2. 外部 API 3. 消息队列 |
2.6. MVC 三层架构 vs DDD 四层架构总结
对比项 | MVC 三层架构 | DDD 四层架构 |
关注点 | 控制器、服务、数据库 | 业务逻辑和领域建模 |
适用场景 | 适合小型项目 | 适合复杂业务系统 |
代码组织 | 业务逻辑混杂在 Service 层 | 业务逻辑封装在领域层 |
扩展性 | 难以扩展 | 易扩展,符合业务变化 |
总结
- MVC 三层架构:适用于简单系统,关注数据流。
- DDD 四层架构:适用于复杂业务,关注业务逻辑。
🚀 建议:
- 小项目用 MVC,简单快捷。
- 复杂业务用 DDD,更清晰、可扩展。
3. DDD 项目结构目录
src/
└── main/├── java/com/example/project/│ ├── domain/ // 领域层│ │ ├── model/ // 聚合根、实体、值对象│ │ ├── service/ // 领域服务│ │ ├── event/ // 领域事件│ │ └── repository/ // 仓储接口│ ├── application/ // 应用层│ │ ├── service/ // 应用服务│ │ ├── command/ // 命令对象│ │ └── query/ // 查询对象│ ├── interfaces/ // 接口层│ │ ├── web/ // Controller 层(HTTP API)│ │ ├── dto/ // 数据传输对象│ │ └── assembler/ // DTO 与领域对象的转换│ └── infrastructure/ // 基础设施层│ ├── config/ // 配置类│ ├── persistence // 持久化实现(仓储实现类、JPA、MyBatis 等)│ ├── messaging/ // 消息队列│ └── external/ // 调用外部系统└── resources/├── application.yml // 配置文件└── db/ // 数据库脚本(Flyway 或 Liquibase)
4. DDD层级调用关系总结
- Controller 接收请求并将请求转发给 Facade 层或 ServiceImpl 层。
- Facade 层调用多个 ServiceImpl 实现类来完成复杂的业务逻辑处理。
- ServiceImpl 层负责处理具体的业务逻辑,并通过 DAO 层与数据库交互。
- DAO 层直接与数据库交互,执行数据的CRUD操作。
4.1. DDD中Facade层和Application层关系
在领域驱动设计(DDD) 中,Facade层 和 Application层 有着不同的职责和作用,但它们之间存在一定的联系。我们可以通过明确它们的职责和调用关系来理解它们在整体架构中的角色。
4.1.1. Facade 层的作用
Facade 层主要是一个简化接口的层级,它提供对复杂子系统或多个服务的统一接口。在 DDD 中,Facade 通常用于隐藏系统内部的复杂性,对外暴露简洁的接口,方便外部系统(例如 API 层或 UI 层)调用。Facade 层通常封装多个领域服务或子系统的调用,提供一个高层次的接口。
- Facade 层的核心职责:
-
- 简化外部访问:通过提供统一的接口,隐藏内部复杂性。
- 协调多个子系统或领域服务:它会协调领域服务、聚合根和其他模块,执行一组任务或一个业务流程。
- 隔离业务逻辑和客户端:它隔离了外部客户端与复杂的业务逻辑,避免外部系统直接接触领域层的细节。
示例:在一个电商系统中,Facade 层可能会提供一个简化的订单处理接口,协调库存、支付、物流等多个子系统的调用。
@Service
public class OrderFacade {@Autowiredprivate InventoryService inventoryService;@Autowiredprivate PaymentService paymentService;@Autowiredprivate ShippingService shippingService;public void placeOrder(String productId, String userId, double amount) {if (inventoryService.checkStock(productId)) {if (paymentService.processPayment(userId, amount)) {shippingService.createShipment(productId, userId);}}}
}
4.1.2. Application 层的作用
Application 层负责协调和组织领域模型来完成业务需求,它是面向业务用例的,通常包含对多个领域服务和领域对象的调用。Application 层的作用是封装业务逻辑的流程,将用户需求(如订单处理、支付等)转化为领域层的操作。
Application 层的核心职责:
- 业务用例:将用户需求或业务流程转化为用例,组织领域层的操作。
- 领域服务和聚合根的调用:组织领域服务、聚合根和实体之间的交互来实现具体的业务逻辑。
- 无业务逻辑实现:Application 层不包含具体的业务逻辑,所有业务逻辑都应该存在于领域层。
示例:在同一个电商系统中,Application 层可能会定义一个订单处理的用例,处理订单的创建、支付等操作,但不会直接处理库存管理、支付处理等具体的业务逻辑。
@Service
public class OrderApplicationService {@Autowiredprivate InventoryService inventoryService;@Autowiredprivate PaymentService paymentService;@Autowiredprivate OrderService orderService;public void createOrder(String productId, String userId, double amount) {if (inventoryService.checkStock(productId)) {if (paymentService.processPayment(userId, amount)) {orderService.createOrder(userId, productId, amount);}}}
}
4.1.3. Facade 层与 Application 层的调用关系
Facade 层是对外的接口层:它通常位于应用系统的最外层,为客户端或外部系统提供简单、统一的调用接口。Facade 层不直接包含业务逻辑,而是组织多个应用服务或领域服务的调用,屏蔽业务复杂性。
Application 层是业务逻辑的执行者:它处理具体的业务用例,调用领域服务、聚合根和实体等来实现业务逻辑。它处理核心的业务逻辑和流程。
Facade 调用 Application 层:
- Facade 层会调用 Application 层来执行某些业务用例。因为 Facade 层是对外暴露接口的层,它将用户请求转发给 Application 层,由 Application 层来处理业务逻辑。
- Facade 层的主要作用是简化客户端的调用,它协调多个应用服务的调用,并向外部系统提供更简洁的接口。
Application 层调用领域层:
- Application 层会调用领域服务、聚合根和实体,执行业务逻辑的实现。Application 层本身不会实现业务逻辑,而是组织和调度领域层的操作。
示例:假设有一个电商系统,Facade 层提供了统一的订单处理接口,而 Application 层负责组织具体的业务逻辑,并调用领域层来完成订单创建的工作。
// Facade 层 - 提供简化接口
@Service
public class OrderFacade {@Autowiredprivate OrderApplicationService orderApplicationService;public void placeOrder(String productId, String userId, double amount) {orderApplicationService.createOrder(productId, userId, amount);}
}
// Application 层 - 执行业务用例
@Service
public class OrderApplicationService {@Autowiredprivate InventoryService inventoryService;@Autowiredprivate PaymentService paymentService;@Autowiredprivate OrderService orderService;public void createOrder(String productId, String userId, double amount) {if (inventoryService.checkStock(productId)) {if (paymentService.processPayment(userId, amount)) {orderService.createOrder(userId, productId, amount);}}}
}
在这个例子中,OrderFacade
是面向外部客户端的接口层,它调用 OrderApplicationService
来完成订单处理的业务流程。
4.2. 外部调用日志记录的最佳实践
4.2.1. Facade 层:
- 适合记录:请求进入、请求时间、外部接口的响应时间、接口调用的成功与失败状态。
- 为什么:Facade 层是外部系统与内部应用系统的接口,它负责接收和转发请求,因此在此层记录日志可以帮助我们了解外部请求的基本情况以及接口的性能。
4.2.2. Application 层:
- 适合记录:业务流程中的每一个重要步骤,比如库存检查、支付处理、订单创建等。
- 为什么:Application 层负责处理具体的业务逻辑,记录该层的日志可以帮助我们跟踪业务流程的每个阶段是否成功,诊断业务逻辑的错误或瓶颈。
4.2.3. 结论:Facade 层还是 Application 层记录日志?
- Facade 层 主要用于记录外部请求的相关日志,如请求的进入、时间、成功或失败的状态等,通常用于对外接口的监控和审计。
- Application 层 更专注于记录业务流程中的详细日志,帮助开发人员理解业务逻辑的执行过程。
因此,外部调用的日志记录通常应该在 Facade 层进行,因为 Facade 层是接收外部请求的入口,记录请求的相关信息可以帮助监控和诊断外部系统与内部服务的交互。而对于更细粒度的业务日志,则可以在 Application 层中记录,以便深入分析业务流程的执行情况。
5. Domain层设计
Domain 层是领域驱动设计(DDD)中的核心层,负责实现 业务逻辑和业务规则。它代表了系统中的实际业务领域,聚焦于解决实际的业务问题。领域层包含领域模型、领域服务以及聚合根等重要概念,是 DDD 的核心部分。
在DDD 中,Domain 层的设计非常重要,因为它决定了系统的业务能力和可扩展性。一个好的领域设计能够确保系统的业务逻辑高度聚焦、易于维护并具备很好的灵活性。
5.1. Domain层主要组成部分
领域模型(Domain Model)
- 实体(Entity):具有唯一标识符的对象,通常代表某个具有生命周期和状态的业务概念。实体的状态是可变的,且通过标识符(ID)来区分不同的实例。
- 值对象(Value Object):没有唯一标识符的对象,通常是一些不可变的、纯粹用来表示某些属性的对象。它们通常用于描述某种概念,且没有独立的生命周期。
- 聚合(Aggregate):是领域模型的一个集合,包含一个根实体(聚合根)和一组与其关联的实体和值对象。聚合通过聚合根来管理和维护一致性。
- 聚合根(Aggregate Root):聚合根是聚合的入口点,也是唯一可以从外部访问的实体。通过聚合根访问其他实体或值对象。
- 领域服务(Domain Service)
- 当某个业务逻辑无法简单地归属于某个实体或值对象时,领域服务应当被引入。它通常处理一些跨多个实体的复杂业务逻辑,或者实现某些业务规则。
- 领域事件(Domain Event)
- 领域事件表示系统中某个重要事件的发生,这些事件通常是系统状态变化的结果。在某些情况下,领域事件也会触发其他操作。
- 仓储(Repository)
- 仓储是与数据持久化相关的服务,它提供了对领域对象的持久化操作。仓储可以让你从数据库或其他存储介质中加载或保存聚合根对象。
5.2. Domain层的设计要点
在设计 Domain 层 时,通常遵循以下原则和步骤:
5.2.1. 建模业务概念
- 聚焦于业务领域:Domain 层的设计应紧密围绕真实世界的业务需求进行建模,确保领域模型代表了实际业务中的概念。
- 领域语言(Ubiquitous Language):团队成员(包括开发人员、业务人员等)应使用一致的语言进行沟通,这种语言在代码中得以体现,确保业务需求和技术实现之间的一致性。
5.2.2. 定义实体与聚合
- 实体:在设计实体时,确保它们有唯一标识符,并且是业务流程中具有重要意义的对象。例如,
Order
可以是一个实体,它有一个唯一的标识符(订单ID),并且有一些可变状态(例如订单状态、支付状态)。 - 聚合根:聚合根是聚合的入口点,它确保聚合内的所有业务规则和一致性得以维护。例如,
Order
可以是一个聚合根,它控制订单中其他子实体(如商品、订单项等)的访问和操作。
5.2.3. 设计领域服务
- 领域服务负责处理一些跨多个实体的业务逻辑,通常不能将这些逻辑归属于某个单一的实体或值对象。
- 例如,在电商系统中,
OrderService
可能会涉及订单的创建、支付等业务逻辑,这些逻辑可能涉及多个实体(如Order
、Payment
等)。
5.2.4. 定义领域事件
- 领域事件用于表示系统中某些重要事件的发生,通常在业务逻辑中某些操作完成后,系统会生成领域事件来通知其他部分。例如,
OrderCreatedEvent
是一个领域事件,当订单被创建时会触发该事件,其他模块可以根据该事件来执行后续的操作,如发送通知或进行库存检查。
5.2.5. 设计仓储(Repository)
- Repository 负责对象的持久化操作,它提供了访问和存储聚合根的方法。通常,Repository 与数据库或其他存储系统的交互是透明的。它隐藏了数据访问的细节,允许业务逻辑专注于核心业务功能。
- 例如,在电商系统中,
OrderRepository
用来加载和存储订单聚合。
5.3. 电商系统中的 Domain 层设计
假设我们正在设计一个电商系统中的订单模块,Domain 层的设计可以如下:
5.3.1. 实体:Order
public class Order {private String id; // 订单IDprivate List<OrderItem> items; // 订单项private OrderStatus status; // 订单状态public Order(String id, List<OrderItem> items) {this.id = id;this.items = items;this.status = OrderStatus.PENDING;}public void addItem(OrderItem item) {this.items.add(item);}public void changeStatus(OrderStatus status) {this.status = status;}// 其他业务方法
}
5.3.2. 值对象:OrderItem
public class OrderItem {private String productId;private int quantity;private BigDecimal price;public OrderItem(String productId, int quantity, BigDecimal price) {this.productId = productId;this.quantity = quantity;this.price = price;}// 其他业务方法
}
5.3.3. 聚合根:Order
Order
作为聚合根,负责管理订单中的所有订单项 OrderItem
。外部系统只能通过 Order
来访问和操作订单项。
5.3.4. 领域服务:OrderService
public class OrderService {private OrderRepository orderRepository;public OrderService(OrderRepository orderRepository) {this.orderRepository = orderRepository;}public void createOrder(String id, List<OrderItem> items) {Order order = new Order(id, items);orderRepository.save(order);}public void cancelOrder(String orderId) {Order order = orderRepository.findById(orderId);order.changeStatus(OrderStatus.CANCELED);orderRepository.save(order);}
}
5.3.5. 领域事件:OrderCreatedEvent
public class OrderCreatedEvent {private String orderId;public OrderCreatedEvent(String orderId) {this.orderId = orderId;}// 事件处理逻辑
}
5.3.6. 仓储:OrderRepository
public interface OrderRepository {void save(Order order);Order findById(String orderId);
}
5.4. 如何设计Domain层
- 领域模型:围绕业务需求进行建模,设计实体、值对象和聚合。确保领域模型表达了业务领域中的核心概念。
- 领域服务:将一些复杂的业务逻辑从实体中抽离,放入领域服务中处理,跨实体的业务操作通过领域服务来协调。
- 仓储:负责将聚合根持久化,提供查找、保存、删除等功能。
- 领域事件:用于表示和处理系统中发生的重要事件。
设计 Domain 层 时,最重要的是确保模型能够真实地反映业务需求,遵循领域语言原则,让开发人员和业务人员使用一致的术语进行交流。
6. DDD中对象定义和使用
- DO(Domain Object):领域对象,用于描述业务领域中的实体,是持久化对象(PO)的进一步抽象。DO 通常与数据库中的表结构相对应,但包含的业务逻辑和方法会更多。
- DTO(Data Transfer Object):数据传输对象,用于在不同的层之间传递数据。DTO 通常是无行为的,主要用于将数据从一层传输到另一层(如从Controller到Service)。DTO 常常用于减少网络开销和提升传输效率。
- BO(Business Object):业务对象,封装了业务逻辑或可以进行业务操作的对象,通常在 Service 层进行操作。BO 与 DO 不同的是,BO 包含了业务逻辑,而 DO 更多地用于持久化操作。
- AO(Application Object):应用对象,与应用层紧密相关的对象,通常用于服务层与界面层之间的数据传递。AO 可以理解为操作层与接口层之间的桥梁。
- VO(View Object):视图对象,用于展示层,将数据从后端传递到前端显示。VO 通常只包含与 UI 显示相关的数据。
- POJO(Plain Old Java Object):简单旧式Java对象,是最普通的 Java 对象,没有继承或实现任何其他类或接口。POJO 在很多场景下都作为基础对象使用,并通过其他对象类型封装实现更多功能。
6.1. 对象存放的目录和位置
在领域驱动设计(DDD)中,常见的对象类型如领域对象(Domain Objects,DO)、值对象(Value Objects,VO)、业务对象(Business Objects,BO)、数据传输对象(Data Transfer Objects,DTO)、普通Java对象(Plain Old Java Objects,POJO)等,通常需要被组织和存放在适当的目录中。
领域对象(Domain Objects,DO):
- 放置在
domain
、entities
或models
等目录中。 - 例如:
src/main/java/com/example/domain/model/entities/
值对象(Value Objects,VO):
- 放置在
valueobjects
、vo
或values
等目录中。 - 例如:
src/main/java/com/example/domain/model/valueobjects/
业务对象(Business Objects,BO):
- 放置在
business
、services
或managers
等目录中。 - 例如:
src/main/java/com/example/domain/service/
数据传输对象(Data Transfer Objects,DTO):
- 放置在
dto
或request/response
等目录中。 - 例如:
src/main/java/com/example/dto/
普通Java对象(Plain Old Java Objects,POJO):
- 可以放置在通用的目录中,比如
model
、common
等。 - 例如:
src/main/java/com/example/model/
7. 数据校验在Facade层还是controller层
通常情况下,在 Controller层进行输入数据校验已经能够保证用户输入的合法性,因为 Controller 直接与外部接口交互,通常会在这一层进行参数校验、数据格式验证等操作。然而,在FacadeImpl 层是否还需要进行数据校验,取决于以下几种场景和考量:
7.1. 安全性和数据一致性
- 防御式编程:即使在 Controller 层已经做了校验,FacadeImpl 也可以进行关键性数据的二次校验,确保即使数据来源于其他调用(例如内部服务调用)时,依然能保证数据的合法性。这种做法可以增强系统的健壮性。
- 安全性考虑:如果 FacadeImpl 直接与其他服务交互,或者涉及外部 API 调用,进一步的校验可以防止绕过 Controller 的直接调用。例如,某些请求可能直接来自内部系统或其他应用,而非用户界面的直接请求。
7.2. 不同层次的职责分离
- Controller 层 主要关注用户输入的基本校验,通常使用注解方式如
@Valid
、@NotNull
等对数据进行格式、范围等简单的校验。 - FacadeImpl 层 可以进行业务逻辑相关的校验,如根据业务场景判断输入参数的合理性、是否满足业务规则。例如,在 FacadeImpl 中检查某个业务对象是否处于某个状态,是否可以进行某种操作等。
7.3. 系统扩展性
- 如果某些方法可能通过非 HTTP 请求调用(例如消息队列、内部服务间调用等),这些调用可能绕过 Controller,因此在 FacadeImpl 层做校验可以确保不同来源的请求都经过必要的验证。
- 如果将来扩展需要,不同的调用来源可能需要不同的校验逻辑,那么在 FacadeImpl 层进行进一步校验会提供更好的灵活性。
7.4. 避免重复校验的策略
- 不必要的重复校验:如果同样的数据校验已经在 Controller 层充分完成,并且 FacadeImpl 只是直接调用相关服务进行业务逻辑处理,那么可以避免重复校验,减少性能开销。
- 必要的校验:如果 FacadeImpl 层涉及复杂的业务逻辑或跨服务调用,那么业务逻辑层面的校验仍然有必要。
7.5. 建议:
- 简单输入校验:如空值、长度、格式等,可以在 Controller 层完成。
- 业务逻辑校验:如业务规则、状态校验,可以放在 FacadeImpl 层。
- 防御性编程:如果有潜在的非 Controller 调用,FacadeImpl 层应适当添加关键性数据的校验。
因此,FacadeImpl 层是否需要校验取决于你是否希望在业务逻辑层面做额外的安全性、业务规则或防御性验证。
8. DAO类和Repository类作用是不是一样?
DAO(Data Access Object)类和Repository类的作用非常相似,但在不同的编程模式或框架中有一些细微差异。
DAO 类:
- DAO 是一种设计模式,专门用于数据访问层,负责与数据库进行直接交互。它主要关注数据的持久化和查询,为上层提供简单的接口来操作数据库,而不涉及业务逻辑。
- DAO 的常见方法包括增删改查(CRUD)操作,例如
insert()
,update()
,delete()
,select()
等。 - 通常与 SQL 语句或 ORM(如 MyBatis, Hibernate)配合使用。
public class UserDao {public User getUserById(Long id) {// 数据库查询操作}
}
Repository 类:
- Repository 是在领域驱动设计(DDD)中常用的模式,作用是封装数据访问层,并且更注重与领域模型的交互,通常与业务逻辑紧密结合。它不只是简单的持久化操作,还会根据领域的需求进行特定的数据查询。
- Repository 类通常会比 DAO 更加抽象化,并与业务逻辑更为关联。
- Repository 通常配合 ORM(如 Spring Data JPA)使用,能提供自动生成的查询方法。
public interface UserRepository extends JpaRepository<User, Long> {// 自定义查询方法User findByUsername(String username);
}
DAO类 和Repository类区别
- 关注点不同:DAO 关注数据访问的实现,通常是直接的数据库交互。Repository 关注领域模型的管理,与业务逻辑结合更紧密。
- 使用场景不同:DAO 更偏向于传统的三层架构,Repository 则是 DDD 领域驱动设计中的概念。
- 抽象层次:Repository 更抽象,能自动生成查询逻辑,而 DAO 需要手动编写。
总结来说,虽然两者的核心作用是数据访问,但 DAO 更加细粒度,关注数据库层的操作,Repository 更加高层次,倾向于领域对象和业务需求的结合。
9. 防腐层的定义是不是可以理解为DDD中Facade层
是的,可以将防腐层(Anti-Corruption Layer, ACL)在一定程度上理解为与DDD(领域驱动设计)中的 Facade 层类似,但它们在概念和用途上仍然有一些区别。以下是详细的对比和解释
9.1. 防腐层(ACL)的定义与作用:
定义:防腐层是领域驱动设计中一个重要的设计模式,用于隔离领域模型和外部系统(如第三方服务、遗留系统等)。其目的是防止外部系统的复杂性、变化或者设计缺陷直接侵蚀领域模型。
作用
- 隔离外部依赖:将外部系统的数据和逻辑转换成符合领域模型的结构。
- 消化复杂性:通过适配器或转换器屏蔽外部系统的细节(如外部系统的数据格式或调用协议)。
- 保护领域模型:避免外部系统的设计问题影响到核心领域逻辑。
- 增强灵活性:在外部系统变更时,只需要修改防腐层,而不是修改核心领域模型。
实现
- 包含适配器、DTO(数据传输对象)、转换器等。
- 实现调用外部系统的远程接口,通常通过 HTTP、RPC 或消息队列通信。
9.2. Facade层的定义与作用:
定义:Facade 是一种设计模式,常用于为一个复杂的子系统提供一个统一的接口,从而简化客户端对该子系统的使用。
作用
- 简化接口:为客户端提供一个简化的、易于理解的接口,隐藏子系统的复杂实现。
- 聚合功能:可以将多个子系统的功能封装到一个接口中,减少客户端与多个模块之间的直接交互。
- 不一定关注隔离:Facade 的重点是简化复杂性,不一定涉及领域模型保护。
实现
- 主要用于聚合逻辑,隐藏实现细节。
- 通常直接依赖内部模块,和外部系统的交互可能较少。
9.3. 防腐层与Facade 的区别:
特性 | 防腐层(ACL) | Facade 层 |
核心目标 | 隔离外部系统的复杂性,保护领域模型 | 简化子系统接口,为客户端提供统一的访问入口 |
关注点 | 外部依赖的适配与转换 | 聚合和简化内部模块的逻辑 |
是否处理领域模型 | 是,负责保护领域模型免受外部依赖的侵蚀 | 否,通常与领域模型关系较弱 |
典型实现 | 包含适配器、DTO、转换器,依赖外部服务 | 聚合多个模块,通常不涉及外部系统 |
调用方向 | 面向外部(如第三方服务、遗留系统) | 面向内部(如应用内部的模块) |
10. 防腐层是不是远程调用接口层?
不完全是。虽然防腐层经常负责调用远程接口,但它的核心功能不仅仅是调用外部系统,而是屏蔽外部系统的复杂性和变化。因此,防腐层除了远程调用功能,还需要处理以下内容:
- 数据格式的转换(外部模型 -> 领域模型)。
- 兼容性问题(如适配多个版本的外部接口)。
- 错误和异常处理。
简单来说,防腐层是一种抽象层,远程调用只是它的一部分实现。
总结:防腐层在功能上和 DDD 中的 Facade 层有相似之处,但作用和实现的场景有所不同:
- 如果是针对外部系统的远程调用、适配和隔离,应该是防腐层。
- 如果是简化领域内复杂模块之间的调用接口,则更适合用 Facade 层。
实际设计时,可以根据场景决定两者是否合并使用,例如一个面向外部系统的 Facade 层也可以承担防腐层的职责,但需要明确职责分离,确保不对领域模型造成侵蚀。
11. 项目comm jar包内容应该放里面?
一个项目的 common
包(或称为 comm 包、common 模块、基础工具包)是整个系统的 基础能力集合,用于存放 无业务逻辑、通用、可复用、稳定 的组件或工具,适用于多个业务模块之间共享使用。通常应该放在 common
包的内容分类如下:
11.1. ✅ 通常应该放在 common
包的内容分类如下:
11.1.1. 1️⃣ 工具类(Utils)
工具类名称 | 功能说明 |
| 日期时间格式化、时间差计算等 |
| 封装 Jackson / Fastjson 操作 |
| 字符串处理封装(如果不依赖 Apache Commons) |
| Bean 拷贝(如使用 Spring BeanUtils MapStruct) |
| 封装雪花算法、UUID、IDWorker 等唯一 ID 生成器 |
| 文件读写、上传封装 |
11.1.2. 2️⃣ 通用常量(Constants)
类名 | 内容 |
| 项目级别的通用常量,如系统编码、token 前缀等 |
| Redis key 统一前缀 |
| HTTP 状态码枚举封装 |
| 通用错误码定义 |
11.1.3. 3️⃣ 自定义注解 + AOP(横切逻辑)
类型 | 用途 |
| 分布式锁注解 |
| 统一日志记录 |
| 数据权限注解 |
| 对应的切面逻辑(如日志切面、分布式锁切面) |
11.1.4. 4️⃣ 通用异常体系
内容 | 描述 |
| 业务基础异常类,继承 |
| 带错误码/错误消息的业务异常 |
| 统一错误枚举 |
| 全局异常处理器(用于Spring |
11.1.5. 5️⃣ 通用响应封装类(统一返回结构)
类名 | 功能 |
| 通用响应结构:封装 |
| 带分页的返回结构 |
| 标准响应码枚举(如 SUCCESS、FAIL、ILLEGAL_PARAM 等) |
11.1.6. 6️⃣ 枚举(Enums)
- 性别、状态、布尔值(YES/NO)、平台类型、支付渠道等通用枚举
- 可以统一实现
BaseEnum
接口,便于映射 code → enum
11.1.7. 7️⃣ 基础接口或抽象类
类型 | 功能 |
| 公共实体字段(如 |
| 公共 controller 操作(如获取当前用户) |
| 通用错误码接口 |
| 通用 MyBatis Mapper 接口(可选) |
11.1.8. 8️⃣ 通用配置类(Config)
配置 | 功能 |
| 自定义序列化规则 |
| 跨域配置 |
| Feign 全局设置 |
| 自定义线程池配置 |
11.1.9. 9️⃣ 网络 & 中间件封装
类型 | 说明 |
Redis 封装 |
|
MQ 工具类 | Kafka / RocketMQ 封装工具 |
Web 工具 | IP 工具、请求参数解析等 |
11.2. ✅ 推荐包结构示例:
common
├── annotation // 注解
├── aspect // AOP 切面
├── config // 通用配置类
├── constant // 常量类
├── enums // 枚举类
├── exception // 异常处理
├── model // 通用实体,如 Result、PageResult
├── util // 工具类
├── lock // 分布式锁(注解 + 实现)
└── ...
11.3. ✅ 最佳实践建议
- 不放任何业务逻辑(如订单、用户、积分等业务域代码)
- 尽量通用稳定,一旦发布不要轻易变动
- 建议设置成单独Maven模块,独立打包
common.jar
供其他模块依赖
12. RPC接口应该放在哪一层?
例如放在common-utils
、common-base
中,这是错误的,因为这些模块主要是放工具类、基础类(如枚举、通用响应体等),而 RPC 接口具有明确业务依赖,不应该混进工具模块中。
12.1. 建议:在Service层单独建立client
或remote
包
📦 项目结构示例:
order-service/└── src/main/java/com/xxx/order/service/├── client/RemoteUserClient.java (仅订单服务内部使用)└── OrderService.java
12.2. 优势:
- 结构清晰、简单。
- 避免过度模块化,减少项目依赖。