一个既可拆分为 微服务(Nacos + Feign)、也可聚合为 单体(单进程)的 Spring Cloud 框架。两种模式共用同一套业务代码,切换只靠一个配置:app.mode。
- 技术栈:Spring Boot 2.7 + JDK8 + Spring Cloud Alibaba 2021.0.5.0 + Nacos 2.x + OpenFeign
- 示例模块:
user(用户)、order(订单),order 通过 Feign 调用 user
root-pom (enty-framework)
├── common # 公共能力:统一返回体、全局异常、双模式开关
├── user # 用户功能模块(聚合父)
│ ├── user-api # 契约层:Feign 接口 + DTO
│ └── user-service # 实现层:微服务(端口 8081)
├── order # 订单功能模块(聚合父)
│ ├── order-api # 契约层:Feign 接口 + DTO
│ └── order-service # 实现层:微服务(端口 8082),Feign 调用 user
└── app-monolith # 单体聚合模块(端口 8090)
| 模块 | 作用 | 说明 |
|---|---|---|
common |
通用公共层 | 统一返回体 R、统一返回码 ResultCode、业务异常 BusinessException、全局异常处理 GlobalExceptionHandler(@RestControllerAdvice)、双模式开关 AppModeEnvironmentPostProcessor。被所有模块依赖 |
xxx-api(契约层) |
跨模块接口契约 | Feign 接口 + DTO,只被「调用方」和「实现方」依赖,不承载业务实现 |
xxx-service(实现层) |
业务实现 | 业务逻辑 + REST 表现层 + 独立启动入口。微服务模式下可独立运行并注册 Nacos;单体模式下被 app-monolith 作为库聚合 |
app-monolith |
单体聚合模块 | 依赖所有 -service 模块,单进程运行。app.mode: mono 时自动关闭 Feign / Nacos discovery,跨模块调用变为本地方法调用 |
com.enty.user
├── UserApplication # 微服务入口(模块根包)
├── controller/
│ └── UserController # REST 表现层,路径与契约一致
├── service/
│ └── UserClientImpl # 业务实现 @Service
└── config/ # 模式相关配置(仅需要时)
com.enty.user.api # user-api 模块
├── client/
│ └── UserClient # Feign 契约接口
└── dto/
└── UserDTO # 数据传输对象
| 值 | 场景 | 效果 |
|---|---|---|
micro |
微服务 | 注册 Nacos、Feign 远程调用 |
mono |
单体 | 关闭 Feign 与 Nacos discovery,跨模块变本地调用 |
一个属性自动推导两件事:
- Feign:
order/config/OrderFeignConfig上@ConditionalOnProperty(name = "app.mode", havingValue = "micro", matchIfMissing = true),mono 时不创建 Feign 代理。 - Nacos discovery:
common的AppModeEnvironmentPostProcessor(Spring Boot 扩展点,经META-INF/spring.factories注册)读到app.mode=mono时自动注入spring.cloud.nacos.discovery.enabled=false与spring.cloud.discovery.enabled=false,无需手写。
若
app.mode未配置,默认按micro处理(matchIfMissing = true)。单体入口务必显式写mono。
以新增 payment(支付)为例,复制 user 的结构:
payment/
├── payment-api # 契约层
└── payment-service # 实现层
- 包:
com.enty.payment.api.client/com.enty.payment.api.dto - 定义 DTO 与 Feign 契约接口:
// com.enty.payment.api.client.PaymentClient
@FeignClient(name = "payment-service")
public interface PaymentClient {
@PostMapping("/payment")
PaymentDTO create(@RequestBody PaymentDTO payment);
@GetMapping("/payment/{id}")
PaymentDTO getById(@PathVariable("id") Long id);
}
@FeignClient(name)必须等于提供方spring.application.name(这里是payment-service)。
- 入口:
com.enty.payment.PaymentApplication
@SpringBootApplication(scanBasePackages = "com.enty") // 扫描 common 通用组件
public class PaymentApplication {
public static void main(String[] args) { SpringApplication.run(PaymentApplication.class, args); }
}- REST 表现层:
com.enty.payment.controller.PaymentController(@RestController,路径与PaymentClient契约一致,委托给本地实现) - 业务实现:
com.enty.payment.service.PaymentClientImpl(@Service implements PaymentClient)
// controller/PaymentController.java
@RestController
public class PaymentController {
@Autowired
private PaymentClient paymentClient;
@GetMapping("/payment/{id}")
public PaymentDTO getById(@PathVariable("id") Long id) {
return paymentClient.getById(id);
}
}// service/PaymentClientImpl.java
@Service
public class PaymentClientImpl implements PaymentClient {
@Override
public PaymentDTO getById(Long id) { /* 业务逻辑 */ }
}application.yml(微服务模式):
server:
port: 8083
app:
mode: micro
spring:
application:
name: payment-service
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848- 根
pom.xml:<modules>加<module>payment</module>,<dependencyManagement>加payment-api/payment-service payment/pom.xml:父 POM 引用根 POM,<modules>列payment-api、payment-servicepayment-service/pom.xml:<parent>指向payment,依赖common+payment-api+ web/openfeign/nacos/loadbalancerapp-monolith/pom.xml:加依赖<artifactId>payment-service</artifactId>(想让它进单体时)
在 payment-service 加一个条件 Feign 配置(模仿 OrderFeignConfig):
package com.enty.payment.config;
@Configuration
@EnableFeignClients(clients = UserClient.class) // 只注册要调用的远程接口
@ConditionalOnProperty(name = "app.mode", havingValue = "micro", matchIfMissing = true)
public class PaymentFeignConfig {
}想让新模块也在单体模式下被聚合,只需在 app-monolith/pom.xml 加一行依赖:
<dependency>
<groupId>com.enty</groupId>
<artifactId>payment-service</artifactId>
</dependency>除此之外什么都不用改,因为单体装配是自动的:
| 单体装配环节 | 为什么自动生效 |
|---|---|
| 组件扫描 | MonolithApplication 已 scanBasePackages = "com.enty",com.enty.payment.* 天然被扫到 |
| 入口类排除 | 排除规则 .*\.Application 已把 PaymentApplication 挡在扫描外,避免入口副作用 |
| Feign 关闭 | PaymentFeignConfig 依赖 app.mode=micro,单体下 mono 自动不启用 |
| Nacos 关闭 | AppModeEnvironmentPostProcessor 按 mono 自动关闭 discovery |
前提:新模块包必须在 com.enty 之下(com.enty.payment.*),否则扫不到。
配置:单体运行时只认 app-monolith/application.yml,所以支付模块需要的业务配置(数据源、自定义属性等)要写进这里,不能写在各 -service 自己的 yml。
构建与运行:
mvn clean install # 或 mvn clean package
java -jar app-monolith/target/app-monolith-1.0.0.jar运行后支付模块的 REST 端点(如 /payment/{id})和 user/order 一样暴露在单体应用上。
注意(bean 名冲突):Spring 默认 bean 名 = 类名首字母小写,单体下所有模块的类在同一个容器里,不同模块不要用相同的类名(比如两个模块都叫 OrderMapper),否则 bean 名会冲突。
| 类型 | 位置 | 说明 |
|---|---|---|
| DTO(传输对象) | com.enty.xxx.api.dto |
跨模块传输的数据模型,放在契约层(api),调用方与实现方共用 |
| VO(视图对象) | com.enty.xxx.controller.vo(约定) |
对外接口的请求/响应视图对象。当前模板未内置 VO,如需对外层与内部模型解耦,建议放 controller 子包 vo,由 Controller 负责 DTO/VO 互转 |
约定:跨模块传参一律走 api 层 DTO,不要用各模块内部实体(Entity/DO)直接对外。
- 引入被调用方的 api 依赖:在
pom.xml加<artifactId>xxx-api</artifactId>。 @Autowired注入契约接口,直接调方法:
@Service
public class OrderClientImpl implements OrderClient {
@Autowired
private UserClient userClient; // 跨模块调用点
@Override
public OrderDTO getOrderWithBuyer(Long id) {
OrderDTO order = getById(id);
UserDTO buyer = userClient.getById(order.getUserId()); // 远程调用 user-service
order.setBuyerName(buyer.getName());
return order;
}
}- 微服务模式:
userClient是 Feign 代理,经 Nacos 负载均衡走 HTTP 调http://user-service/...。 - 单体模式:
userClient是本进程内的UserClientImpl,直接本地方法调用。
同一个 @Autowired UserClient,两种模式下行为自动切换,调用方代码零改动。
提供方必须把对应接口暴露为 REST:
UserController的路径要与UserClient的@GetMapping路径一致(微服务模式下 Feign 请求的就是这些端点)。
# 需先启动 Nacos(本项目示例用 Docker:nacos/nacos-server:v2.2.3,standalone 模式,暴露 8848/9848/9849)
java -jar user/user-service/target/user-service-1.0.0.jar # 8081
java -jar order/order-service/target/order-service-1.0.0.jar # 8082每个
-service模块的 jar 即 fat jar(可直接java -jar)。若服务模块恢复<classifier>exec</classifier>配置,则运行xxx-service-1.0.0-exec.jar。
| 配置 | 值 | 说明 |
|---|---|---|
app.mode |
micro |
微服务模式(默认值,可不写) |
spring.application.name |
服务名 | 必须等于他人 @FeignClient(name) 的值 |
spring.cloud.nacos.discovery.server-addr |
Nacos 地址 | 如 127.0.0.1:8848 |
server.port |
各服务独立端口 | 8081 / 8082 … |
Nacos 地址可按环境覆盖:java -jar xxx.jar --spring.cloud.nacos.discovery.server-addr=10.0.0.5:8848。
框架当前未内置 ORM / 数据源依赖,以下为接入约定。每个微服务各自连接自己的库,配置在各自服务的
application.yml:
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/order_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: xxxx每个 -service 一个数据源、一个库,天然隔离,无需多数据源路由。
- 库名:
<模块名>_db,如order_db、user_db、payment_db - 表名:
<模块前缀>_<业务名>,前缀与库同名,如order_t_order、order_t_order_item、user_t_user - 主键统一
id BIGINT,公共字段建议create_time/update_time/deleted(逻辑删除)
前缀规则是「单体合并库」的前提,务必从第一天起遵守(见 8.3)。
java -jar app-monolith/target/app-monolith-1.0.0.jar # 8090,不需要 Nacosapp-monolith/src/main/resources/application.yml:
server:
port: 8090
app:
mode: mono # 关键:单体模式,自动关 Feign + Nacos discovery
spring:
application:
name: monolith-app只有这一份配置文件生效。各 -service 自己的 application.yml 在单体运行时不会被采用(它们被包进嵌套 jar,Spring Boot 不读取)。因此单体模式下新增的公共配置(如数据库)要写在这里。
单体模式约定单库:所有模块共用一个数据库(合并库),配置集中在
app-monolith的application.yml:
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/mono_all_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: xxxx单进程单数据源,不需要 @DS 多数据源,跨模块调用天然是本地事务。
- 库名:合并库,如
mono_all_db - 表名:必须保留模块前缀,如
order_t_order、user_t_user——这是合并后不撞名的关键 - 「微服务多库 → 单体单库」的数据准备:把各
-service库dump出来,按前缀规则导入合并库即可
| 微服务 | 单体 | |
|---|---|---|
| 库 | 每服务一个库(order_db…) |
一个合并库(mono_all_db) |
| 数据源配置位置 | 各 -service 的 application.yml |
app-monolith 的 application.yml |
| 表前缀 | order_t_xxx |
同一套前缀(合并不撞名) |
| 跨模块事务 | 分布式(需 Seata/补偿) | 本地事务 |
- 构建/安装:内部模块只在 reactor 中存在,单独构建
app-monolith(或 IDEA 解析单模块)需先执行mvn install装进本地仓库。日常从根目录mvn clean install或mvn clean package。 - 异常处理:全局异常集中在
common的GlobalExceptionHandler,不要在某个-service里另写@RestControllerAdvice,否则单体模式下多个 advice 会竞争。 - 依赖去重:Maven 对同版本依赖只保留一份,
common等公共依赖不会因多个模块引用而重复。 - 配置生效范围:单体运行时只认
app-monolith/application.yml;微服务运行时各服务认各自的。 app.mode未配置时默认微服务,单体入口必须显式mono。