feat(master): OpenFeign相关的学习

OpenFeign 的基本认知和使用
master
土豆兄弟 2 months ago
parent 6b69d06381
commit 3e350dc766

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

@ -20,6 +20,12 @@
</properties> </properties>
<dependencies> <dependencies>
<!-- ribbon -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<!-- openfeign -->
<dependency> <dependency>
<groupId>io.github.openfeign</groupId> <groupId>io.github.openfeign</groupId>
<artifactId>feign-micrometer</artifactId> <artifactId>feign-micrometer</artifactId>
@ -28,6 +34,19 @@
<groupId>org.springframework.cloud</groupId> <groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId> <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency> </dependency>
<!-- feign 替换 JDK 默认的 URLConnection 为 okhttp -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
<!-- 使用原生的 Feign Api 做的自定义配置, encoder 和 decoder -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-gson</artifactId>
<version>12.1</version>
</dependency>
<!-- springboot -->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-web</artifactId>
@ -36,6 +55,8 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId> <artifactId>spring-boot-starter-test</artifactId>
</dependency> </dependency>
<!-- tools -->
<dependency> <dependency>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>

@ -2,7 +2,10 @@ package org.example.controller;
import org.example.common.vo.JwtToken; import org.example.common.vo.JwtToken;
import org.example.common.vo.UsernameAndPassword; import org.example.common.vo.UsernameAndPassword;
import org.example.feign.UseFeignApi;
import org.example.service.AuthorityFeignClient;
import org.example.service.UseRestTemplateService; import org.example.service.UseRestTemplateService;
import org.example.service.UseRibbonService;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
@ -16,19 +19,19 @@ import org.springframework.web.bind.annotation.RestController;
public class CommunicationController { public class CommunicationController {
private final UseRestTemplateService restTemplateService; private final UseRestTemplateService restTemplateService;
// private final UseRibbonService ribbonService; private final UseRibbonService ribbonService;
// private final AuthorityFeignClient feignClient; private final AuthorityFeignClient feignClient;
// private final UseFeignApi useFeignApi; private final UseFeignApi useFeignApi;
public CommunicationController(UseRestTemplateService restTemplateService public CommunicationController(UseRestTemplateService restTemplateService,
// UseRibbonService ribbonService, UseRibbonService ribbonService,
// AuthorityFeignClient feignClient, AuthorityFeignClient feignClient,
// UseFeignApi useFeignApi UseFeignApi useFeignApi
) { ) {
this.restTemplateService = restTemplateService; this.restTemplateService = restTemplateService;
// this.ribbonService = ribbonService; this.ribbonService = ribbonService;
// this.feignClient = feignClient; this.feignClient = feignClient;
// this.useFeignApi = useFeignApi; this.useFeignApi = useFeignApi;
} }
@PostMapping("/rest-template") @PostMapping("/rest-template")
@ -44,7 +47,7 @@ public class CommunicationController {
usernameAndPassword); usernameAndPassword);
} }
/* @PostMapping("/ribbon") @PostMapping("/ribbon")
public JwtToken getTokenFromAuthorityServiceByRibbon( public JwtToken getTokenFromAuthorityServiceByRibbon(
@RequestBody UsernameAndPassword usernameAndPassword) { @RequestBody UsernameAndPassword usernameAndPassword) {
return ribbonService.getTokenFromAuthorityServiceByRibbon(usernameAndPassword); return ribbonService.getTokenFromAuthorityServiceByRibbon(usernameAndPassword);
@ -63,5 +66,5 @@ public class CommunicationController {
@PostMapping("/thinking-in-feign") @PostMapping("/thinking-in-feign")
public JwtToken thinkingInFeign(@RequestBody UsernameAndPassword usernameAndPassword) { public JwtToken thinkingInFeign(@RequestBody UsernameAndPassword usernameAndPassword) {
return useFeignApi.thinkingInFeign(usernameAndPassword); return useFeignApi.thinkingInFeign(usernameAndPassword);
}*/ }
} }

@ -0,0 +1,57 @@
package org.example.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;
/**
* <h1>OpenFeign </h1>
* */
@Configuration
public class FeignConfig {
/**
* <h2> OpenFeign </h2>
* */
@Bean
public Logger.Level feignLogger() {
return Logger.Level.FULL; // 需要注意, 日志级别需要修改成 debug, 因为 feign 基本都是 debug 级别的日志, info 的日志很少
}
/**
* <h2>OpenFeign </h2>
* period = 100 , ms
* maxPeriod = 1000 , ms
* maxAttempts = 5
* */
@Bean
public Retryer feignRetryer() {
return new Retryer.Default(
100,
SECONDS.toMillis(1),
5
);
}
public static final int CONNECT_TIMEOUT_MILLS = 5000;
public static final int READ_TIMEOUT_MILLS = 5000;
/**
* <h2></h2>
* */
@Bean
public Request.Options options() {
return new Request.Options(
CONNECT_TIMEOUT_MILLS, TimeUnit.MICROSECONDS,
READ_TIMEOUT_MILLS, TimeUnit.MILLISECONDS,
// 转发请求是否要进行限制
true
);
}
}

@ -0,0 +1,39 @@
package org.example.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;
/**
* <h1>OpenFeign 使 OkHttp </h1>
* Feign
* */
@Configuration
@ConditionalOnClass(Feign.class)
@AutoConfigureBefore(FeignAutoConfiguration.class)
public class FeignOkHttpConfig {
/**
* <h2> OkHttp, </h2>
* */
@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();
}
}

@ -0,0 +1,87 @@
package org.example.feign;
import feign.Feign;
import feign.Logger;
import feign.gson.GsonDecoder;
import feign.gson.GsonEncoder;
import lombok.extern.slf4j.Slf4j;
import org.example.common.vo.JwtToken;
import org.example.common.vo.UsernameAndPassword;
import org.example.service.AuthorityFeignClient;
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 org.springframework.util.CollectionUtils;
import java.util.List;
import java.lang.annotation.Annotation;
import java.util.Random;
/**
* <h1>使 Feign Api, OpenFeign = Feign + Ribbon</h1>
* */
@Slf4j
@Service
public class UseFeignApi {
private final DiscoveryClient discoveryClient;
public UseFeignApi(DiscoveryClient discoveryClient) {
this.discoveryClient = discoveryClient;
}
/**
* <h2>使 Feign api </h2>
* 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 去拿可用服务实例 OpenFeign 是使用 Ribbon 做这个事
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
// 默认的 encoder、decoder 不支持对象,不能实现编解码, 所以要自己进行定义
// 默认的 contract 默认的协议不支持 SpringCloud 对应的 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())
);
return feignClient.getTokenByFeign(usernameAndPassword);
}
}

@ -0,0 +1,22 @@
package org.example.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;
/**
* <h1>使 Ribbon , RestTemplate</h1>
* */
@Component
public class RibbonConfig {
/**
* <h2> RestTemplate</h2>
* */
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

@ -0,0 +1,31 @@
package org.example.service;
import org.example.common.vo.JwtToken;
import org.example.common.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;
/**
* <h1> Authority Feign Client </h1>
* */
@FeignClient(
// contextId 是对 FeignClient 的声明, 每一个进行的通信都要进行定义, value 表示需要进行通信的服务id是什么
contextId = "AuthorityFeignClient", value = "e-commerce-authority-center"
// fallback = AuthorityFeignClientFallback.class
// fallbackFactory = AuthorityFeignClientFallbackFactory.class
)
public interface AuthorityFeignClient {
/**
* <h2> OpenFeign 访 Authority Token</h2>
*
* value , ip + port
* consumes, produces: , , 使 Api ,
* */
@RequestMapping(value = "/ecommerce-authority-center/authority/token",
method = RequestMethod.POST,
consumes = "application/json", produces = "application/json")
JwtToken getTokenByFeign(@RequestBody UsernameAndPassword usernameAndPassword);
}

@ -0,0 +1,116 @@
package org.example.service;
import com.alibaba.fastjson.JSON;
import com.netflix.loadbalancer.*;
import com.netflix.loadbalancer.reactive.LoadBalancerCommand;
import lombok.extern.slf4j.Slf4j;
import org.example.common.constant.CommonConstant;
import org.example.common.vo.JwtToken;
import org.example.common.vo.UsernameAndPassword;
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;
/**
* <h1>使 Ribbon </h1>
* */
@Slf4j
@Service
public class UseRibbonService {
private final RestTemplate restTemplate;
private final DiscoveryClient discoveryClient;
public UseRibbonService(RestTemplate restTemplate,
DiscoveryClient discoveryClient) {
this.restTemplate = restTemplate;
this.discoveryClient = discoveryClient;
}
/**
* <h2> Ribbon Authority Token, [{使}]</h2>
* */
public JwtToken getTokenFromAuthorityServiceByRibbon(
UsernameAndPassword usernameAndPassword) {
// 注意到 url 中的 ip 和端口换成了服务名称
String requestUrl = String.format(
"http://%s/ecommerce-authority-center/authority/token",
CommonConstant.AUTHORITY_CENTER_SERVICE_ID
);
log.info("login request url and body: [{}], [{}]", 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
);
}
/**
* <h2>使 Ribbon Api, Ribbon : + [{使}]</h2>
* */
public JwtToken thinkingInRibbon(UsernameAndPassword usernameAndPassword) {
String urlFormat = "http://%s/ecommerce-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("found target instance: [{}] -> [{}]", 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("target request 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);
}
}

@ -0,0 +1,18 @@
# Feign 的相关配置
feign:
# feign 开启 gzip 压缩
compression:
request:
enabled: true # 对请求的数据开启压缩
mime-types: text/xml,application/xml,application/json # 压缩的数据类型
min-request-size: 1024 # 最小多大的请求数据开启压缩
response:
enabled: true # 对响应的数据开启压缩
# 禁用默认的 http, 启用 okhttp
httpclient:
enabled: false
okhttp:
enabled: true
# # OpenFeign 集成 Hystrix
# hystrix:
# enabled: true

@ -17,6 +17,25 @@ Content-Type: application/json
"password": "25d55ad283aa400af464c76d713c07ad" "password": "25d55ad283aa400af464c76d713c07ad"
} }
### 通过 Ribbon 去获取 Token
POST http://127.0.0.1:8000/ecommerce-nacos-client/communication/ribbon
Content-Type: application/json
{
"username": "Qinyi@imooc.com",
"password": "25d55ad283aa400af464c76d713c07ad"
}
### 通过原生 Ribbon Api 去获取 Token
POST http://127.0.0.1:8000/ecommerce-nacos-client/communication/thinking-in-ribbon
Content-Type: application/json
{
"username": "Qinyi@imooc.com",
"password": "25d55ad283aa400af464c76d713c07ad"
}
### 通过 OpenFeign 获取 Token ### 通过 OpenFeign 获取 Token
POST http://127.0.0.1:8000/ecommerce-nacos-client/communication/token-by-feign POST http://127.0.0.1:8000/ecommerce-nacos-client/communication/token-by-feign
Content-Type: application/json Content-Type: application/json

@ -36,14 +36,49 @@
- 通过注册中心(推荐Nacos)获取服务地址,可以实现负载均衡的效果 - 通过注册中心(推荐Nacos)获取服务地址,可以实现负载均衡的效果
--- ---
## 3. Ribbon 实现微服务通信及其原理解读
- 如何使用
- 引入依赖: spring-cloud-starter-netflix-ribbon
- 增强 RestTemplate @LoadBalanced 注解.使其具备负载均衡的能力
- ps: 学习源码最好的方式就是对部分看懂的逻辑进行仿写
---
## 4. OpenFeign 的简单应用及配置
- OpenFeign 是实际企业中使用更为常见的一种使用
- 如何使用
- 引入依赖 spring-cloud-starter-openfeign
- 添加 @EnableFeignClients 注解, 启用 open-feign
- SpringCloud OpenFeign 最常用的配置
- OpenFeign 开启 gzip 压缩
- 统一 OpenFeign 使用配置: 日志、重试、请求连接和响应时间限制
- 使用 okhttp 替换 httpclient(别忘了引入 okhttp 依赖)
- feign-okhttp
- 补充: okhttp 和 httpclient 的区别和优缺点
- ps: 写框架或者工程加载时候的日志一般都用 debug 级别, 别总是打 info 级别的日志
---
- OkHttp和HttpClient都是Java语言中常用的HTTP客户端库用于发送HTTP请求并处理响应。它们之间的区别如下
- OkHttp是由Square公司开发的而HttpClient是Apache软件基金会开发的。
- OkHttp的性能更快更高效。因为它使用了连接池、请求压缩、缓存等技术而HttpClient则需要手动进行配置和优化才能达到类似的效果。
- OkHttp支持SPDY升级版的HTTP/1.1协议和HTTP/2而HttpClient只支持HTTP/1.1。
- OkHttp是基于异步请求的而HttpClient只能使用同步请求。
- OkHttp的代码量更少更加简洁易懂而HttpClient的API更加复杂使用起来需要更多的代码。
- OkHttp的优点
- 高效,支持连接池、请求压缩、缓存等技术,性能更快。
- 支持SPDY和HTTP/2协议可以更好地适应现代网络环境。
- 简单易用代码量少API清晰明了。
- HttpClient的优点
- 稳定可靠,经过多年的发展和优化,被广泛使用和验证。
- 功能强大支持重试、重定向、认证、代理、Cookie等功能使用起来非常灵活。
- 综上所述,**OkHttp适用于对性能和效率要求比较高的场景而HttpClient适用于功能比较复杂的场景。**
---
## 5. 通过 Feign 的原生 API 解析其实现原理
- ![Feign实现流程图.png](pic/Feign实现流程图.png)
## OpenFeign 核心源码解析 ## OpenFeign 核心源码解析

Loading…
Cancel
Save