深入Nacos:服务发现与远程调用

这几天正是端午假期,属实舒服了几天,cloud方面零零散散过了一些,大致是:从公共模块的抽取,理解服务之间如何通过 Nacos 互相发现,再到使用 RestTemplate 完成远程调用上一篇我们已经把服务注册到了 Nacos,这一篇就让服务之间真正"通"起来。


1. 回顾:上篇我们走到了哪里

上一篇完成了三件事:

  • 理解了单体、集群、分布式、微服务的区别

  • 搭建了父工程 + 服务聚合模块 + 两个微服务(service-orderservice-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") 动态获取。

这个过程可以拆成三步:

  1. 服务发现 :查 Nacos,service-product 在哪台机器、哪个端口

  2. 拼接 URL:根据查到的信息拼出完整的请求地址

  3. 发送请求 :用 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 模块是"数据契约"

ProductOrder 类放在 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. 最后

当前代码虽然能跑通,但有明显的优化方向:

  1. OpenFeign :把 RestTemplate + 手动拼 URL 替换成声明式接口调用,一行代码搞定

  2. 负载均衡 :解决 get(0) 的问题,让请求在多个服务实例之间合理分配

先把下面这条线彻底吃透最重要:

model 公共实体 → Nacos 服务发现 → RestTemplate 远程调用 → 完整链路跑通

只要这条线清楚了,后面的 OpenFeign 和负载均衡就是顺水推舟的事了。