目录
11.1.1 微服务通信方案解读
11.1.1.1 RPC 方案
- RPC 实现微服务通信的核心思想
- 全局注册表:将 RPC 支持的所有方法都注册进去
- 通过将 Java 对象进行编码(IDL、JSON、XML 等等) + 方法名传递(TCP / IP 协议)到目标服务器实现微服务通信
11.1.1.2 HTTP 方案 (Rest)
- 认识 HTTP
- 标准化的 HTTP 协议(GET、POST、PUT、DELETE等),目前主流的微服务框架通信实现都是 HTTP
- 简单、标准,需要做的工作和维护工作少;几乎不需要做额外的工作即可与其他的微服务集成
11.1.1.3 Message 方案
- 认识 Message
- 通过 Kafka、RocketMQ 等消息队列实现消息的发布与订阅(消费)
- 可以实现(削峰填谷),缓冲机制实现数据、任务处理
- 最大的缺点就是只能够做到最终一致性,而不能做到实时一致性;当然,这也是看业务需求
11.1.1.4 微服务通信该如何选择
- 结合微服务框架与业务的需要做出选择
- SpringCloud 建议的通信方法是 OpenFeign(Rest)
- 需要最终一致性且不要求快速响应的业务场景可以选择使用 Message
- 问题来了:SpringCloud 可不可以使用 RPC 呢? (但是,要有足够强的理由说明你为什么要使用 RPC)
11.2.1 使用RestTemplate实现微服务通信 (基本很少用,除非第三方对接)
11.2.1.1 RestTemplate 思想
- 使用 RestTemplate 的两种方式(思想)
- 在代码(或配置文件中)写死 IP 和 端口号 (需要知道,这并不是不可行!)
- 通过注册中心获取服务地址,可以实现负载均衡的效果
11.2.1.2 RestTemplate 方式模拟获取 Token
在实践项目进行:sca-commerce-alibaba-nacos-client
使用 RestTemplate 实现微服务通信
package com.edcode.commerce.service.communication;
import com.alibaba.fastjson.JSON;
import com.edcode.commerce.constant.CommonConstant;
import com.edcode.commerce.vo.JwtToken;
import com.edcode.commerce.vo.UsernameAndPassword;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
/**
* @author eddie.lee
* @blog blog.eddilee.cn
* @description 使用 RestTemplate 实现微服务通信
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class UseRestTemplateService {
/**
* 随机挑选客户端(负载均衡)
*/
private final LoadBalancerClient loadBalancerClient;
/**
* 从授权服务中获取 JwtToken
*/
public JwtToken getTokenFromAuthorityService(UsernameAndPassword usernameAndPassword) {
// 第一种方式: 写死 url
String requestUrl = "http://127.0.0.1:7000/scacommerce-authority-center" + "/authority/token";
log.info("RestTemplate请求url和正文: [{}], [{}]",
requestUrl,
JSON.toJSONString(usernameAndPassword)
);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return new RestTemplate().postForObject(
requestUrl,
new HttpEntity<>(JSON.toJSONString(usernameAndPassword), headers),
JwtToken.class
);
}
/**
* 从授权服务中获取 JwtToken, 且带有负载均衡
*/
public JwtToken getTokenFromAuthorityServiceWithLoadBalancer(UsernameAndPassword usernameAndPassword) {
// 第二种方式: 通过注册中心拿到服务的信息(是所有的实例), 再去发起调用
ServiceInstance serviceInstance = loadBalancerClient.choose(CommonConstant.AUTHORITY_CENTER_SERVICE_ID);
log.info("Nacos客户端信息: [{}], [{}], [{}]",
serviceInstance.getServiceId(),
serviceInstance.getInstanceId(),
JSON.toJSONString(serviceInstance.getMetadata())
);
// 与第一种方式区别,就是不用在写死 url, 从 loadBalancerClient 里面获取
String requestUrl = String.format(
"http://%s:%s/scacommerce-authority-center/authority/token",
serviceInstance.getHost(),
serviceInstance.getPort()
);
log.info("登录请求url和正文: [{}], [{}]", requestUrl, JSON.toJSONString(usernameAndPassword));
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return new RestTemplate().postForObject(
requestUrl,
new HttpEntity<>(JSON.toJSONString(usernameAndPassword), headers),
JwtToken.class
);
}
}
微服务通信 Controller
package com.edcode.commerce.controller;
import com.edcode.commerce.service.communication.UseRestTemplateService;
import com.edcode.commerce.vo.JwtToken;
import com.edcode.commerce.vo.UsernameAndPassword;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author eddie.lee
* @blog blog.eddilee.cn
* @description 微服务通信 Controller
*/
@RestController
@RequestMapping("/communication")
@RequiredArgsConstructor
public class CommunicationController {
private final UseRestTemplateService restTemplateService;
@PostMapping("/rest-template")
public JwtToken getTokenFromAuthorityService(@RequestBody UsernameAndPassword usernameAndPassword) {
return restTemplateService.getTokenFromAuthorityService(usernameAndPassword);
}
@PostMapping("/rest-template-load-balancer")
public JwtToken getTokenFromAuthorityServiceWithLoadBalancer(@RequestBody UsernameAndPassword usernameAndPassword) {
return restTemplateService.getTokenFromAuthorityServiceWithLoadBalancer(usernameAndPassword);
}
}
HTTP请求测试
communication.http
### 获取 Token
POST http://127.0.0.1:8000/scacommerce-nacos-client/communication/rest-template
Content-Type: application/json
{
"username": "eddie@qq.com",
"password": "25d55ad283aa400af464c76d713c07ad"
}
### 获取 Token, 带有负载均衡
POST http://127.0.0.1:8000/scacommerce-nacos-client/communication/rest-template-load-balancer
Content-Type: application/json
{
"username": "eddie@qq.com",
"password": "25d55ad283aa400af464c76d713c07ad"
}
###
日志输出
2021-11-17 22:05:43.168 INFO [sca-commerce-nacos-client,ee3376fb85e00ef1,ee3376fb85e00ef1,true] 19192 --- [nio-8000-exec-1] c.e.c.s.c.UseRestTemplateService : RestTemplate请求url和正文: [http://127.0.0.1:7000/scacommerce-authority-center/authority/token], [{"password":"25d55ad283aa400af464c76d713c07ad","username":"eddie@qq.com"}]
...
2021-11-17 22:06:33.216 INFO [sca-commerce-nacos-client,cffcaf69c382565e,cffcaf69c382565e,true] 19192 --- [nio-8000-exec-9] c.e.c.s.c.UseRestTemplateService : Nacos客户端信息: [sca-commerce-authority-center], [192.168.3.192:7000], [{"preserved.register.source":"SPRING_CLOUD","management.context-path":"/scacommerce-authority-center/actuator"}]
2021-11-17 22:06:33.216 INFO [sca-commerce-nacos-client,cffcaf69c382565e,cffcaf69c382565e,true] 19192 --- [nio-8000-exec-9] c.e.c.s.c.UseRestTemplateService : 登录请求url和正文: [http://192.168.3.192:7000/scacommerce-authority-center/authority/token], [{"password":"25d55ad283aa400af464c76d713c07ad","username":"eddie@qq.com"}]
2021-11-17 22:07:03.197 INFO [sca-commerce-nacos-client,,,] 19192 --- [erListUpdater-0] c.netflix.config.ChainedDynamicProperty : Flipping property: sca-commerce-authority-center.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
11.3.1 Ribbon实现微服务通信及其原理
11.3.1.1 SpringCloud Netflix Ribbon 实现微服务通信及其原理
- 如何使用 SpringCloud Netflix Ribbon
- pom.xml 文件中引入 Ribbon 依赖
- 增强 RestTemplate,添加 @LoadBalanced 注解,使之具备负载均衡的能力
- SpringCloud Netflix Ribbon 实现原理
- 根据服务名从注册中心获取服务地址 + 负载均衡策略
pom.xml
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-ribbon</artifactId>
</dependency>
@LoadBalanced 注解
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
11.3.1.1 SpringCloud Netflix Ribbon 实践
使用 Ribbon 之前的配置, 增强 RestTemplate
package com.edcode.commerce.service.communication.ribbon;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
/**
* @author eddie.lee
* @blog blog.eddilee.cn
* @description 使用 Ribbon 之前的配置, 增强 RestTemplate
*/
@Component
public class RibbonConfig {
/**
* 注入 RestTemplate
* @return
*/
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
使用 Ribbon 实现微服务通信
package com.edcode.commerce.service.communication.ribbon;
import com.alibaba.fastjson.JSON;
import com.edcode.commerce.constant.CommonConstant;
import com.edcode.commerce.vo.JwtToken;
import com.edcode.commerce.vo.UsernameAndPassword;
import com.netflix.loadbalancer.*;
import com.netflix.loadbalancer.reactive.LoadBalancerCommand;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import rx.Observable;
import java.util.ArrayList;
import java.util.List;
/**
* @author eddie.lee
* @blog blog.eddilee.cn
* @description 使用 Ribbon 实现微服务通信
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class UseRibbonService {
private final RestTemplate restTemplate;
private final DiscoveryClient discoveryClient;
/**
* 通过 Ribbon 调用 Authority 服务获取 Token
* */
public JwtToken getTokenFromAuthorityServiceByRibbon(
UsernameAndPassword usernameAndPassword) {
// 注意到 url 中的 ip 和端口换成了服务名称
String requestUrl = String.format(
"http://%s/scacommerce-authority-center/authority/token",
CommonConstant.AUTHORITY_CENTER_SERVICE_ID
);
log.info("登录请求url和正文: [{}], [{}]", requestUrl,
JSON.toJSONString(usernameAndPassword));
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
// 这里一定要使用自己注入的 RestTemplate
return restTemplate.postForObject(
requestUrl,
new HttpEntity<>(JSON.toJSONString(usernameAndPassword), headers),
JwtToken.class
);
}
/**
* 使用原生的 Ribbon Api, 看看 Ribbon 是如何完成: 服务调用 + 负载均衡
* */
public JwtToken thinkingInRibbon(UsernameAndPassword usernameAndPassword) {
String urlFormat = "http://%s/scacommerce-authority-center/authority/token";
// 1. 找到服务提供方的地址和端口号
List<ServiceInstance> targetInstances = discoveryClient.getInstances(
CommonConstant.AUTHORITY_CENTER_SERVICE_ID
);
// 构造 Ribbon 服务列表
List<Server> servers = new ArrayList<>(targetInstances.size());
targetInstances.forEach(i -> {
servers.add(new Server(i.getHost(), i.getPort()));
log.info("找到目标实例: [{}] -> [{}]", i.getHost(), i.getPort());
});
// 2. 使用负载均衡策略实现远端服务调用
// 构建 Ribbon 负载实例
BaseLoadBalancer loadBalancer = LoadBalancerBuilder.newBuilder()
.buildFixedServerListLoadBalancer(servers);
// 设置负载均衡策略
loadBalancer.setRule(new RetryRule(new RandomRule(), 300));
String result = LoadBalancerCommand.builder().withLoadBalancer(loadBalancer)
.build().submit(server -> {
String targetUrl = String.format(
urlFormat,
String.format("%s:%s",
server.getHost(),
server.getPort()
)
);
log.info("目标请求url: [{}]", targetUrl);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
String tokenStr = new RestTemplate().postForObject(
targetUrl,
new HttpEntity<>(JSON.toJSONString(usernameAndPassword), headers),
String.class
);
return Observable.just(tokenStr);
}).toBlocking().first().toString();
return JSON.parseObject(result, JwtToken.class);
}
}
微服务通信 Controller
package com.edcode.commerce.controller;
import com.edcode.commerce.service.communication.restTemplate.UseRestTemplateService;
import com.edcode.commerce.service.communication.ribbon.UseRibbonService;
import com.edcode.commerce.vo.JwtToken;
import com.edcode.commerce.vo.UsernameAndPassword;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author eddie.lee
* @blog blog.eddilee.cn
* @description 微服务通信 Controller
*/
@RestController
@RequestMapping("/communication")
@RequiredArgsConstructor
public class CommunicationController {
private final UseRibbonService ribbonService;
@PostMapping("/ribbon")
public JwtToken getTokenFromAuthorityServiceByRibbon(@RequestBody UsernameAndPassword usernameAndPassword) {
return ribbonService.getTokenFromAuthorityServiceByRibbon(usernameAndPassword);
}
@PostMapping("/thinking-in-ribbon")
public JwtToken thinkingInRibbon(@RequestBody UsernameAndPassword usernameAndPassword) {
return ribbonService.thinkingInRibbon(usernameAndPassword);
}
}
communication.http
### 通过 Ribbon 去获取 Token
POST http://127.0.0.1:8000/scacommerce-nacos-client/communication/ribbon
Content-Type: application/json
{
"username": "eddie@qq.com",
"password": "25d55ad283aa400af464c76d713c07ad"
}
### 通过原生 Ribbon Api 去获取 Token
POST http://127.0.0.1:8000/scacommerce-nacos-client/communication/thinking-in-ribbon
Content-Type: application/json
{
"username": "eddie@qq.com",
"password": "25d55ad283aa400af464c76d713c07ad"
}
###
日志输出
2021-11-17 22:59:47.748 INFO [sca-commerce-nacos-client,8d83d0dae46c4dd5,8d83d0dae46c4dd5,true] 19612 --- [nio-8000-exec-1] c.e.c.s.c.ribbon.UseRibbonService : 登录请求url和正文: [http://sca-commerce-authority-center/scacommerce-authority-center/authority/token], [{"password":"25d55ad283aa400af464c76d713c07ad","username":"eddie@qq.com"}]
2021-11-17 22:59:48.200 INFO [sca-commerce-nacos-client,8d83d0dae46c4dd5,80859917190dad7f,true] 19612 --- [nio-8000-exec-1] c.netflix.config.ChainedDynamicProperty : Flipping property: sca-commerce-authority-center.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
2021-11-17 22:59:48.220 INFO [sca-commerce-nacos-client,8d83d0dae46c4dd5,80859917190dad7f,true] 19612 --- [nio-8000-exec-1] c.netflix.loadbalancer.BaseLoadBalancer : Client: sca-commerce-authority-center instantiated a LoadBalancer: DynamicServerListLoadBalancer:{NFLoadBalancer:name=sca-commerce-authority-center,current list of Servers=[],Load balancer stats=Zone stats: {},Server stats: []}ServerList:null
2021-11-17 22:59:48.224 INFO [sca-commerce-nacos-client,8d83d0dae46c4dd5,80859917190dad7f,true] 19612 --- [nio-8000-exec-1] c.n.l.DynamicServerListLoadBalancer : Using serverListUpdater PollingServerListUpdater
2021-11-17 22:59:50.276 INFO [sca-commerce-nacos-client,8d83d0dae46c4dd5,80859917190dad7f,true] 19612 --- [nio-8000-exec-1] c.n.l.DynamicServerListLoadBalancer : DynamicServerListLoadBalancer for client sca-commerce-authority-center initialized: DynamicServerListLoadBalancer:{NFLoadBalancer:name=sca-commerce-authority-center,current list of Servers=[],Load balancer stats=Zone stats: {},Server stats: []}ServerList:com.alibaba.cloud.nacos.ribbon.NacosServerList@2d424cd6
...
2021-11-17 23:35:32.712 INFO [sca-commerce-nacos-client,fcc5287fe3467330,fcc5287fe3467330,true] 19612 --- [nio-8000-exec-3] c.e.c.s.c.ribbon.UseRibbonService : 找到目标实例: [192.168.3.192] -> [7000]
2021-11-17 23:35:32.714 INFO [sca-commerce-nacos-client,fcc5287fe3467330,fcc5287fe3467330,true] 19612 --- [nio-8000-exec-3] c.netflix.config.ChainedDynamicProperty : Flipping property: default.ribbon.ActiveConnectionsLimit to use NEXT property: niws.loadbalancer.availabilityFilteringRule.activeConnectionsLimit = 2147483647
2021-11-17 23:35:32.715 INFO [sca-commerce-nacos-client,fcc5287fe3467330,fcc5287fe3467330,true] 19612 --- [nio-8000-exec-3] c.netflix.loadbalancer.BaseLoadBalancer : Client: default instantiated a LoadBalancer: {NFLoadBalancer:name=default,current list of Servers=[],Load balancer stats=Zone stats: {},Server stats: []}
2021-11-17 23:35:32.749 INFO [sca-commerce-nacos-client,fcc5287fe3467330,fcc5287fe3467330,true] 19612 --- [nio-8000-exec-3] c.e.c.s.c.ribbon.UseRibbonService : 目标请求url: [http://192.168.3.192:7000/scacommerce-authority-center/authority/token]
11.4.1 SpringCloud OpenFeign 的简单应用
OpenFeign 基于 Ribbon 实现,而 Ribbon 基于 RestTemplate 实现
11.4.1.1 如何使用 OpenFeign
- pom.xml 引入 open-feign 依赖
- 添加 @EnableFeignClients 注解,启用 open-feign
11.4.1.2 OpenFeign 简单实践
pom.xml 引入 open-feign 依赖
<!-- open feign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
添加 @EnableFeignClients 注解
@RefreshScope
@EnableDiscoveryClient
@SpringBootApplication
@EnableFeignClients
public class NacosClientApplication {
public static void main(String[] args) {
SpringApplication.run(NacosClientApplication.class, args);
}
}
与 Authority 服务通信的 Feign Client 接口定义
package com.edcode.commerce.service.communication.feign;
import com.edcode.commerce.vo.JwtToken;
import com.edcode.commerce.vo.UsernameAndPassword;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
/**
* @author eddie.lee
* @blog blog.eddilee.cn
* @description 与 Authority 服务通信的 Feign Client 接口定义
*/
@FeignClient(contextId = "AuthorityFeignClient", value = "sca-commerce-authority-center")
public interface AuthorityFeignClient {
/**
* 通过 OpenFeign 访问 Authority 获取 Token
* @param usernameAndPassword
* @return
*/
@RequestMapping(value = "/scacommerce-authority-center/authority/token", method = RequestMethod.POST, consumes = "application/json", produces = "application/json")
JwtToken getTokenByFeign(@RequestBody UsernameAndPassword usernameAndPassword);
}
创建 POST API
@RestController
@RequestMapping("/communication")
@RequiredArgsConstructor
public class CommunicationController {
private final AuthorityFeignClient feignClient;
@PostMapping("/token-by-feign")
public JwtToken getTokenByFeign(@RequestBody UsernameAndPassword usernameAndPassword) {
return feignClient.getTokenByFeign(usernameAndPassword);
}
}
communication.http
### 通过 OpenFeign 获取 Token
POST http://127.0.0.1:8000/scacommerce-nacos-client/communication/token-by-feign
Content-Type: application/json
{
"username": "eddie@qq.com",
"password": "25d55ad283aa400af464c76d713c07ad"
}
11.5.1 配置 SpringCloud OpenFeign
11.5.1.1 最常用的配置
- OpenFeign 开启 gzip 压缩 (通常不会开启,除非特殊场景)
- 统一 OpenFeign 使用配置:日志、重试、请求连接和响应时间限制
- 使用 okhttp 替换 httpclient (需要引入 okhttp 依赖)
11.5.1.2 OpenFeign 开启 gzip 压缩
# Feign 的相关配置
feign:
# feign 开启 gzip 压缩
compression:
# 压缩请求数据
request:
enabled: true
min-request-size: 1024
mime-types: text/xml,application.xml,application/json
# 压缩响应数据
response:
enabled: true
11.5.1.3 统一 OpenFeign 使用配置:日志、重试、请求连接和响应时间限制
package com.edcode.commerce.service.communication.feign;
import feign.Logger;
import feign.Request;
import feign.Retryer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
import static java.util.concurrent.TimeUnit.SECONDS;
/**
* @author eddie.lee
* @blog blog.eddilee.cn
* @description OpenFeign 配置类
*/
@Configuration
public class FeignConfig {
/**
* 开启 OpenFeign 日志
*/
@Bean
public Logger.Level feignLogger() {
// 需要注意, 日志级别需要修改成 debug
return Logger.Level.FULL;
}
/**
* OpenFeign 开启重试
* period = 100 发起当前请求的时间间隔, 单位是 ms
* maxPeriod = 1000 发起当前请求的最大时间间隔, 单位是 ms
* maxAttempts = 5 最多请求次数
*/
@Bean
public Retryer feignRetryer() {
return new Retryer.Default(100,
SECONDS.toMillis(1),
5);
}
/**
* 连接超时,单位是 ms
*/
public static final int CONNECT_TIMEOUT_MILLS = 5000;
/**
* 读取超时,单位是 ms
*/
public static final int READ_TIMEOUT_MILLS = 5000;
/**
* 对请求的连接和响应时间进行限制
*/
@Bean
public Request.Options options() {
return new Request.Options(
CONNECT_TIMEOUT_MILLS,
TimeUnit.MICROSECONDS,
READ_TIMEOUT_MILLS,
TimeUnit.MILLISECONDS,
true
);
}
}
11.5.1.4 使用 okhttp 替换 httpclient (需要引入 okhttp 依赖)
Maven 依赖
<!-- feign 替换 JDK 默认的 URLConnection 为 okhttp -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
bootstrap.yml 配置
# Feign 的相关配置
feign:
# 禁用默认的 http, 开启 okhttp
httpclient:
enabled: false
okhttp:
enabled: true
OpenFeign 使用 OkHttp 配置类
package com.edcode.commerce.service.communication.feign;
import feign.Feign;
import okhttp3.ConnectionPool;
import okhttp3.OkHttpClient;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.cloud.openfeign.FeignAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
/**
* @author eddie.lee
* @blog blog.eddilee.cn
* @description OpenFeign 使用 OkHttp 配置类
*
* 尽管不写这个配置类,okhttp也是能用,这个配置类并非必要,但通常都会写,使得 okhttp 更友好
*/
@Configuration
@ConditionalOnClass(Feign.class)
@AutoConfigureBefore(FeignAutoConfiguration.class) // 在 feign 初始化之前就注入 okhttp
public class FeignOkHttpConfig {
/**
* 注入 OkHttp, 并自定义配置
* @return
*/
@Bean
public okhttp3.OkHttpClient okHttpClient() {
return new OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS) // 设置连接超时
.readTimeout(5, TimeUnit.SECONDS) // 设置读超时
.writeTimeout(5, TimeUnit.SECONDS) // 设置写超时
.retryOnConnectionFailure(true) // 是否自动重连
// 配置连接池中的最大空闲线程个数为 10, 并保持 5 分钟
.connectionPool(
new ConnectionPool(10, 5L, TimeUnit.MINUTES)
)
.build();
}
}
尽管不写这个配置类,okhttp也是能用,这个配置类并非必要,但通常都会写,使得 okhttp 更友好
11.6.1 通过Feign的原生API解析其实现原理(理解即可)
11.6.1.1 Feign 工作流程图
图片出自于:Feign 工作流程图解
11.6.1.2 Feign 客户端初始化, 必须要配置 encoder、decoder、contract
<!-- 使用原生的 Feign Api 做的自定义配置, encoder 和 decoder -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-gson</artifactId>
<version>11.0</version>
</dependency>
11.6.1.3 使用 Feign 的原生 Api, 而不是 OpenFeign = Feign + Ribbon
原生Api实践
package com.edcode.commerce.service.communication.feign;
import com.edcode.commerce.vo.JwtToken;
import com.edcode.commerce.vo.UsernameAndPassword;
import feign.Feign;
import feign.Logger;
import feign.gson.GsonDecoder;
import feign.gson.GsonEncoder;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.cloud.openfeign.support.SpringMvcContract;
import org.springframework.stereotype.Service;
import java.lang.annotation.Annotation;
import java.util.List;
import java.util.Random;
/**
* @author eddie.lee
* @blog blog.eddilee.cn
* @description 使用 Feign 的原生 Api, 而不是 OpenFeign = Feign + Ribbon
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class UseFeignApi {
private final DiscoveryClient discoveryClient;
/**
* 使用 Feign 原生 api 调用远端服务
*
* Feign 默认配置初始化、设置自定义配置、生成代理对象
*/
public JwtToken thinkingInFeign(UsernameAndPassword usernameAndPassword) {
// 通过反射去拿 serviceId
String serviceId = null;
Annotation[] annotations = AuthorityFeignClient.class.getAnnotations();
for (Annotation annotation : annotations) {
if (annotation.annotationType().equals(FeignClient.class)) {
serviceId = ((FeignClient) annotation).value();
log.info("get service id from AuthorityFeignClient: [{}]", serviceId);
break;
}
}
// 如果服务 id 不存在, 直接抛异常
if (null == serviceId) {
throw new RuntimeException("can not get serviceId");
}
// 通过 serviceId 去拿可用服务实例
List<ServiceInstance> targetInstances = discoveryClient.getInstances(serviceId);
if (CollectionUtils.isEmpty(targetInstances)) {
throw new RuntimeException("can not get target instance from serviceId: " +
serviceId);
}
// 随机选择一个服务实例: 负载均衡
ServiceInstance randomInstance = targetInstances.get(
new Random().nextInt(targetInstances.size())
);
log.info("choose service instance: [{}], [{}], [{}]", serviceId,
randomInstance.getHost(), randomInstance.getPort());
// Feign 客户端初始化, 必须要配置 encoder、decoder、contract
AuthorityFeignClient feignClient = Feign.builder() // 1. Feign 默认配置初始化
.encoder(new GsonEncoder()) // 2.1 设置定义配置
.decoder(new GsonDecoder()) // 2.2 设置定义配置
.logLevel(Logger.Level.FULL) // 2.3 设置定义配置
.contract(new SpringMvcContract())
.target( // 3 生成代理对象
AuthorityFeignClient.class,
String.format("http://%s:%s",
randomInstance.getHost(), randomInstance.getPort())
);
return feignClient.getTokenByFeign(usernameAndPassword);
}
}
控制层
package com.edcode.commerce.controller;
import com.edcode.commerce.service.communication.feign.AuthorityFeignClient;
import com.edcode.commerce.service.communication.feign.UseFeignApi;
import com.edcode.commerce.service.communication.restTemplate.UseRestTemplateService;
import com.edcode.commerce.service.communication.ribbon.UseRibbonService;
import com.edcode.commerce.vo.JwtToken;
import com.edcode.commerce.vo.UsernameAndPassword;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author eddie.lee
* @blog blog.eddilee.cn
* @description 微服务通信 Controller
*/
@RestController
@RequestMapping("/communication")
@RequiredArgsConstructor
public class CommunicationController {
private final UseFeignApi useFeignApi;
@PostMapping("/thinking-in-feign")
public JwtToken thinkingInFeign(@RequestBody UsernameAndPassword usernameAndPassword) {
return useFeignApi.thinkingInFeign(usernameAndPassword);
}
}
communication.http
### 通过原生 Feign Api 获取 Token
POST http://127.0.0.1:8000/scacommerce-nacos-client/communication/thinking-in-feign
Content-Type: application/json
{
"username": "eddie@qq.com",
"password": "25d55ad283aa400af464c76d713c07ad"
}
11.6.1.4 Feign 客户端初始化的过程
- Feign 客户端初始化包含三个部分
AuthorityFeignClient feignClient = Feign.builder() // 1. Feign 默认配置初始化
.encoder(new GsonEncoder()) // 2.1 设置定义配置
.decoder(new GsonDecoder()) // 2.2 设置定义配置
.logLevel(Logger.Level.FULL) // 2.3 设置定义配置
.contract(new SpringMvcContract())
.target( // 3 生成代理对象
AuthorityFeignClient.class,
String.format("http://%s:%s",
randomInstance.getHost(), randomInstance.getPort())
);
11.7.1 微服务通信总结
- 三类常用的微服务通信方案
- RPC 效率高,可选实现方式多
- REST 标准化程度高,学习、使用成本低
- Message 对于削峰填谷有重大意义
- Rest -> Ribbon -> OpenFeign 演进过程
- Rest 需要写死服务的 IP 与 Port (可以通过注册中心手动获取),灵活性低
- Ribbon 提供基于 RestTemplate 的 HTTP 客户端并且支持服务负载均衡功能
- OpenFeign 基于 Ribbon,只需要使用注解和接口的配置即可完成对服务提供方的接口绑定