集成Feign简化调用方式

api-gateway-netflix-zuul
DerekYRC 3 years ago
commit 147b19ce7f

@ -16,7 +16,7 @@
* [服务注册](https://github.com/DerekYRC/mini-spring-cloud/blob/main/changelog.md#服务注册)
* [服务发现](https://github.com/DerekYRC/mini-spring-cloud/blob/main/changelog.md#服务发现)
* [负载均衡](https://github.com/DerekYRC/mini-spring-cloud/blob/main/changelog.md#集成ribbon实现客户端负载均衡)
* [集成Feign简化调用]()
* [集成Feign简化调用方式](https://github.com/DerekYRC/mini-spring-cloud/blob/main/changelog.md#集成Feign简化调用方式)
* [流量控制]()
* [熔断降级]()
* [API 网关]()

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

@ -5,18 +5,17 @@
- spring推荐本人写的简化版的spring框架 [**mini-spring**](https://github.com/DerekYRC/mini-spring/blob/main/README_CN.md) 。熟悉spring源码阅读springboot源码会非常轻松。
- spring boot重点掌握1、启动流程 2、**自动装配的原理! 自动装配的原理!! 自动装配的原理!!!** 推荐文章:
- [《Spring Boot精髓启动流程源码分析》](https://www.cnblogs.com/java-chen-hao/p/11829344.html)
- [《细说SpringBoot的自动装配原理》](https://blog.csdn.net/qq_38526573/article/details/107084943)
- [《Spring Boot 自动装配原理》](https://www.cnblogs.com/javaguide/p/springboot-auto-config.html)
- spring cloud先学会使用再研究源码切勿本末倒置。推荐[《精尽Spring Cloud学习指南》](http://svip.iocoder.cn/Spring-Cloud/tutorials/)
- [《SpringBoot自动装配原理这一篇就够了!》](https://mp.weixin.qq.com/s/f6oED1hbiWat_0HOwxgfnA)
- spring cloud先学会使用再研究源码切勿本末倒置。推荐[《精尽Spring Cloud学习指南》](http://svip.iocoder.cn/Spring-Cloud/tutorials/) 。
关于spring cloud。spring cloud是构建通用模式的分布式系统的工具集通过[**spring-cloud-commons**](https://github.com/spring-cloud/spring-cloud-commons) 定义了统一的抽象API相当于定义了一套协议标准具体的实现需要符合这套协议标准。spring cloud官方整合第三方组件Eureka、Ribbon、Hystrix等实现了spring-cloud-netflix阿里巴巴结合自身的Nacos、Sentinel等实现了spring-cloud-alibaba。本项目基于spring-cloud-commons的协议标准自主开发或整合第三方组件提供具体的实现。
写作本项目的目的之一是降低阅读原始spring cloud源码的难度。希望掌握本项目讲解的内容之后再阅读原始spring-cloud的源码能起到事半功倍的效果所以本项目的功能实现逻辑及原理和官方保持一致但追求代码最大精简化可以理解为一个源码导读的项目。
写作本项目的目的之一是降低阅读原始spring cloud源码的难度。希望掌握本项目讲解的内容之后再阅读原始spring-cloud的源码能起到事半功倍的效果所以本项目的功能实现逻辑及原理和官方保持一致但追求代码最大精简化**本项目可以理解为一个源码导读的项目**
技术能力有限且文采欠佳,大家可以在此[**issue**](https://github.com/DerekYRC/mini-spring-cloud/issues/1) 留言提问题和发表建议也欢迎Pull Request完善此项目。
# [服务注册](#服务注册)
> 分支: service-registry
> 代码分支: service-registry
为了演示,写一个非常简单的单机版的服务注册和发现中心,命名图图
```java
@ -338,7 +337,7 @@ server:
```
# [服务发现](#服务发现)
> 分支: service-discovery
> 代码分支: service-discovery
spring-cloud-commons定义的服务发现接口```org.springframework.cloud.client.discovery.DiscoveryClient```:
```java
@ -476,16 +475,16 @@ spring:
Port of the service provider: 19922
```
# [集成ribbon实现客户端负载均衡](#集成ribbon实现客户端负载均衡)
> 分支: load-balancer
> 代码分支: load-balancer
## 关于ribbon
> (翻译自官方文档)ribbon是一个提供如下功能的依赖包:
> - 负载均衡
> - 容错机制
> - 支持多种协议(HTTP, TCP, UDP),支持异步和响应式的调用方式
> - 缓存和批处理
(翻译自官方文档)ribbon是一个提供如下功能的依赖包:
- 负载均衡
- 容错机制
- 支持多种协议(HTTP, TCP, UDP),支持异步和响应式的调用方式
- 缓存和批处理
#### ribbon核心API
@ -759,11 +758,11 @@ public class TutuRibbonClientConfiguration {
}
```
每一个Provider服务集群应用名称即spring.application.name相同的所有应用服务提供者对应一套ribbon核心API。SpringClientFactory继承自NamedContextFactory为每一套ribbon核心API创建一个子spring应用上下文ApplicationContext来隔离不同服务的ribbon核心API配置可以定制化不同服务的负载均衡规则扩展篇实现
每一个Provider服务集群应用名称即spring.application.name相同的所有应用服务提供者对应一套ribbon核心API。**SpringClientFactory继承自NamedContextFactory为每一套ribbon核心API创建一个子spring应用上下文ApplicationContext**来隔离不同服务的ribbon核心API配置可以定制化不同服务的负载均衡规则扩展篇实现
SpringClientFactory的构造函数参数RibbonClientConfiguration配置ribbon默认的核心API。
SpringClientFactory的构造函数参数**RibbonClientConfiguration**配置ribbon默认的核心API。
修饰RibbonTutuAutoConfiguration配置类的注解RibbonClients引入了自动配置类RibbonClientConfigurationRegistrar将RibbonClients注解指定的defaultConfiguration属性的值即TutuRibbonClientConfiguration配置类包装为RibbonClientSpecification。RibbonClientSpecification作为SpringClientFactory的属性用来覆盖RibbonClientConfiguration配置类指定的默认的核心API比如TutuRibbonClientConfiguration配置类使用TutuServerList替换RibbonClientConfiguration配置类中指定的ConfigurationBasedServerList。
修饰RibbonTutuAutoConfiguration配置类的注解RibbonClients引入了**自动配置类RibbonClientConfigurationRegistrar**将RibbonClients注解指定的defaultConfiguration属性的值即TutuRibbonClientConfiguration配置类**包装为RibbonClientSpecification****RibbonClientSpecification作为SpringClientFactory的属性用来覆盖RibbonClientConfiguration配置类指定的默认的核心API**比如TutuRibbonClientConfiguration配置类使用TutuServerList替换RibbonClientConfiguration配置类中指定的ConfigurationBasedServerList。
可能表述得不清楚为了充分理解子spring容器的创建逻辑可以在下面的测试环节debug如下几个方法:
@ -912,14 +911,14 @@ public class RibbonLoadBalancerClient implements LoadBalancerClient {
}
```
- reconstructURI方法重建请求URI将服务名称替换为服务实例的IP:端口例如http://provider-application/echo被重建为http://192.168.100.1:8888/echo
- reconstructURI方法重建请求URI将服务名称替换为服务实例的IP:端口,例如```http://provider-application/echo``` 被重建为```http://192.168.100.1:8888/echo```
- execute方法处理http请求
有了RibbonLoadBalancerClient的reconstructURI和execute方法将所有http请求委托给RibbonLoadBalancerClient即可。其实spring-cloud-commons已经帮我们配置好拦截RestTemplate的http请求委托给RibbonLoadBalancerClient的拦截器LoadBalancerInterceptor配置类如下:
有了RibbonLoadBalancerClient的reconstructURI和execute方法将所有http请求委托给RibbonLoadBalancerClient即可。其实spring-cloud-commons已经帮我们配置好拦截RestTemplate的http请求委托给RibbonLoadBalancerClient的拦截器LoadBalancerInterceptor配置类(有删减)如下:
![](./assets/load-balancer-LoadBalancerAutoConfiguration.png)
LoadBalancerAutoConfiguration配置类为每一个被LoadBalanced注解修饰的RestTemplate增加LoadBalancerInterceptor拦截器。
LoadBalancerAutoConfiguration配置类**为每一个被LoadBalanced注解修饰的RestTemplate增加LoadBalancerInterceptor拦截器。**
![](./assets/load-balancer-LoadBalancerInterceptor.png)
@ -972,59 +971,345 @@ public class ConsumerApplication {
访问```http://localhost:8080/foo```
# [集成Feign简化调用方式](#集成Feign简化调用方式)
> 代码分支: open-feign
## 关于feign
[Open Feign](https://github.com/OpenFeign/feign) 是一个简化http调用方式的Java客户端。使用示例:
```java
interface HelloService {
@RequestLine("GET /hello")
String hello();
}
@Test
public void testOpenFeign() {
HelloService helloService = Feign.builder()
.target(HelloService.class, "http://localhost:8080");
String response = helloService.hello();
}
```
Spring Cloud基于Open Feign开发了[Spring Cloud OpenFeign](https://github.com/spring-cloud/spring-cloud-openfeign) 得以支持Spring Mvc的注解通过实现了feign的Contract接口的实现类SpringMvcContract使用示例:
```java
interface WorldService {
@GetMapping("/world")
String world();
}
@Test
public void testSpringCloudOpenFeign() {
WorldService worldService = Feign.builder()
.contract(new SpringMvcContract())
.target(WorldService.class, "http://localhost:8080");
String response = worldService.world();
}
```
可以dubug上面两个示例代码放在测试类FeignTest中重点关注Contract接口对注解的解析
#### Open Feign工作流程:
![](./assets/feign工作流程.png)
#### Open Feign核心API:
一、Contract接口
负责解析Feign客户端接口的类注解、方法注解和参数。
实现类```feign.Contract.Default```支持Open Feign的注解比如上面第一个示例中的RequestLine注解。
Spring Cloud OpenFeign开发的实现类```SpringMvcContract```支持Spring MVC的注解如GetMapping、PostMapping、RequestMapping。
二、Encoder接口
编码器,将请求对象编码为请求体
三、Decoder接口
解码器,将响应体解码为对象
四、RequestInterceptor拦截器接口
对请求进行拦截处理
五、Client接口
提交http请求的接口
## 功能实现
**@EnableFeignClients注解**开启集成Feign客户端该注解Import配置类FeignClientsRegistrar:
```java
/**
* 启用Feign
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
}
```
配置类FeignClientsRegistrar扫描每个被FeignClient注解修饰的接口基于JDK动态代理生成对象注册到bean容器:
```java
/**
* 往bean容器中注册Feign客户端
*/
public class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar {
/**
* 往bean容器中注册Feign客户端
*/
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
//为FeignClient注解修饰的接口生成代理bean即Feign客户端并注册到bean容器
String packageName = ClassUtils.getPackageName(importingClassMetadata.getClassName());
//扫描所有被FeignClient注解修饰的接口
Set<Class<?>> classes = ClassUtil.scanPackageByAnnotation(packageName, FeignClient.class);
for (Class<?> clazz : classes) {
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
//使用FeignClientFactoryBean生成Feign客户端
beanDefinition.setBeanClass(FeignClientFactoryBean.class);
String clientName = clazz.getAnnotation(FeignClient.class).value();
beanDefinition.getPropertyValues().addPropertyValue("contextId", clientName);
beanDefinition.getPropertyValues().addPropertyValue("type", clazz);
//将Feign客户端注册进bean容器
String beanName = clazz.getName();
registry.registerBeanDefinition(beanName, beanDefinition);
}
}
}
```
注意BeanDefinition指定的beanClass为FeignClientFactoryBean它是FactoryBean的实现类bean容器取其getObject方法返回值作为bean:
```java
/**
* 生成Feign客户端的FactoryBean
*/
public class FeignClientFactoryBean implements FactoryBean<Object>, ApplicationContextAware {
private String contextId;
private Class<?> type;
private ApplicationContext applicationContext;
@Override
public Object getObject() throws Exception {
FeignContext feignContext = applicationContext.getBean(FeignContext.class);
Encoder encoder = feignContext.getInstance(contextId, Encoder.class);
Decoder decoder = feignContext.getInstance(contextId, Decoder.class);
Contract contract = feignContext.getInstance(contextId, Contract.class);
Client client = feignContext.getInstance(contextId, Client.class);
return Feign.builder()
.encoder(encoder)
.decoder(decoder)
.contract(contract)
.client(client)
.target(new HardCodedTarget<>(type, contextId, "http://" + contextId));
}
//other methods
}
```
跟ribbon一样每一个Provider服务集群应用名称即spring.application.name相同的所有应用服务提供者对应一套feign核心API。**FeignContext继承自NamedContextFactory为每一套feign核心API创建一个子spring应用上下文ApplicationContext**来隔离不同服务的feign核心API配置(扩展篇实现)。
FeignContext:
```java
/**
* 为每个feign客户端创建一个应用上下文(ApplicationContext)隔离每个feign客户端的配置
*/
public class FeignContext extends NamedContextFactory<FeignClientSpecification> {
public FeignContext() {
super(FeignClientsConfiguration.class, "feign", "feign.client.name");
}
}
```
FeignClientsConfiguration配置类配置feign的核心API
```java
/**
* 配置feign的核心API
*/
@Configuration
public class FeignClientsConfiguration {
@Bean
@ConditionalOnMissingBean
public Encoder encoder() {
return new Encoder.Default();
}
@Bean
@ConditionalOnMissingBean
public Decoder decoder() {
return new Decoder.Default();
}
@Bean
@ConditionalOnMissingBean
public Contract contract() {
return new SpringMvcContract();
}
@Bean
@ConditionalOnMissingBean
public Client client(LoadBalancerClient loadBalancerClient) {
return new LoadBalancerFeignClient(loadBalancerClient, new Client.Default(null, null));
}
}
```
SpringMvcContract简单实现支持Spring MVC的PostMapping注解:
```java
/**
* feign支持Spring MVC的注解
*/
public class SpringMvcContract extends Contract.BaseContract {
@Override
protected void processAnnotationOnClass(MethodMetadata data, Class<?> clz) {
//TODO 解析接口注解
}
@Override
protected void processAnnotationOnMethod(MethodMetadata data, Annotation annotation, Method method) {
//解析方法注解
//解析PostMapping注解
if (annotation instanceof PostMapping) {
PostMapping postMapping = (PostMapping) annotation;
data.template().method(Request.HttpMethod.POST);
String path = postMapping.value()[0];
if (!path.startsWith("/") && !data.template().path().endsWith("/")) {
path = "/" + path;
}
data.template().uri(path, true);
}
//TODO 解析其他注解
}
@Override
protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex) {
//TODO 解析参数
return true;
}
}
```
LoadBalancerFeignClient组合ribbon的客户端负责均衡能力选择服务示例然后发送http请求:
```java
/**
* 具备负载均衡能力的feign client
*/
public class LoadBalancerFeignClient implements Client {
private LoadBalancerClient loadBalancerClient;
private Client delegate;
public LoadBalancerFeignClient(LoadBalancerClient loadBalancerClient, Client delegate) {
this.loadBalancerClient = loadBalancerClient;
this.delegate = delegate;
}
@SuppressWarnings("deprecation")
@Override
public Response execute(Request request, Request.Options options) throws IOException {
try {
//客户端负载均衡
URI original = URI.create(request.url());
String serviceId = original.getHost();
//选择服务实例
ServiceInstance serviceInstance = loadBalancerClient.choose(serviceId);
//重建请求URI
URI uri = loadBalancerClient.reconstructURI(serviceInstance, original);
Request newRequest = Request.create(request.httpMethod(), uri.toASCIIString(), new HashMap<>(),
request.body(), StandardCharsets.UTF_8);
return delegate.execute(newRequest, options);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
```
自动装配:
```java
@Configuration
public class FeignAutoConfiguration {
@Bean
public FeignContext feignContext() {
return new FeignContext();
}
}
```
spring.factories:
```yaml
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.github.cloud.openfeign.FeignAutoConfiguration
```
测试:
消费者代码,使用@EnableFeignClients注解启用Feign:
```java
@EnableFeignClients
@SpringBootApplication
public class ConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
@RestController
static class HelloController {
@Autowired
private EchoService echoService;
@GetMapping("/bar")
public String bar() {
return echoService.echo();
}
}
}
```
Feign客户端:
```java
@FeignClient("provider-application")
public interface EchoService {
@PostMapping("echo")
String echo();
}
```
访问```http://localhost:8080/bar```

@ -18,6 +18,12 @@
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github</groupId>
<artifactId>mini-spring-cloud-tutu-discovery</artifactId>
@ -27,6 +33,18 @@
<groupId>com.github</groupId>
<artifactId>mini-spring-cloud-load-balancer</artifactId>
</dependency>
<dependency>
<groupId>com.github</groupId>
<artifactId>mini-spring-cloud-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
<version>1.4.7.RELEASE</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>

@ -1,5 +1,6 @@
package com.github.cloud.examples;
import com.github.cloud.openfeign.EnableFeignClients;
import com.github.cloud.tutu.discovery.TutuDiscoveryClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
@ -20,6 +21,7 @@ import java.util.List;
* @author derek()
* @date 2022/3/20
*/
@EnableFeignClients
@SpringBootApplication
public class ConsumerApplication {
@ -54,6 +56,9 @@ public class ConsumerApplication {
@Autowired
private RestTemplate loadBalancedRestTemplate;
@Autowired
private EchoService echoService;
private RestTemplate restTemplate = new RestTemplate();
@GetMapping("/hello")
@ -85,6 +90,11 @@ public class ConsumerApplication {
public String foo() {
return loadBalancedRestTemplate.postForObject("http://provider-application/echo", null, String.class);
}
@GetMapping("/bar")
public String bar() {
return echoService.echo();
}
}
}

@ -0,0 +1,15 @@
package com.github.cloud.examples;
import com.github.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
/**
* @author derek()
* @date 2022/4/9
*/
@FeignClient("provider-application")
public interface EchoService {
@PostMapping("echo")
String echo();
}

@ -0,0 +1,52 @@
package com.github.cloud.examples;
import feign.Feign;
import feign.RequestLine;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.openfeign.support.SpringMvcContract;
import org.springframework.web.bind.annotation.GetMapping;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author derek()
* @date 2022/3/27
*/
public class FeignTest {
private static Logger logger = LoggerFactory.getLogger(FeignTest.class);
interface HelloService {
@RequestLine("GET /hello")
String hello();
}
@Test
public void testOpenFeign() {
HelloService helloService = Feign.builder()
.target(HelloService.class, "http://localhost:8080");
String response = helloService.hello();
logger.info("response: {}", response);
boolean succ = response.startsWith("Port of the service provider");
assertThat(succ).isTrue();
}
interface WorldService {
@GetMapping("/world")
String world();
}
@Test
public void testSpringCloudOpenFeign() {
WorldService worldService = Feign.builder()
.contract(new SpringMvcContract())
.target(WorldService.class, "http://localhost:8080");
String response = worldService.world();
logger.info("response: {}", response);
boolean succ = response.startsWith("Port of the service provider");
assertThat(succ).isTrue();
}
}

@ -54,7 +54,6 @@
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>test</scope>
</dependency>
<dependency>

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>mini-spring-cloud</artifactId>
<groupId>com.github</groupId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>mini-spring-cloud-openfeign</artifactId>
<dependencies>
<dependency>
<groupId>com.github</groupId>
<artifactId>mini-spring-cloud-load-balancer</artifactId>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-context</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
</dependencies>
</project>

@ -0,0 +1,18 @@
package com.github.cloud.openfeign;
import org.springframework.context.annotation.Import;
import java.lang.annotation.*;
/**
* Feign
*
* @author derek()
* @date 2022/4/7
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
}

@ -0,0 +1,17 @@
package com.github.cloud.openfeign;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author derek()
* @date 2022/4/9
*/
@Configuration
public class FeignAutoConfiguration {
@Bean
public FeignContext feignContext() {
return new FeignContext();
}
}

@ -0,0 +1,22 @@
package com.github.cloud.openfeign;
import java.lang.annotation.*;
/**
* Feign
*
* @author derek()
* @date 2022/4/7
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FeignClient {
/**
*
*
* @return
*/
String value();
}

@ -0,0 +1,69 @@
package com.github.cloud.openfeign;
import feign.Client;
import feign.Contract;
import feign.Feign;
import feign.Target.HardCodedTarget;
import feign.codec.Decoder;
import feign.codec.Encoder;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
/**
* FeignFactoryBean
*
* @author derek()
* @date 2022/4/7
*/
public class FeignClientFactoryBean implements FactoryBean<Object>, ApplicationContextAware {
private String contextId;
private Class<?> type;
private ApplicationContext applicationContext;
@Override
public Object getObject() throws Exception {
FeignContext feignContext = applicationContext.getBean(FeignContext.class);
Encoder encoder = feignContext.getInstance(contextId, Encoder.class);
Decoder decoder = feignContext.getInstance(contextId, Decoder.class);
Contract contract = feignContext.getInstance(contextId, Contract.class);
Client client = feignContext.getInstance(contextId, Client.class);
return Feign.builder()
.encoder(encoder)
.decoder(decoder)
.contract(contract)
.client(client)
.target(new HardCodedTarget<>(type, contextId, "http://" + contextId));
}
@Override
public Class<?> getObjectType() {
return this.type;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
public String getContextId() {
return contextId;
}
public void setContextId(String contextId) {
this.contextId = contextId;
}
public Class<?> getType() {
return type;
}
public void setType(Class<?> type) {
this.type = type;
}
}

@ -0,0 +1,55 @@
package com.github.cloud.openfeign;
import org.springframework.cloud.context.named.NamedContextFactory;
import java.util.Arrays;
import java.util.Objects;
/**
* @author derek()
* @date 2022/4/9
*/
public class FeignClientSpecification implements NamedContextFactory.Specification {
private String name;
private Class<?>[] configuration;
public FeignClientSpecification(String name, Class<?>[] configuration) {
this.name = name;
this.configuration = configuration;
}
@Override
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public Class<?>[] getConfiguration() {
return configuration;
}
public void setConfiguration(Class<?>[] configuration) {
this.configuration = configuration;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
FeignClientSpecification that = (FeignClientSpecification) o;
return name.equals(that.name) && Arrays.equals(configuration, that.configuration);
}
@Override
public int hashCode() {
int result = Objects.hash(name);
result = 31 * result + Arrays.hashCode(configuration);
return result;
}
}

@ -0,0 +1,46 @@
package com.github.cloud.openfeign;
import com.github.cloud.openfeign.ribbon.LoadBalancerFeignClient;
import com.github.cloud.openfeign.support.SpringMvcContract;
import feign.Client;
import feign.Contract;
import feign.codec.Decoder;
import feign.codec.Encoder;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* feignAPI
*
* @author derek()
* @date 2022/4/9
*/
@Configuration
public class FeignClientsConfiguration {
@Bean
@ConditionalOnMissingBean
public Encoder encoder() {
return new Encoder.Default();
}
@Bean
@ConditionalOnMissingBean
public Decoder decoder() {
return new Decoder.Default();
}
@Bean
@ConditionalOnMissingBean
public Contract contract() {
return new SpringMvcContract();
}
@Bean
@ConditionalOnMissingBean
public Client client(LoadBalancerClient loadBalancerClient) {
return new LoadBalancerFeignClient(loadBalancerClient, new Client.Default(null, null));
}
}

@ -0,0 +1,45 @@
package com.github.cloud.openfeign;
import cn.hutool.core.util.ClassUtil;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.util.ClassUtils;
import java.util.Set;
/**
* beanFeign
*
* @author derek()
* @date 2022/4/7
*/
public class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar {
/**
* beanFeign
*
* @param importingClassMetadata
* @param registry
*/
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
//为FeignClient注解修饰的接口生成代理bean即Feign客户端并注册到bean容器
String packageName = ClassUtils.getPackageName(importingClassMetadata.getClassName());
//扫描所有被FeignClient注解修饰的接口
Set<Class<?>> classes = ClassUtil.scanPackageByAnnotation(packageName, FeignClient.class);
for (Class<?> clazz : classes) {
GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
//使用FeignClientFactoryBean生成Feign客户端
beanDefinition.setBeanClass(FeignClientFactoryBean.class);
String clientName = clazz.getAnnotation(FeignClient.class).value();
beanDefinition.getPropertyValues().addPropertyValue("contextId", clientName);
beanDefinition.getPropertyValues().addPropertyValue("type", clazz);
//将Feign客户端注册进bean容器
String beanName = clazz.getName();
registry.registerBeanDefinition(beanName, beanDefinition);
}
}
}

@ -0,0 +1,16 @@
package com.github.cloud.openfeign;
import org.springframework.cloud.context.named.NamedContextFactory;
/**
* feign(ApplicationContext)feign
*
* @author derek()
* @date 2022/4/9
*/
public class FeignContext extends NamedContextFactory<FeignClientSpecification> {
public FeignContext() {
super(FeignClientsConfiguration.class, "feign", "feign.client.name");
}
}

@ -0,0 +1,50 @@
package com.github.cloud.openfeign.ribbon;
import feign.Client;
import feign.Request;
import feign.Response;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
/**
* feign client
*
* @author derek()
* @date 2022/4/9
*/
public class LoadBalancerFeignClient implements Client {
private LoadBalancerClient loadBalancerClient;
private Client delegate;
public LoadBalancerFeignClient(LoadBalancerClient loadBalancerClient, Client delegate) {
this.loadBalancerClient = loadBalancerClient;
this.delegate = delegate;
}
@SuppressWarnings("deprecation")
@Override
public Response execute(Request request, Request.Options options) throws IOException {
try {
//客户端负载均衡
URI original = URI.create(request.url());
String serviceId = original.getHost();
//选择服务实例
ServiceInstance serviceInstance = loadBalancerClient.choose(serviceId);
//重建请求URI
URI uri = loadBalancerClient.reconstructURI(serviceInstance, original);
Request newRequest = Request.create(request.httpMethod(), uri.toASCIIString(), new HashMap<>(),
request.body(), StandardCharsets.UTF_8);
return delegate.execute(newRequest, options);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

@ -0,0 +1,46 @@
package com.github.cloud.openfeign.support;
import feign.Contract;
import feign.MethodMetadata;
import feign.Request;
import org.springframework.web.bind.annotation.PostMapping;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
/**
* feignSpring MVC
*
* @author derek()
* @date 2022/4/9
*/
public class SpringMvcContract extends Contract.BaseContract {
@Override
protected void processAnnotationOnClass(MethodMetadata data, Class<?> clz) {
//TODO 解析接口注解
}
@Override
protected void processAnnotationOnMethod(MethodMetadata data, Annotation annotation, Method method) {
//解析方法注解
//解析PostMapping注解
if (annotation instanceof PostMapping) {
PostMapping postMapping = (PostMapping) annotation;
data.template().method(Request.HttpMethod.POST);
String path = postMapping.value()[0];
if (!path.startsWith("/") && !data.template().path().endsWith("/")) {
path = "/" + path;
}
data.template().uri(path, true);
}
//TODO 解析其他注解
}
@Override
protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex) {
//TODO 解析参数
return true;
}
}

@ -0,0 +1,2 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.github.cloud.openfeign.FeignAutoConfiguration

@ -22,6 +22,7 @@
<module>mini-spring-cloud-examples/mini-spring-cloud-provider-example</module>
<module>mini-spring-cloud-examples/mini-spring-cloud-consumer-examples</module>
<module>mini-spring-cloud-load-balancer</module>
<module>mini-spring-cloud-openfeign</module>
</modules>
<properties>
@ -29,6 +30,7 @@
<fastjson.version>1.2.79</fastjson.version>
<hutool.version>5.7.21</hutool.version>
<ribbon.version>2.3.0</ribbon.version>
<feign.version>11.8</feign.version>
</properties>
<dependencyManagement>
@ -61,6 +63,12 @@
<version>1.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.github</groupId>
<artifactId>mini-spring-cloud-openfeign</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
@ -90,6 +98,12 @@
<artifactId>ribbon-core</artifactId>
<version>${ribbon.version}</version>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-core</artifactId>
<version>${feign.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

Loading…
Cancel
Save