尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Java框架 SpringCloud 快速入门: 服务拆分之服务远程调用

Java框架 SpringCloud 快速入门: 服务拆分之服务远程调用 概述上一篇把cloud-demo拆成了order-service和user-service两个独立服务遗留了一个尾巴查询订单接口里的user字段永远是 null。这篇就把它填上——用最朴素的RestTemplate讲清楚微服务远程调用的本质发一次 HTTP 请求。纲要问题回顾user字段为什么是 null为什么不能直接查库解决远程调用技术选型RPCDubbo / gRPC与 HTTPRestTemplate / Feign对比远程调用的本质像浏览器一样发 HTTP 请求动手实现三步在OrderApplication启动类注册RestTemplateBean在OrderService中用getForObject发起 GET 请求并反序列化OrderController返回带用户信息的完整Order完整调用链时序图浏览器 → order-service → user-service → MySQL服务提供者与服务消费者角色是相对的硬编码localhost:8081的三个风险引出注册中心实战坑URL 拼接、两份User类字段不一致、超时未配置user 为什么是 null上一篇拆完服务验证结果是这样的curlhttp://localhost:8080/order/101# {id:101,price:699900,name:Apple 苹果 iPhone 12 ,num:1,userId:1,user:null}需求是根据订单 ID 查询订单的同时把订单所属的用户信息一起返回。现在只做到了前半句。有人会想订单服务里直接写一条 SQL 查用户表不就完了不行。上一篇已经定了规矩——数据独立cloud_order库里根本没有tb_user这张表你想查也查不到。而且就算把表复制一份过来用户模块的查询逻辑就被订单模块重复实现了一遍拆分直接白做。数据库这条路走不通只剩一条订单服务向用户服务发起远程调用。用户服务已经对外暴露了 Restful 接口GET /user/{id}浏览器里输入http://localhost:8081/user/1一定能拿到用户数据。既然浏览器能发这个请求订单模块凭什么不能问题就变成了如何在 Java 代码里发 HTTP 请求。远程调用方式怎么选Java 里做服务间调用的技术大体分两类类别代表技术通信方式特点适用场景RPCDubbo、gRPC自定义二进制协议性能高、传输体积小但要求双方用同一套框架和序列化公司内部、技术栈统一的系统HTTPRestTemplate、FeignOpenFeign标准 HTTP JSON语言无关、门槛低、调试方便性能略逊对外开放接口、异构系统、微服务入门RPC 性能好但绑定性强HTTP 胜在简单通用——只要对方暴露了 HTTP 接口不管它是 Java 写的还是 Python 写的都能调。这篇先用RestTemplate把远程调用的本质讲透Feign是后面篇章的升级方案。动手三步完成远程调用改动只发生在order-serviceuser-service一行代码都不用动——它已经把接口提供好了。order-service/src/main/java/cn/itcast/order/ ├── OrderApplication.java # 改动一注册 RestTemplate Bean ├── web/OrderController.java # 不变返回 Orderuser 字段将不再是 null ├── service/OrderService.java # 改动二发起远程调用填充 user ├── mapper/OrderMapper.java # 不变 └── pojo/ ├── Order.java # 不变user 字段派上用场 └── User.java # 不变用于接收远程调用的返回值注册 RestTemplateRestTemplate是 Spring 提供的 HTTP 客户端工具各种 HTTP 请求都能发。它是个普通类要交给 Spring 容器管理才能在业务代码里注入使用。Bean只能写在配置类里而启动类本身就带SpringBootApplication内含Configuration所以直接写在启动类里最省事packagecn.itcast.order;importorg.mybatis.spring.annotation.MapperScan;importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;importorg.springframework.context.annotation.Bean;importorg.springframework.web.client.RestTemplate;MapperScan(cn.itcast.order.mapper)SpringBootApplicationpublicclassOrderApplication{publicstaticvoidmain(String[]args){SpringApplication.run(OrderApplication.class,args);}/** * 注册 RestTemplate 到 Spring 容器供业务代码注入使用 */BeanpublicRestTemplaterestTemplate(){returnnewRestTemplate();}}在 OrderService 里发起远程调用OrderService的queryOrderById方法目前只查了订单。订单对象里带着userId拿着它去调用户服务的接口结果填进order.user即可packagecn.itcast.order.service;importcn.itcast.order.mapper.OrderMapper;importcn.itcast.order.pojo.Order;importcn.itcast.order.pojo.User;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Service;importorg.springframework.web.client.RestTemplate;ServicepublicclassOrderService{AutowiredprivateOrderMapperorderMapper;AutowiredprivateRestTemplaterestTemplate;publicOrderqueryOrderById(LongorderId){// 1.查询订单OrderorderorderMapper.findById(orderId);// 2.利用 RestTemplate 向 user-service 发起 HTTP 远程调用查询用户Stringurlhttp://localhost:8081/user/order.getUserId();UseruserrestTemplate.getForObject(url,User.class);// 3.将用户信息封装进订单对象order.setUser(user);// 4.返回returnorder;}}几个关键点getForObject对应 GET 请求发 POST 用postForObjectAPI 命名和 HTTP 方法一一对应。第一个参数是 URL 字符串效果和你在浏览器地址栏输入一模一样。order.getUserId()从订单里动态取值千万别写死成/user/1。第二个参数告诉RestTemplate返回值要转成什么类型。用户服务返回的是 JSONRestTemplate会自动完成反序列化拿到手的就是User对象不用手动解析。OrderController 返回完整 OrderController 层不需要任何改动user字段被 Service 填充后自然就带出来了。完整的调用链从入口看一遍packagecn.itcast.order.web;importcn.itcast.order.pojo.Order;importcn.itcast.order.service.OrderService;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.PathVariable;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RestController;RestControllerRequestMapping(order)publicclassOrderController{AutowiredprivateOrderServiceorderService;GetMapping({orderId})publicOrderqueryOrderByUserId(PathVariable(orderId)LongorderId){// 根据id查询订单并返回returnorderService.queryOrderById(orderId);}}两个实体类Order里的user字段就是为这一刻准备的packagecn.itcast.order.pojo;importlombok.Data;DatapublicclassOrder{privateLongid;privateLongprice;privateStringname;privateIntegernum;privateLonguserId;privateUseruser;}packagecn.itcast.order.pojo;importlombok.Data;DatapublicclassUser{privateLongid;privateStringusername;privateStringaddress;}User在两个服务里各定义了一份字段要和user-service返回的 JSON 对得上不然反序列化会丢字段后面坑的部分细说。完整调用链重启OrderApplication整条链路是这样的MySQLuser-service (8081)order-service (8080)浏览器MySQLuser-service (8081)order-service (8080)浏览器GET /order/101select * from tb_order where id101订单数据含 userId1HTTP GET /user/1select * from tb_user where id1用户数据JSON自动反序列化为 UserOrderuser 字段已填充验证curlhttp://localhost:8080/order/101# {id:101,price:699900,name:Apple 苹果 iPhone 12 ,num:1,userId:1,# user:{id:1,username:柳岩,address:湖南省衡阳市}}curlhttp://localhost:8080/order/103# user 字段对应 userId3华沉鱼湖北省十堰市user字段有值了跨服务的远程调用完成。服务提供者与服务消费者调用关系里有两个角色角色定义本例中服务提供者一次业务中被其它微服务调用的服务user-service提供GET /user/{id}服务消费者一次业务中调用其它微服务的服务order-service发起 HTTP 调用这对角色不是绝对的而是相对某一次业务而言。如果服务 A 调 B、B 又调 C那么对A 调 B这次业务A 是消费者B 是提供者对B 调 C这次业务B 是消费者C 是提供者也就是说一个服务可以同时身兼两职。随着调用链变长绝大多数中间服务都是这种双重身份。硬编码的 URL撑不了多久现在的写法有个致命细节http://localhost:8081是写死在代码里的。本地玩玩可以生产环境三个问题立刻暴露IP 和端口会变。用户服务换个机器部署、改个端口订单服务就得改代码重新发版。多实例没法负载均衡。user-service 部署了 3 个实例URL 只指向其中一个另外两台闲着写死的那台被打爆。不知道对方是否健康。user-service 宕机了订单服务每次请求都等到超时才报错故障还在服务间扩散。解法是引入注册中心Eureka、Nacos服务启动时把自己的地址注册上去消费者按服务名取地址列表再做负载均衡。这样 URL 就能写成http://userservice/user/1这种形式。具体怎么搭下一篇展开。几个真实的坑URL 拼接少斜杠。http://localhost:8081user/1——字符串拼接时把/丢了RestTemplate会直接抛IllegalArgumentException或 404。拼接后先打印一遍 URL 核对比对着报错猜半天快得多。两份 User 类字段不一致。order-service和user-service各有一份User是两份独立的代码。如果订单服务这份少写了address字段反序列化不会报错只是address静静地变 null——页面上一片空白才知道出问题。排查手法先curl用户服务的原始 JSON确认数据在源头是完整的再核对自己这边的字段名。字段名大小写、JSON 键与 Java 属性的映射任何一处对不上都是静默丢失。超时未配置请求挂死。new RestTemplate()出来的实例默认没有连接和读取超时底层依赖 JDK 的默认值可能长达几分钟。用户服务一慢订单服务的 Tomcat 线程就被一个个占满最后整个服务雪崩。至少要配上连接超时和读取超时packagecn.itcast.order.config;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.http.client.SimpleClientHttpRequestFactory;importorg.springframework.web.client.RestTemplate;ConfigurationpublicclassRestTemplateConfig{BeanpublicRestTemplaterestTemplate(){SimpleClientHttpRequestFactoryfactorynewSimpleClientHttpRequestFactory();factory.setConnectTimeout(1000);// 建立连接超时1 秒factory.setReadTimeout(3000);// 等待响应超时3 秒returnnewRestTemplate(factory);}}超时了宁可快速失败也别把线程吊死在等一个不响应的服务上。API 速览API用途说明restTemplate.getForObject(url, User.class)发 GET 请求并反序列化第二个参数指定目标类型JSON 自动转对象restTemplate.postForObject(url, body, Result.class)发 POST 请求第二个参数是请求体restTemplate.exchange(...)完全控制请求可自定义 Header、方法返回ResponseEntity官方文档Spring Framework - RestTemplateSpring Cloud Hoxton 文档总结数据独立之后跨服务拿数据只有一条路远程调用。它的本质就是发一次 HTTP 请求和浏览器访问没有任何区别。RestTemplate三步走注册 Bean、getForObject发请求并指定返回类型、把结果组装进业务对象。提供者与消费者是相对角色一个服务完全可以同时扮演两者。写死localhost:8081在变更、扩容、健康检查三件事上全都无解注册中心就是为此而生的——下一篇从 Eureka 开始。
返回列表