黑马教学地址
day03-微服务01 - 飞书云文档 (feishu.cn)
RestTemplate
一 问题引入
问题一:
在拆分黑马商城项目的时候,我们发现一个问题:就是cart-service (购物车业务)中需要查询商品信息,但商品信息查询的逻辑全部迁移到了item-service(商品
服务)。由于这是两个model,所以导致我们无法查询到商品信息。
就是说,一个微服务无法访问另外一个微服务的信息。
最终结果就是查询到的购物车数据不完整,导致一个微服务内某个功能的信息不完全。
因此要想解决这个问题,我们就必须改造其中的代码,把原本 本地 方法调用,改造成跨微服务的远程调用(RPC,即Remote Produce Call)
因此,现在查询购物车列表的流程变成了这样,要实现一个微服务用远程调用技术来访问另外一个微服务内的内容:
问题二:
问题来了:我们该如何跨服务调用,准确的说,如何在cart-service
中获取item-service
服务中的提供的商品数据呢?
大家思考一下,我们以前有没有实现过类似的远程查询的功能呢?
答案是肯定的,我们前端向服务端查询数据,其实就是从浏览器远程查询服务端数据。比如我们刚才通过Swagger测试商品查询接口,就是向http://localhost:8081/items
这个接口发起的请求:
而这种查询就是通过http请求的方式来完成的,不仅仅可以实现远程查询,还可以实现新增、删除等各种远程请求。
假如我们在cart-service中能模拟浏览器,发送http请求到item-service,是不是就实现了跨微服务的远程调用了呢?
那么:我们该如何用Java代码发送Http的请求呢?
答案就是: RestTemplate!!!
二 RestTemplate 介绍
Spring给我们提供了一个RestTemplate的API,可以方便的实现Http请求的发送。
org.springframework.web.client public class RestTemplate
extends InterceptingHttpAccessor
implements RestOperations
----------------------------------------------------------------------------------------------------------------
同步客户端执行HTTP请求,在底层HTTP客户端库(如JDK HttpURLConnection、Apache HttpComponents等)上公开一个简单的模板方法API。RestTemplate通过HTTP方法为常见场景提供了模板,此外还提供了支持不太常见情况的通用交换和执行方法。 RestTemplate通常用作共享组件。然而,它的配置不支持并发修改,因此它的配置通常是在启动时准备的。如果需要,您可以在启动时创建多个不同配置的RestTemplate实例。如果这些实例需要共享HTTP客户端资源,它们可以使用相同的底层ClientHttpRequestFactory。 注意:从5.0开始,这个类处于维护模式,只有对更改和错误的小请求才会被接受。请考虑使用org.springframework.web.react .client. webclient,它有更现代的API,支持同步、异步和流场景。
----------------------------------------------------------------------------------------------------------------
自: 3.0 参见: HttpMessageConverter, RequestCallback, ResponseExtractor, ResponseErrorHandler
其中提供了大量的方法,方便我们发送Http请求,例如:
可以看到常见的Get、Post、Put、Delete请求都支持,如果请求参数比较复杂,还可以使用exchange方法来构造请求。
三 实战
我们在cart-service
服务中定义一个配置类:
先将RestTemplate注册为一个Bean:
package com.hmall.cart.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class RemoteCallConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
使用restTemplate,里面的参数都是场景需要,要自己按需求更改的
temIds);
// 2.1.利用RestTemplate发起http请求,得到http的响应
ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(
"http://localhost:8081/items?ids={ids}",
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<ItemDTO>>() {
},
Map.of("ids", CollUtil.join(itemIds, ","))
);
可以看到,利用RestTemplate发送http请求与前端ajax发送请求非常相似,都包含四部分信息:
-
① 请求方式
-
② 请求路径
-
③ 请求参数
-
④ 返回值类型
-
四 总结
服务拆分之后,不可避免的会出现跨微服务的业务,此时微服务之间就需要进行远程调用。微服务之间的远程调用被称为RPC,即远程过程调用。RPC的实现方式有很多,比如:
-
基于Http协议
-
基于Dubbo协议
我们课堂中使用的是Http方式,这种方式不关心服务提供者的具体技术实现,只要对外暴露Http接口即可,更符合微服务的需要。
Java发送http请求可以使用Spring提供的RestTemplate,使用的基本步骤如下:
-
注册RestTemplate到Spring容器
-
调用RestTemplate的API发送请求,常见方法有:
-
getForObject:发送Get请求并返回指定类型对象
-
PostForObject:发送Post请求并返回指定类型对象
-
put:发送PUT请求
-
delete:发送Delete请求
-
exchange:发送任意类型请求,返回ResponseEntity
-
Nacos
一 问题引入:
利用RestTemplate通过Http请求实现了跨微服务的远程调用存在一些问题。
试想一下,假如商品微服务被调用较多,为了应对更高的并发,我们进行了多实例部署
此时,每个item-service
的实例其IP或端口不同,问题来了:
-
item-service这么多实例,cart-service如何知道每一个实例的地址?
-
http请求要写url地址,
cart-service
服务到底该调用哪个实例呢? -
如果在运行过程中,某一个
item-service
实例宕机,cart-service
依然在调用该怎么办? -
如果并发太高,
item-service
临时多部署了N台实例,cart-service
如何知道新实例的地址?
为了解决上述问题,就必须引入注册中心的概念了,接下来我们就一起来分析下注册中心的原理。
二 注册中心原理
在微服务远程调用的过程中,包括两个角色:
-
服务提供者:提供接口供其它微服务访问,比如
item-service
-
服务消费者:调用其它微服务提供的接口,比如
cart-service
在大型微服务项目中,服务提供者的数量会非常多,为了管理这些服务就引入了注册中心的概念。注册中心、服务提供者、服务消费者三者间关系如下:
注册中心流程如下:
-
服务启动时就会注册自己的服务信息(服务名、IP、端口)到注册中心
-
调用者可以从注册中心订阅想要的服务,获取服务对应的实例列表(1个服务可能多实例部署)
-
调用者自己对实例列表负载均衡,挑选一个实例
-
调用者向该实例发起远程调用
当服务提供者的实例宕机或者启动新实例时,调用者如何得知呢?
-
服务提供者会定期向注册中心发送请求,报告自己的健康状态(心跳请求)
-
当注册中心长时间收不到提供者的心跳时,会认为该实例宕机,将其从服务的实例列表中剔除
-
当服务有新实例启动时,会发送注册服务请求,其信息会被记录在注册中心的服务实例列表
-
当注册中心服务列表变更时,会主动通知微服务,更新本地服务列表
我们使用的注册中心是nacos
目前开源的注册中心框架有很多,国内比较常见的有:
-
Eureka:Netflix公司出品,目前被集成在SpringCloud当中,一般用于Java应用
-
Nacos:Alibaba公司出品,目前被集成在SpringCloudAlibaba中,一般用于Java应用
-
Consul:HashiCorp公司出品,目前集成在SpringCloud中,不限制微服务语言
以上几种注册中心都遵循SpringCloud中的API规范,因此在业务开发使用上没有太大差异。由于Nacos是国内产品,中文文档比较丰富,而且同时具备配置管理功能(后面会学习),因此在国内使用较多
三 安装nacos
前置步骤,安装等请看教学文档。
上面将nacos端口号设置为8848。
a few moment later......
启动完成后,访问下面地址:http://192.168.150.101:8848/nacos/,注意将192.168.150.101
替换为你自己的虚拟机IP地址。首次访问会跳转到登录页,账号密码都是nacos
登陆成功后的页面
四 快速入门
服务注册
添加依赖
<!--nacos 服务注册发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
配置nacos路径
按道理来说是可以在application.yml下面配置的,但是我这里一直出错。
-Dspring.cloud.nacos.server-addr=192.168.6.100:8848
配置完之后服务会自动注册,无序手动添加。
可以看到nacos上更新了服务
服务发现
服务的消费者要去nacos订阅服务,这个过程就是服务发现,步骤如下:
-
1 引入依赖
-
2 配置Nacos地址
-
3 发现并调用服务
前两部和服务注册一样,添加依赖和配置nacos路径
发现并调用服务
private final DiscoveryClient discoveryClient;
Set<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet());
// 2.0查询商品
//服务发现与使用
//2.0.1使用服务名字获取服务实例
List<ServiceInstance> instances = discoveryClient.getInstances("item-service");
//2.0.2 负载均衡,挑选一个实例
ServiceInstance instance = instances.get(RandomUtil.randomInt(instances.size()));
//2.0.3 获取uri (uri = ip + 端口号)
URI uri = instance.getUri();
// 2.1.利用RestTemplate发起http请求,得到http的响应
ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(
uri + "/items?ids={ids}",
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<ItemDTO>>() {
},
Map.of("ids", CollUtil.join(itemIds, ","))
);
OpenFeign
一 问题引入
在上面,我们利用Nacos实现了服务的治理,利用RestTemplate实现了服务的远程调用。但是远程调用的代码太复杂了
因此,我们必须想办法改变远程调用的开发模式,让远程调用像本地方法调用一样简单。而这就要用到OpenFeign组件了。
其实远程调用的关键点就在于四个:
-
请求方式
-
请求路径
-
请求参数
-
返回值类型
所以,OpenFeign就利用SpringMVC的相关注解来声明上述4个参数,然后基于动态代理帮我们生成远程调用的代码,而无需我们手动再编写,非常方便。
二 快速入门
在原来nacos服务发现与调用和restTemplate发送请求来实现跨微服务请求资料的过程中,可以总结为以下四点
openFeign会一一对应来实现
1 导入依赖
<!--openFeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--负载均衡器-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
2 开启openFeign
在启动类上加 @EnableFeignClients 注解
package com.hmall.cart;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@EnableFeignClients
@MapperScan("com.hmall.cart.mapper")
@SpringBootApplication
public class HMallCartApplication {
public static void main(String[] args) {
SpringApplication.run(HMallCartApplication.class, args);
}
//做http请求
@Bean
public RestTemplate createRestTemplate(){
return new RestTemplate();
}
}
3 设置openFeign配置接口
package com.hmall.cart.cliect;
import com.hmall.cart.domain.dto.ItemDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.Collection;
import java.util.List;
@FeignClient("item-service")
public interface ItemClient {
@GetMapping("/items")
List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);
}
这里只需要声明接口,无需实现方法。接口中的几个关键信息:
-
@FeignClient("item-service")
:声明服务名称 -
@GetMapping
:声明请求方式 -
@GetMapping("/items")
:声明请求路径 -
@RequestParam("ids") Collection<Long> ids
:声明请求参数 -
List<ItemDTO>
:返回值类型
这里可以看到一一对应上面的,繁琐的代码
private final ItemClient itemClient;
private void handleCartItems(List<CartVO> vos) {
// TODO 1.获取商品id
Set<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet());
// // 2.0查询商品
// //服务发现与使用
// //2.0.1使用服务名字获取服务实例
// List<ServiceInstance> instances = discoveryClient.getInstances("item-service");
// //2.0.2 负载均衡,挑选一个实例
// ServiceInstance instance = instances.get(RandomUtil.randomInt(instances.size()));
// //2.0.3 获取uri (uri = ip + 端口号)
// URI uri = instance.getUri();
// // 2.1.利用RestTemplate发起http请求,得到http的响应
// ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(
// uri + "/items?ids={ids}",
// HttpMethod.GET,
// null,
// new ParameterizedTypeReference<List<ItemDTO>>() {
// },
// Map.of("ids", CollUtil.join(itemIds, ","))
// );
// // 2.2.解析响应
// if(!response.getStatusCode().is2xxSuccessful()){
// // 查询失败,直接结束
// return;
// }
// List<ItemDTO> items = response.getBody();
//调用接口
List<ItemDTO> items = itemClient.queryItemByIds(itemIds);
if (CollUtils.isEmpty(items)) {
return;
}
// 3.转为 id 到 item的map
Map<Long, ItemDTO> itemMap = items.stream().collect(Collectors.toMap(ItemDTO::getId, Function.identity()));
// 4.写入vo
for (CartVO v : vos) {
ItemDTO item = itemMap.get(v.getItemId());
if (item == null) {
continue;
}
v.setNewPrice(item.getPrice());
v.setStatus(item.getStatus());
v.setStock(item.getStock());
}
}
三 连接池
Feign底层发起http请求,依赖于其它的框架。其底层支持的http客户端实现包括:
-
HttpURLConnection:默认实现,不支持连接池
-
Apache HttpClient :支持连接池
-
OKHttp:支持连接池
默认底层不支持连接池,每一次调用方法都会创建一次HttpURLConnection,效率低下。
因此我们通常会使用带有连接池的客户端来代替默认的HttpURLConnection。比如,我们使用OK Http.
引入依赖
<!--OK http 的依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
开启连接池
feign:
okhttp:
enabled: true # 开启OKHttp功能
四 最佳实践
由于我们每一个微服务都要去远程调用,就是说我们每一个微服务都要写上面相关的client和引入与本微服务无关的一些类,麻烦而且耦合度上升,我们需要想方案来优化这些东西。
相信大家都能想到,避免重复编码的办法就是抽取。不过这里有两种抽取思路:
-
思路1:抽取到微服务之外的公共module
-
思路2:每个微服务自己抽取一个module
方案1抽取更加简单,工程结构也比较清晰,但缺点是整个项目耦合度偏高。
方案2抽取相对麻烦,工程结构相对更复杂,但服务之间耦合度降低。
因此这里我们采用方案1来演示
演示部分
我们已抽取cart-service的内容为例
此微服务引入了client, dto, 和相关的依赖
注意!!!
1 将本模块的信息给cart-service添加依赖
2 由于client-service启动类扫描不到我们在外面的api接口,我们要手动加上扫描,在启动feign注解后面加上我们client的包路径
五 日志
OpenFeign只会在FeignClient所在包的日志级别为DEBUG时,才会输出日志。而且其日志级别有4级:
-
NONE:不记录任何日志信息,这是默认值。
-
BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
-
HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
-
FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。
Feign默认的日志级别就是NONE,所以默认我们看不到请求日志
如果我们想设置日志级别,需要自己手动配置
先保证日志输出的环境是debug环境,我这里已经是debug环境了
步骤一 在hm-api模块下新建一个配置类,定义Feign的日志级别:
步骤二 要让日志级别生效,还需要配置这个类。
有两种方式:
-
局部生效:在某个
FeignClient
中配置,只对当前FeignClient
生效
@FeignClient(value = "item-service", configuration = DefaultFeignConfig.class)
-
全局生效:在
@EnableFeignClients
中配置,针对所有FeignClient
生效。
@EnableFeignClients(defaultConfiguration = DefaultFeignConfig.class)