前端视角 Java Web 入门手册 5.4:真实世界 Web 开发——Java Web 代码组织与分层

当在 IDE 中打开一个陌生项目时,第一件事应该通过观察包命名规范、分层策略和模块划分,这样可以快速了解项目设计理念,找到自己需要变更的代码文件,如同建筑师看懂蓝图的能力,这是每个开发者必备的素养

MVC

相信前端同学肯定已经很了解 MVC 模式,简单 Java Web 项目同样可以使用 MVC 模式

plain 复制代码
src/
├── controller/   # 处理HTTP请求(如 UserController)
├── service/       # 业务逻辑层(如 UserService)
├── dao/           # 数据访问接口(如 UserDao)
└── model/         # 数据实体(贫血模型,仅有属性)
└── templates/    # 视图模板文件夹,存放如 HTML、JSP 等模板文件

MVC 模式由于其简单性和清晰的职责分离,适用于小型或中等复杂度的应用。然而当面对复杂的业务场景时,MVC 模式会有几个问题

  • 贫血模式:业务逻辑分散在 Controller 和 Service 中,模型仅用于数据传输,缺乏业务规则封装,导致 Service 层混杂业务逻辑与流程编排过于庞大,形成"上帝类"。
  • 按技术分层:代码按照技术层次组织,而非基于业务领域,导致业务逻辑与技术实现混杂,不利于业务需求的变化和扩展。
  • 层间耦合度高:各层直接依赖具体实现,增加了模块间的耦合,降低了系统的灵活性和可维护性。

DDD

领域驱动设计(Domain-Driven Design)是以业务领域为核心构建软件的方法论,通过充血模型将业务规则内聚到领域对象,以领域而非技术分层组织代码,采用四层架构实现业务与技术解耦,从而解决传统 MVC 贫血模型、代码组织混乱、层间耦合等问题,提升复杂系统的可维护性和业务响应能力。

以下是一个符合 DDD 规范的示例项目目录

plain 复制代码
src/
├── interface/         # 接口层(API/RPC/事件监听)
├── application/       # 应用服务层(用例编排)
├── domain/            # 领域层(核心业务逻辑)
│   ├── model/         # 聚合根/实体/值对象
│   ├── repository/    # 仓储接口
│   └── service/       # 领域服务
└── infrastructure/    # 基础设施层(DB/缓存实现)

DDD 一般采用四层架构

  • 用户接口层(Interface) :负责处理用户交互,接收用户输入并展示处理结果。与用户直接交互,不包含业务逻辑,只调用应用层的服务来执行业务操作。
  • 应用层(Application) :协调领域对象完成业务流程,调用领域层的组件来执行业务逻辑,同时管理流程和权限,不直接包含业务规则。
  • 领域层(Domain) :核心层,包含业务领域的模型和规则。定义了实体、值对象、聚合根以及领域服务,负责封装业务逻辑和确保业务规则的一致性。
  • 基础设施层(Infrastructure) :提供技术支持和实现细节,如数据库访问、消息队列、外部服务集成等。实现领域层和应用层定义的接口,处理具体的技术操作,保持与业务逻辑的解耦。

通过这四层的分工,DDD 架构能够有效应对复杂业务场景,提升系统的灵活性和可扩展性,同时解决传统 MVC 模式下的业务逻辑分散和高耦合问题。

Interface

接口层(Interfaces Layer)位于 DDD 四层架构的最外层,主要负责与外部系统或用户进行交互。它的核心职责是接收输入请求、处理输出响应,以及将数据在内部模型与外部表示之间进行转换。接口层确保系统的内部实现细节对外部保持透明,同时提供统一的接口以供外部调用。接口层通常包含几个主要目录,每个目录负责不同的交互和适配任务:

  • Controller:处理 HTTP 请求,调用应用层的服务,并返回响应。控制器充当用户接口层与应用层之间的桥梁,负责请求的路由和初步的输入验证。
  • DTO:用于在接口层与其他层之间传递数据。DTO 通常是简单的、无业务逻辑的类,确保数据在不同层之间的传输安全且高效,避免业务对象变成万能大对象。
  • Facade:简化对复杂子系统的访问,提供统一的接口以协调多个应用层服务的调用。Facade 模式有助于减少接口层与应用层之间的直接依赖,提高系统的模块化。
  • Mapper:负责在 DTO 与领域模型或应用层模型之间进行转换。Mapper 确保数据格式的一致性,避免在不同层之间传递不必要的数据。
  • ExceptionHandler:集中处理接口层抛出的异常,转换为统一的错误响应格式。异常处理器提高了系统的健壮性和用户体验。
  • Interceptor:在请求处理前后执行特定的逻辑,如日志记录、权限验证、性能监控等。拦截器帮助在不修改业务代码的情况下添加横切关注点。
  • Filter:在请求到达控制器之前或响应返回给客户端之前对请求和响应进行预处理或后处理,如请求认证、请求体解析、响应压缩等。过滤器常用于处理全局性的请求和响应逻辑。
plain 复制代码
my-java-ddd-web-app/
├── src/
│   ├── main/
│   │   └── java/com/example/app/
│   │       ├── interfaces/         # 接口层根目录
│   │       │   ├── controller/     # 控制器,处理外部请求
│   │       │   ├── dto/            # 数据传输对象,用于接口层与应用层数据传递
│   │       │   ├── facade/         # 外观模式,封装应用层服务调用逻辑
│   │       │   ├── mapper/         # 映射器,负责对象之间的转换
│   │       │   ├── exception/      # 异常处理,包含全局异常处理器和自定义异常
│   │       │   ├── security/       # 安全配置,保障系统安全
│   │       │   ├── config/         # 其它配置,如Web相关配置
│   │       │   ├── interceptor/    # 拦截器,进行请求预处理等操作
│   │       │   └── filter/         # 过滤器,如身份验证过滤器
│   │       ├── application/        # 应用层,协调领域对象完成业务流程
│   │       ├── domain/             # 领域层,封装业务规则与领域模型
│   │       └── infrastructure/     # 基础设施层,提供数据库等技术实现
│   └── test/
│       └── java/...
├── pom.xml
└── README.md

当然也可以做一些分类、聚合

plain 复制代码
interfaces/
├── web/                  # Web接口适配
│   ├── controller/       # REST API入口(如OrderController)
│   ├── dto/              # 数据传输对象(请求/响应体)
│   ├── assembler/        # DTO 与领域对象转换器
│   └── validation/       # 接口参数校验(如@Valid分组规则)
│
├── rpc/                  # 跨服务通信适配
│   ├── client/           # Feign/Dubbo客户端接口(如PaymentClient) 
│   └── dto/              # RPC专用传输对象
│
├── messaging/            # 消息监听与发布
│   ├── consumer/         # 消息消费者(如OrderPaidEventListener)
│   └── producer/         # 消息生产者(如OrderKafkaProducer)
│
├── exception/            # 接口层异常处理
│   ├── handler/          # 全局异常处理器(@ControllerAdvice)
│   └── error/            # 错误码定义(如ErrorCode枚举)
│
└── facade/               # 复杂接口服务聚合

用户接口层(Interface Layer) 有时也被称为 表现层(Presentation Layer) 。这两个术语通常指同一层,主要区别在于侧重点不同:

  • 用户接口层:强调与外部系统(如客户端、第三方服务)的交互,关注请求/响应的适配和技术实现(如 RESTful API、Controller)。
  • 表现层:更侧重于对用户的信息展示和交互逻辑(如前端页面渲染、数据格式化),但在 DDD 中通常不涉及具体 UI 实现,而是泛指接口层的输入输出功能。

Application

应用层在 DDD 四层架构中位于接口层和领域层之间,负责协调业务流程、管理事务以及调用领域层的服务与仓储。它不包含具体的业务逻辑,而是组织和调度领域对象以完成业务用例,确保系统的高效运作和维护性。应用层常见的目录结构有:

  • service:存放应用服务类,通过组合领域对象完成业务流程编排
  • assembler:负责 DTO 与领域对象的双向转换(避免直接暴露领域模型)
  • client:集成其它微服务的远程调用客户端(如 Feign/Dubbo)
  • event:处理领域事件的监听与发布(实现事件驱动架构)
  • config:应用层专用配置(不包含技术实现细节)
plain 复制代码
application/
├── service/              # 应用服务层
│   ├── order/            # 领域相关服务(如OrderApplicationService)
│   ├── payment/          # 跨领域服务协调
│   └── tx/               # 事务管理(如全局事务注解)
│
├── command/              # CQRS 写操作
│   ├── handler/          # 命令处理器(CreateOrderCommandHandler)
│   └── cmd/              # 命令定义(CreateOrderCommand)
│
├── query/                # CQRS 读操作
│   ├── handler/          # 查询处理器(GetOrderListQueryHandler)
│   └── qry/              # 查询定义(OrderListQuery)
│
├── assembler/            # DTO 与领域对象转换器
│   ├── user/             # 用户相关转换逻辑
│   └── order/            # 订单相关转换逻辑
│
├── client/               # 跨微服务调用客户端
│   ├── payment/          # 支付服务Feign客户端
│   └── inventory/        # 库存服务gRPC客户端
│
├── event/                # 领域事件处理
│   ├── listener/         # 事件监听(如OrderCreatedListener)
│   └── publisher/        # 事件发布者(如EventPublisher)
│
└── config/               # 应用层配置
    └── ApplicationConfig.java  # 配置Bean(如线程池、限流策略)

CQRS 即命令查询职责分离(Command Query Responsibility Segregation)模式,它是一种将系统操作划分为命令和查询两部分的架构设计模式

  • 命令(Command) :负责对系统进行数据的创建、更新和删除操作,通常是改变系统状态的操作,强调的是执行动作并产生副作用。例如,在一个电商系统中,创建订单、修改订单状态等操作就属于命令操作。
  • 查询(Query) :负责从系统中获取数据,不改变系统的状态,只用于读取信息。比如查询订单列表、获取订单详情等操作就是查询操作。

CQRS 模式将读写操作分开处理,使用不同的模型和接口。这意味着可以针对读和写的不同需求进行独立优化,比如对读操作可以采用缓存技术提高性能,对写操作可以专注于数据的一致性和事务处理。

Domain

领域层是 DDD 架构的核心部分,负责封装业务规则和领域模型,承载核心业务逻辑。该层独立于其它层,专注于实现业务领域的需求和规则,确保系统的高内聚性和低耦合性。领域层常见的目录结构有:

  • 模型(Model)

    • 实体(Entities):实体是具有唯一标识的对象,其标识在整个生命周期内保持不变,且可以随着时间的推移而发生状态变化。实体通常包含业务逻辑和行为。
    • 值对象(Value Objects):值对象用于描述领域中的某个概念,它没有唯一标识,主要通过属性值来定义。值对象是不可变的,一旦创建,其属性值不能被修改。
    • 聚合(Aggregate):聚合是一组相关的实体和值对象的集合,它有一个根实体(聚合根),外部只能通过聚合根来访问聚合内部的对象,以此保证数据的一致性和业务规则的完整性。
  • 仓储(Repository) :提供领域对象的持久化和检索接口,隐藏具体的数据访问实现细节,确保领域层与基础设施层的解耦。

  • 领域服务(Service) :当某个业务逻辑不属于单个实体或值对象时,就可以将其放在领域服务中。领域服务处理的业务逻辑通常涉及多个领域对象,或者与外部系统交互。

  • 领域事件(Domain Events) :领域事件用于表示领域中发生的重要业务事件,当某个业务操作完成后,可以发布相应的领域事件,其它部分的系统可以监听这些事件并做出相应的处理。

plain 复制代码
├── model/                  # 领域模型核心
│   ├── aggregate/          # 聚合根(OrderAggregate)
│   ├── entity/             # 实体(OrderItem)
│   └── value_object/       # 值对象(ShippingAddress)
│
├── service/                # 领域服务
├── repository/             # 仓储接口
├── event/                  # 领域事件
├── exception/              # 领域专属异常
└── specification/          # 业务规则规格

Infrastructure

基础设施层位于 DDD 四层架构的最底层,主要负责提供技术实现支持,如数据库访问、消息队列、文件存储、第三方服务集成等。该层确保上层(应用层和领域层)的业务逻辑能够高效、稳定地运行,同时保持与技术实现的解耦。基础设置层常见的目录结构有:

  • persistence:负责领域对象的持久化操作,包含仓储接口实现和数据对象与领域对象的转换逻辑。
  • messaging:处理消息队列相关事务,涵盖消息的生产和消费,实现系统间的异步通信。
  • client:作为外部服务的适配器,集成 Feign、Dubbo、gRPC 等客户端,用于调用其它微服务。
  • config:管理系统的技术配置,如缓存、数据源、安全等方面的配置信息。
  • utils:提供通用工具类,包含分布式锁、ID 生成器等常用工具,辅助系统开发。
  • event:处理领域事件,包括基于 Kafka 的事件处理和本地事件总线的实现。
plain 复制代码
infrastructure/
├── persistence/           # 持久化实现
│   ├── repository/        # 仓储接口实现(如JPA/Hibernate)
│   └── mapper/            # 数据对象与领域对象转换(如MyBatis)
│
├── messaging/             # 消息组件
│   ├── producer/          # 消息生产者(如Kafka/RabbitMQ)
│   └── consumer/          # 消息消费者(监听器)
│
├── client/                # 外部服务适配器
│   ├── feign/             # Feign客户端(调用其他微服务)
│   └── rpc/               # Dubbo/gRPC客户端
│
├── config/                # 技术配置
│   ├── cache/             # 缓存配置(Redis连接池)
│   ├── datasource/        # 数据源配置(多数据源管理)
│   └── security/          # 安全基础设施配置,可包括认证、授权等相关配置
│
├── utils/                 # 通用工具
│   ├── lock/              # 分布式锁(Redisson实现)
│   ├── id_generator/      # ID生成器(Snowflake算法)
│   └── common/            # 其他通用工具类,如日期处理、加密算法等
│
└── event/                 # 事件基础设施
    ├── kafka/             # Kafka相关事件处理
    └── local/             # 本地事件总线实现

基础设置层有一个很重要的设计原则:基础设施层依赖领域层的抽象(如仓储接口),而非领域层依赖基础设施,这也就是大家常说的依赖倒置,这样领域层通过接口与基础设施层交互,允许切换技术实现(如从 MySQL 迁移到 MongoDB)

复杂 DDD 目录结构示例

如果看到一个项目是这样的目录结构,应该修改哪里是不是清楚了很多

plain 复制代码
ecommerce-system/                 # 电商系统主模块
├── order-service/                # 订单限界上下文(核心域)
│   ├── src/main/java/
│   │   └── com/ecommerce/order/
│   │       ├── interfaces/               # 接口层:对外暴露
│   │       │   ├── web/                  # REST API(OrderController)
│   │       │   ├── rpc/                  # 跨服务调用(库存服务客户端)
│   │       │   └── messaging/            # 消息发布(Kafka生产者)
│   │       ├── application/              # 应用层:业务流程编排
│   │       │   ├── command/              # CQRS命令(如创建订单)
│   │       │   ├── query/                # 查询服务(订单查询)
│   │       │   └── event_handler/        # 事件处理(支付成功监听)
│   │       ├── domain/                   # 领域层:核心业务逻辑
│   │       │   ├── aggregate/            # 聚合根(OrderAggregate)
│   │       │   ├── entity/               # 实体(OrderItem)
│   │       │   ├── value_object/         # 值对象(OrderStatus)
│   │       │   ├── service/              # 领域服务(订单校验)
│   │       │   ├── event/                # 领域事件(OrderCreatedEvent)
│   │       │   └── repository/           # 仓储接口(OrderRepository)
│   │       └── infrastructure/           # 基础设施层
│   │           ├── persistence/          # 持久化实现(JPA/MyBatis)
│   │           │   ├── entity/           # 数据库实体(OrderJpaEntity)
│   │           │   └── converter/        # 领域模型转换器
│   │           ├── cache/                # 缓存实现(Redis)
│   │           └── config/               # 技术配置(数据库连接)
│   └── pom.xml                         # 模块依赖管理
├── payment-service/                # 支付限界上下文(支撑子域)
│   └── ...                         # 结构同order-service
├── inventory-service/              # 库存限界上下文(支撑子域)
│   └── ...
├── shared/                         # 共享内核模块
│   ├── common/                     # 通用工具
│   │   ├── util/                   # 工具类(日期处理)
│   │   └── exception/              # 全局异常(业务异常基类)
│   ├── auth/                       # 认证授权组件
│   │   ├── domain/                 # 权限模型(角色、权限)
│   │   └── client/                 # 认证客户端(Feign调用)
│   └── messaging/                  # 消息契约共享
│       └── event/                  # 全局事件定义(领域事件接口)
├── gateway/                        # API网关层
│   ├── route/                      # 路由配置(Nginx/Apache)
│   └── filter/                     # 全局过滤器(权限校验)
├── pom.xml                         # 父项目POM(管理各模块依赖)
└── README.md                       # 系统架构说明文档

独立的 pom.xml

在 DDD 项目里,domain、infrastructure 等模块目录下维护独立的pom.xml文件,而非与项目根目录的pom.xml 共享,主要是因为

  • 高内聚低耦合:每个模块都有明确的职责和边界,通过独立的 pom.xml 可以将模块的依赖和构建配置与其它模块隔离开,不同模块间的依赖关系清晰。
  • 独立开发与测试:开发人员能够独立对某个模块进行开发、测试和部署。例如 domain 模块的开发人员可以专注于业务逻辑的实现,而不用担心 infrastructure 模块的依赖和配置对其产生影响,提高开发效率。
  • 并行构建:在大型项目中,独立的 pom.xml 允许各个模块进行并行构建,加快整体构建速度。
  • 独立发布:每个模块可以作为一个独立的组件进行发布和复用,通过独立的 pom.xml 可以方便地将其打包成一个 JAR 包。
相关推荐
王磊鑫1 分钟前
重返JAVA之路——图书管理系统
java·开发语言
听闻风很好吃4 分钟前
Java设计模式之观察者模式:从入门到架构级实践
java·观察者模式·设计模式
艺杯羹7 分钟前
JDBC 初认识、速了解
java·数据库·jdbc
陵易居士7 分钟前
Spring如何解决项目中的循环依赖问题?
java·后端·spring
铁弹神侯15 分钟前
Maven相关名词及相关配置
java·maven
Aska_Lv20 分钟前
RocketMQ---core原理
后端
AronTing25 分钟前
10-Spring Cloud Alibaba 之 Dubbo 深度剖析与实战
后端·面试·架构
会飞的皮卡丘EI28 分钟前
关于Blade框架对数字类型的null值转为-1问题
java·spring boot
没逻辑29 分钟前
⏰ Redis 在支付系统中作为延迟任务队列的实践
redis·后端
雷渊31 分钟前
如何保证数据库和Es的数据一致性?
java·后端·面试