这几天正是端午假期,属实舒服了几天,cloud方面零零散散过了一些,大致是:从公共模块的抽取,理解服务之间如何通过 Nacos 互相发现,再到使用 RestTemplate 完成远程调用 。上一篇我们已经把服务注册到了 Nacos,这一篇就让服务之间真正"通"起来。
1. 回顾:上篇我们走到了哪里
上一篇完成了三件事:
-
理解了单体、集群、分布式、微服务的区别
-
搭建了父工程 + 服务聚合模块 + 两个微服务(
service-order、service-product) -
把两个服务注册到了 Nacos
但是服务虽然注册上去了,它们之间还"不认识"。订单服务不知道去哪里找商品服务,也不知道怎么调用它。
这一篇要解决的就是这两个问题:
服务怎么找到对方?找到之后怎么调用?
2. 为什么要抽一个 Model 模块
在远程调用之前,先要解决一个前置问题:两个服务之间传递什么数据?
比如订单服务调用商品服务查询商品信息,商品服务返回的数据是一个 Product 对象。这个 Product 类放在哪里?
如果放在 service-product 模块里,那 service-order 就引用不到它。
如果两个服务各写一份 Product 类,那字段一改就要改两处,迟早出问题。
所以正确做法是:把公共的实体类抽到一个独立的 model 模块里,谁用谁引。
2.1 Model 模块的结构
java
model/
└── src/main/java/com/liu/
├── product/bean/Product.java ← 商品实体
└── order/bean/Order.java ← 订单实体
Product.java:
java
@Data
public class Product {
private Long id;
private BigDecimal price;
private String productName;
private int num;
}
Order.java:
java
@Data
public class Order {
private Long id;
private BigDecimal totalAmount;
private Long userId;
private String nickname;
private String address;
private List<Product> productList; // 一个订单包含多个商品
}
注意 Order 里面引用了 Product。这说明 model 模块内部的类之间也可以互相引用------它们本来就是同一层的东西。
2.2 Model 模块的 POM
XML
<parent>
<groupId>com.liu</groupId>
<artifactId>cloud-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>model</artifactId>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>annotationProcessor</scope>
</dependency>
</dependencies>
model 模块非常轻量:只引入了 Lombok,用于自动生成 getter/setter。它不需要 Spring Boot,因为它只放 POJO,不放业务逻辑。
2.3 服务怎么引用 Model
在 services/pom.xml 中:
XML
<dependency>
<groupId>com.liu</groupId>
<artifactId>model</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
这一行写在 services 聚合层,所以下面的三个子服务(order、product、user)全部自动继承,不用每个服务各写一遍。
一句话理解 Model 模块的意义:把"大家都要用的数据模型"放在一个公共地方,各服务只引不写,保证数据定义的一致性。
3. 远程调用:从写死地址到动态发现
有了公共实体之后,回到核心问题:订单服务怎么调用商品服务?
3.1 最原始的想法:写死地址
java
// 最原始的方式 ------ 把地址写死在代码里
String url = "http://127.0.0.1:9001/product/" + productId;
Product product = restTemplate.getForObject(url, Product.class);
问题很明显:
-
商品服务换端口了,代码要改
-
商品服务部署到另一台机器了,代码要改
-
商品服务部署了多份,写死地址只能调一台
这就是上一篇说的:注册中心要解决的问题。
3.2 进阶:从注册中心动态拿地址
Nacos 提供了 DiscoveryClient,让我们可以从注册中心查询服务信息:
java
@Autowired
DiscoveryClient discoveryClient;
private Product getProductFromRemote(Long productId) {
// 1. 从 Nacos 查出 service-product 的所有实例
List<ServiceInstance> instances = discoveryClient.getInstances("service-product");
// 2. 拿到第一个实例的 IP 和端口
String url = "http://" + instances.get(0).getHost()
+ ":" + instances.get(0).getPort()
+ "/product/" + productId;
log.info("远程请求: {}", url);
// 3. 发送 HTTP 请求
Product product = restTemplate.getForObject(url, Product.class);
log.info("product: {}", product);
return product;
}
关键变化 :地址不再写死,而是通过 discoveryClient.getInstances("service-product") 动态获取。
这个过程可以拆成三步:
-
服务发现 :查 Nacos,
service-product在哪台机器、哪个端口 -
拼接 URL:根据查到的信息拼出完整的请求地址
-
发送请求 :用
RestTemplate发出 HTTP 调用
3.3 RestTemplate 是什么
RestTemplate 是 Spring 提供的一个 HTTP 客户端工具,用来发送 REST 请求并接收响应。
使用之前需要先注册成 Bean:
java
@Configuration
public class OrderServiceConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
它的核心方法是 getForObject(url, 类型.class)------发送 GET 请求,并把返回的 JSON 自动转成 Java 对象。
4. 完整调用链路走一遍
现在把整个流程串起来。
4.1 商品服务端:提供数据
ProductController.java:
java
@RestController
public class ProductController {
@Autowired
ProductService productService;
// 查询商品
@GetMapping("/product/{id}")
public Product getProduct(@PathVariable("id") Long productId) {
Product product = productService.getProductById(productId);
return product;
}
}
ProductServiceImpl.java:
java
@Service
public class ProductServiceImpl implements ProductService {
@Override
public Product getProductById(Long productId) {
Product product = new Product();
product.setId(productId);
product.setProductName("IQOO-" + productId);
product.setNum(2);
product.setPrice(new BigDecimal("99"));
return product;
}
}
目前只是模拟数据,没有连数据库。重点是先把调用链路跑通。
4.2 订单服务端:远程调用商品服务
OrderController.java:
java
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping("/create")
public Order createOrder(@RequestParam("productId") Long productId,
@RequestParam("userId") Long userId) {
return orderService.createOrder(productId, userId);
}
}
OrderServiceImpl.java(核心):
java
@Slf4j
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
DiscoveryClient discoveryClient;
@Autowired
private RestTemplate restTemplate;
@Override
public Order createOrder(Long productId, Long userId) {
// 1. 远程调用商品服务,获取商品信息
Product product = getProductFromRemote(productId);
// 2. 组装订单数据
Order order = new Order();
order.setId(1L);
order.setTotalAmount(product.getPrice().multiply(new BigDecimal(product.getNum())));
order.setUserId(userId);
order.setNickname("张三");
order.setAddress("上海");
order.setProductList(Arrays.asList(product));
return order;
}
private Product getProductFromRemote(Long productId) {
// 从 Nacos 发现服务地址
List<ServiceInstance> instances = discoveryClient.getInstances("service-product");
// 拼接请求 URL
String url = "http://" + instances.get(0).getHost()
+ ":" + instances.get(0).getPort()
+ "/product/" + productId;
log.info("远程请求: {}", url);
// 发送 HTTP 请求
Product product = restTemplate.getForObject(url, Product.class);
log.info("product: {}", product);
return product;
}
}
4.3 调用流程图
java
用户浏览器
│ GET /create?productId=1&userId=100
▼
service-order (端口 8000)
│ OrderController.createOrder()
│ → OrderServiceImpl.createOrder()
│
│ ① discoveryClient.getInstances("service-product")
│ → 去 Nacos 查"service-product 在哪?"
│ → Nacos 返回: IP = 127.0.0.1, Port = 9001
│
│ ② restTemplate.getForObject("http://127.0.0.1:9001/product/1", Product.class)
│ → 向商品服务发 HTTP 请求
│
▼
service-product (端口 9001)
│ ProductController.getProduct(1)
│ → ProductServiceImpl.getProductById(1)
│ → 返回 Product{id=1, name="IQOO-1", price=99, num=2}
│
▼ 返回 JSON
service-order
│ 收到 Product 数据
│ 组装 Order 对象
│ 返回给用户
▼
用户收到订单 JSON
这就是一次完整的跨服务远程调用 。核心就一句话:订单服务通过 Nacos 发现商品服务的地址,再发 HTTP 请求获取数据。
5. DiscoveryClient 的两个用法
我在**DiscoveryTest**测试类中,展示了两种获取服务信息的方式:
5.1 方式一:Spring Cloud 通用的 DiscoveryClient
java
@Autowired
DiscoveryClient discoveryClient;
@Test
void discoveryTest() {
for (String service : discoveryClient.getServices()) {
System.out.println(service);
List<ServiceInstance> instances = discoveryClient.getInstances(service);
for (ServiceInstance instance : instances) {
System.out.println("ip:" + instance.getHost() + " port:" + instance.getPort());
}
}
}
DiscoveryClient 是 Spring Cloud 定义的标准接口,不绑定任何具体的注册中心。以后如果从 Nacos 换成 Eureka 或 Consul,这行代码不用改。
5.2 方式二:Nacos 专属的 NacosServiceDiscovery
java
@Autowired
NacosServiceDiscovery nacosServiceDiscovery;
@Test
void nacosDiscoveryTest() throws Exception {
for (String service : nacosServiceDiscovery.getServices()) {
System.out.println(service);
List<ServiceInstance> instances = nacosServiceDiscovery.getInstances(service);
for (ServiceInstance instance : instances) {
System.out.println("ip:" + instance.getHost() + " port:" + instance.getPort());
}
}
}
NacosServiceDiscovery 是 Nacos 专属的 API,功能更丰富(比如可以获取服务的元数据等)。但在日常开发中,推荐优先使用通用的 DiscoveryClient,保持代码与注册中心解耦。
面试点 :DiscoveryClient 是 Spring Cloud 的抽象,**NacosServiceDiscovery**是 Nacos 的实现------这就是面向接口编程的思想。
6. 新增第三个服务:service-user
在学习过程中,敲了一个 service-user 模块,用来表示用户服务。
java
@EnableDiscoveryClient
@SpringBootApplication
public class ServiceUserApplication {
public static void main(String[] args) {
SpringApplication.run(ServiceUserApplication.class, args);
}
}
spring:
application:
name: service-user
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
server:
port: 9090
加上之前的 order(8000)和 product(9001),现在 Nacos 上会有三个服务同时在线。
这也验证了微服务架构的一个重要特性:添加新服务不影响已有服务。每加一个新模块,只需要保证服务名和端口不冲突,然后启动即可。
7. 值得揣摩的几个点
7.1 Model 模块是"数据契约"
Product 和 Order 类放在 model 模块里,不只是一个代码复用的技巧。从架构角度看,它们承担的是服务之间的数据契约------双方约定好了数据的字段、类型、结构,远程调用才有意义。
7.2 DiscoveryClient 帮你解耦了"找地址"
思考一下 **discoveryClient.getInstances("service-product")**这行代码的含金量:
-
它不需要知道商品服务的 IP
-
它不需要知道商品服务的端口
-
它只 需要知道服务名
这就是注册中心的核心价值:用服务名替代 IP + 端口,让服务之间的通信不依赖物理地址。
7.3 RestTemplate 只是过渡方案
当前用 RestTemplate + 手动拼 URL 的方式虽然能跑通,但代码明显有点啰嗦:
-
要手动注入
DiscoveryClient -
要手动拼 URL
-
要手动选实例(
get(0)) -
没有负载均衡
这为以后OpenFeign的引入埋下了伏笔。OpenFeign 的目标就是把这 7~8 行代码缩减成 1 行接口声明。
7.4 当前代码的一个隐患:get(0)
java
List<ServiceInstance> instances = discoveryClient.getInstances("service-product");
String url = "http://" + instances.get(0).getHost() + ":" + instances.get(0).getPort() + ...
get(0) 永远取列表的第一个实例。如果商品服务部署了 3 台,另外两台永远不会被调用到。这就引出了下一个话题:负载均衡。
8. 成果
这几天总体上完成了:
从"两个服务各自独立注册",到"订单服务通过 Nacos 发现商品服务并完成远程调用"的完整链路。
具体来说:
-
✅ 抽取了 model 模块,统一管理公共实体类
-
✅ 掌握了
DiscoveryClient的用法,理解了服务发现的机制 -
✅ 使用
RestTemplate完成了第一次跨服务 HTTP 调用 -
✅ 新增了
service-user模块,项目从 2 个服务扩展到 3 个 -
✅ 通过测试类验证了 Nacos 服务发现功能的正确性
这一步把上一篇文章的"服务注册"和"远程调用"真正串了起来------服务不再是孤立的,而是开始协同工作了。
9. 小结
9.1 Model 模块
-
公共实体类统一放在 model 模块中
-
各服务通过 Maven 依赖引用 model
-
保证数据定义的唯一性,避免多处维护
9.2 远程调用
-
使用
RestTemplate发起 HTTP 请求 -
使用
DiscoveryClient从 Nacos 动态获取服务地址 -
调用链路:查 Nacos → 拼 URL → 发请求 → 收响应
9.3 服务发现
-
DiscoveryClient是 Spring Cloud 的标准接口 -
NacosServiceDiscovery是 Nacos 的专属实现 -
推荐优先使用通用接口,与具体注册中心解耦
9.4 当前阶段的全局视图
java
Nacos 注册中心 (127.0.0.1:8848)
│
├── service-order (8000) ← 订单服务(调用方)
├── service-product (9001) ← 商品服务(被调用方)
└── service-user (9090) ← 用户服务
调用关系:
service-order ──RestTemplate──→ service-product
10. 最后
当前代码虽然能跑通,但有明显的优化方向:
-
OpenFeign :把
RestTemplate+ 手动拼 URL 替换成声明式接口调用,一行代码搞定 -
负载均衡 :解决
get(0)的问题,让请求在多个服务实例之间合理分配
先把下面这条线彻底吃透最重要:
model 公共实体 → Nacos 服务发现 → RestTemplate 远程调用 → 完整链路跑通
只要这条线清楚了,后面的 OpenFeign 和负载均衡就是顺水推舟的事了。