Lecture02-SpringCloud-微服务直连调用(RestTemplate)

文章目录

    [toc]

    单词词性音标含义
    directadj./dəˈrekt/直接的(客户端直接连服务地址,不经过注册中心)
    remoteadj./rɪˈməʊt/远程的(跨进程、跨机器发生的调用)
    endpointn./ˈendpɔɪnt/端点(一个可访问的接口地址,如 /user/1)
    portn./pɔːt/端口(同一台机器上区分不同服务的编号)
    hardcodev./ˌhɑːdˈkəʊd/硬编码(把地址、参数等直接写死在代码里)
    serializev./ˈsɪəriəlaɪz/序列化(对象与网络传输格式之间的相互转换)

    Lecture02-SpringCloud-微服务直连调用(RestTemplate)

    目标:先不接注册中心,用最笨也最直观的方式——IP + 端口 + RestTemplate——让两个 Spring Boot 应用互相调用一次。先亲手摸到"微服务之间的网络调用"是什么感觉,再回头理解它有什么毛病,为下一讲 Nacos 做铺垫。

    1、先分清:本地调用 vs 远程调用

    1、单体里的调用(不出 JVM)

    单体应用里,用户、订单、商品都在同一个进程中,OrderService 想拿用户数据,直接调方法就行:

    // 同一个 JVM 内,方法调用,不走网络
    User user = userService.getUser(1L);
    

    特点:快、简单,但前提是"大家住在一起"。


    2、微服务里的调用(必须走网络)

    微服务拆开之后,user-service 和 order-service 是两个独立进程(可能还在两台机器上),各有各的 JVM、各有各的端口:

             ┌───────────────┐         HTTP          ┌───────────────┐
             │ order-service │  ──────────────────▶  │ user-service  │
             │   (订单,8083) │   http://…:8081/user/1│  (用户,8081)  │
             └───────────────┘                       └───────────────┘
                    两个独立进程(两个 JVM)
    

    order-service 想拿用户数据,只能通过网络发一个 HTTP 请求过去——这就是"分布式调用",也是微服务最基本的动作。

    direct-call

    一句话:跨进程 = 跨网络。微服务的第一课,就是学会"隔着网络调别人的接口"。


    2、准备两个服务

    在上一讲的 cloud-parent 父工程下建两个子模块。

    1、user-service(服务提供者)

    user-service/pom.xml 加 Web 依赖:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    

    user-service/src/main/resources/application.yml:

    server:
      port: 8081
    
    spring:
      application:
        name: user-service
    

    写一个查询用户的接口:

    @RestController
    public class UserController {
    
        @GetMapping("/user/{id}")
        public String getUser(@PathVariable Long id) {
            return "user-" + id + " @ user-service(8081)";
        }
    }
    

    启动后能单独访问:http://localhost:8081/user/1


    2、order-service(服务消费者)

    同样加 spring-boot-starter-web 依赖,配置里注意端口不同:

    server:
      port: 8083
    
    spring:
      application:
        name: order-service
    

    RestTemplate 在 spring-web 里(spring-boot-starter-web 已包含),不需要额外加依赖。先把它声明成一个 Bean:

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.client.RestTemplate;
    
    @Configuration
    public class RestTemplateConfig {
    
        @Bean
        public RestTemplate restTemplate() {
            return new RestTemplate();
        }
    }
    

    3、直连调用:把地址写死在代码里

    order-service 直接按 http://IP:端口 去调 user-service:

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;
    import org.springframework.web.client.RestTemplate;
    
    @RestController
    public class OrderController {
    
        @Autowired
        private RestTemplate restTemplate;
    
        // user-service 的地址被"写死"在这里 —— 这就是最原始的做法
        private static final String USER_SERVICE_URL = "http://localhost:8081";
    
        @GetMapping("/order/call")
        public String callUser(@RequestParam Long id) {
            String url = USER_SERVICE_URL + "/user/" + id;
            // getForObject:发 GET 请求,并把响应体转换成指定类型(这里是 String)
            return restTemplate.getForObject(url, String.class);
        }
    }
    

    跑起来的完整链路

    1. 启动 user-service(8081)和 order-service(8083)。
    2. 浏览器访问:http://localhost:8083/order/call?id=1
    3. 页面返回:user-1 @ user-service(8081)
    浏览器 ──▶ order-service(8083) ──HTTP──▶ user-service(8081)
                      ◀────── "user-1 @ user-service(8081)" ──────
    

    结论:两个服务确实是"隔着网络"在通信——你已经把一个微服务调起来了。

    RestTemplate 常用方法:getForObject(url, 返回类型) 发 GET;postForObject(url, 请求体, 返回类型) 发 POST;exchange(...) 用于需要自定义请求头/方法的场景。


    4、这种写法的痛点

    跑通只是开始,把上面的代码拿去做真实项目,会立刻撞上四个问题:

    问题具体表现后果
    ① 地址写死USER_SERVICE_URL 写死在代码里换个 IP、改个端口就要改代码重新打包
    ② 无法负载均衡只能指向一个实例3 个 user-service 也只会打到这一个,白扩
    ③ 实例挂掉无感知目标服务宕机,调用直接失败没有自动切换、没有兜底
    ④ 多环境难维护开发/测试/生产地址各不相同到处改配置,容易漏、容易错

    思考一下:既然 spring.application.name 已经给服务起好名字了,为什么调用时还是得写 IP?

    因为"服务名"目前只是个名字,没有任何地方记录"这个名字对应哪些 IP:端口"。

    谁来记这个"通讯录"?——注册中心。第 03 讲的 Nacos 就是来收编这些问题的。


    5、课堂练习

    1. 在 cloud-parent 下建好 user-service、order-service,把上面这次直连调用跑通。
    2. 把 user-service 的端口从 8081 改成 8091,不改 order-service 的代码,再访问一次——观察报错,体会"地址写死"的代价。
    3. (思考)如果有 3 个 user-service 实例同时运行,现在的写法能不能把请求分到 3 个实例上?为什么?

    6、本讲小结

    • 本地调用在同一 JVM 内走方法栈;远程调用必须跨网络,这是微服务的常态。
    • RestTemplate 是 Spring 提供的 HTTP 客户端封装,getForObject(url, 返回类型) 一行就能完成调用。
    • 直连调用虽然能跑通,但暴露了 地址写死、无法负载均衡、无感知宕机、多环境难维护 四个痛点。
    • 解决思路:让服务启动时把自己的地址登记到"通讯录",调用方按服务名去查——这就是服务注册与发现。

    7、本节验证

    • 能说清"单体内部方法调用"与"微服务远程调用"的本质区别 ✅
    • 会用 RestTemplate 完成一次跨服务的 HTTP 调用(getForObject)✅
    • 能说出 IP 直连写法的至少三条痛点 ✅
    • 能解释"为什么已经有服务名了,调用还是得写 IP" ✅
    end