Spring Cloud学习|第三篇:声明式调用-Feign

1.Feign简介

​ Feign是一种声明式的web service客户端,使用Feign只需定义一个接口并加上相应注解即可使用。通过Feign只需要简单的几行配置,即可实现调用远程服务如调用本地服务般简单

2.入门案例

服务名 服务端口 作用
192.168.1.100 8500 注册中心
eureka-client 8762,8763 服务提供者
eureka-feign-client 8765 feign客户端
  • 新建服务eureka-feign-client

  • 引入依赖

    从此章开始,后续章节均使用consul作为注册中心

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-all</artifactId>
        </dependency>
    </dependencies>
    
  • 配置bootstrap.yml

    server:
    	port: 8765
    spring:
      application:
        # 服务注册名
        name: eureka-feign-service
      cloud:
        consul:
          config:
            # 设置配置文件夹
            prefix: config
            # 设置配置文件夹位置
            default-context: application
            # 设置配置文件名称
            data-key: data
            # 设置配置文件显示样式,有properties和yaml两种格式
            format: yaml
            # 开启监听,动态刷新配置,默认每秒触发一次
            watch:
              enabled: true
              # 修改动态刷新配置时间
            #          delay: 2000
            # 应用和profile之间的分割符,与spring.profiles.active配合使用
            #        profile-separator: ,
            enabled: true
          # consul服务器地址
          host: 192.168.1.100
          # 默认端口号
          port: 8500
          # 是否注册,默认为true
          discovery:
            register: true
            # 健康检查时间
            health-check-timeout: 2m
            enabled: true
            heartbeat:
              enabled: true
            # 优先ip注册
            prefer-ip-address: true
            instance-id: ${spring.application.name}:${spring.cloud.client.hostname}:${spring.application.instance_id:${server.port}}
    
  • 书写启动类

    注意启动类中需引入注解@EnableDiscoveryClient@EnableFeignClients

    @EnableDiscoveryClient:服务注册至注册中心可被发现

    @EnableFeignClients:开启feign

    @SpringBootApplication
    @EnableFeignClients
    @EnableDiscoveryClient
    public class EurekaFeignclientApplication {
    
      public static void main(String[] args) {
        SpringApplication.run(EurekaFeignclientApplication.class, args);
      }
    }
    
  • 书写一个FeignClient,开始远程调用

    @FeignClient(name = "eureka-client")
    public interface EurekaFeignclientService {
    
      @GetMapping("/hi")
      String sayHello(@RequestParam String name);
    
    }
    
  • 访问http://localhost:8765/hi出现如下结果

    hi 张三, i am from port:8763

    hi 张三, i am from port:8762

3.Feign工作原理简单分析

​ Feign通过包扫描FeignClient的Bean,该源码在FeignClientsRegistar类中。程序启动时,会检查是否有@EnableFeignClients注解,如果有该注解,则开启包扫描,扫描被@FeignClient注解的接口

  • 3.1 FeignClientsRegistrar分析
class FeignClientsRegistrar
    implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {}
  • 3.2 查看registerBeanDefinitions()方法可知注册bean时需处理下述两个方法
@Override
	public void registerBeanDefinitions(AnnotationMetadata metadata,
			BeanDefinitionRegistry registry) {
		registerDefaultConfiguration(metadata, registry);
		registerFeignClients(metadata, registry);
	}

  • 3.3 继续分析其中的两个方法

registerDefaultConfiguration:扫描是否开启@EnableFeignClients,如果开启,通过调用registerClientConfiguration方法,将该类加载进spring容器中

private void registerDefaultConfiguration(AnnotationMetadata metadata,
                                          BeanDefinitionRegistry registry) {
    Map<String, Object> defaultAttrs = metadata
        .getAnnotationAttributes(EnableFeignClients.class.getName(), true);

    if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) {
        String name;
        if (metadata.hasEnclosingClass()) {
            name = "default." + metadata.getEnclosingClassName();
        }
        else {
            name = "default." + metadata.getClassName();
        }
        registerClientConfiguration(registry, name,
                                    defaultAttrs.get("defaultConfiguration"));
    }
}

registerFeignClients:扫描项目中配置了@FeignClient注解的对象,如果存在,则将该对象加载进spring容器中

  • 3.4 ReflectiveFeign分析

    注入BeanDefinition之后,通过JDK代理,当调用Feign Client接口里边的方法时,该方法被拦截,从而根据参数生成RequestTemplate对象,最终实现请求下游服务

  • 3.5 负载均衡分析

    FeignRibbonClientAutoConfiguration中注入了HttpClientFeignLoadBalancedConfiguration

    @ConditionalOnClass({ ILoadBalancer.class, Feign.class })
    @Configuration
    @AutoConfigureBefore(FeignAutoConfiguration.class)
    @EnableConfigurationProperties({ FeignHttpClientProperties.class })
    @Import({ HttpClientFeignLoadBalancedConfiguration.class,
             OkHttpFeignLoadBalancedConfiguration.class,
             DefaultFeignLoadBalancedConfiguration.class })
    public class FeignRibbonClientAutoConfiguration {}
    

    查看HttpClientFeignLoadBalancedConfiguration源码中通过feignClient加载Client对象,最终将给LoadBalancerFeignClient对象进行初始化

    @Bean
    @ConditionalOnMissingBean(Client.class)
    public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
                              SpringClientFactory clientFactory, HttpClient httpClient) {
        ApacheHttpClient delegate = new ApacheHttpClient(httpClient);
        return new LoadBalancerFeignClient(delegate, cachingFactory, clientFactory);
    }
    

    查看LoadBalancerFeignClient对象中的execute(),该方法主要解析url,服务名,最终将请求信息交由executeWithLoadBalancer()处理

    @Override
    public Response execute(Request request, Request.Options options) throws IOException {
        try {
            URI asUri = URI.create(request.url());
            String clientName = asUri.getHost();
            URI uriWithoutHost = cleanUrl(request.url(), clientName);
            FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(
                this.delegate, request, uriWithoutHost);
    
            IClientConfig requestConfig = getClientConfig(options, clientName);
            return lbClient(clientName)
                .executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse();
        }
       //省略
    }
    

    继续查看executeWithLoadBalancer()源码中submit()中selectServer()方法为具体选择下游服务实现

    private Observable<Server> selectServer() {
        return Observable.create(new OnSubscribe<Server>() {
            @Override
            public void call(Subscriber<? super Server> next) {
                try {
                    Server server = loadBalancerContext.getServerFromLoadBalancer(loadBalancerURI, loadBalancerKey);   
                    next.onNext(server);
                    next.onCompleted();
                } catch (Exception e) {
                    next.onError(e);
                }
            }
        });3.5 负载均衡分析
    }
    

    最终通过loadBalancerContext对象实现负载均衡,而loadBalancerContext则通过上篇文章中ILoadBalancers获取下游服务器列表实现,最终根据选择的服务列表实现负载均衡

  • 3.6 源码实现总结

    (1) 首先通过@EnableFeignClients注解开启FeignClient功能,只有注解存在,才会扫描@FeignClient

    (2) 根据扫描到的@FeignClient接口,将接口创建交给spring容器管理

    (3) 当调用@FeignClient修饰的接口时,通过JDK代理生成具体的RequestTemplate模板对象

    (4) 根据RequestTemplate再生成Http请求的Request对象

    (5) Request对象交给Client去处理,其中的Client网络请求框架可以是HttpURLConnection、HttpClient和OKHttp

    (6) 最后Client被封装到LoadBalancerClient类中,通过Ribbon实现负载均衡

4. Feign基础功能及配置

4.1 默认配置文件

Feign default 用途
Decoder ResponseEntityDecoder 编码
Logger Slf4jLogger 日志
Encoder SpringEncoder 解码
Contract SpringMvcContract 协议
Feign.Builder HystrixFeign.Builder
Client 如果Ribbon可用则为LoadBalancerFeignClient,否则为默认Feign client
  • 使用自定义配置

可以自定义配置,使用默认值,但要需要注意,如果使用自定义配置,则不能将该配置置至@ComponentScan之内

@Configuration
public class FooConfiguration {
    @Bean
    public Contract feignContract() {
        return new feign.Contract.Default();
    }

    @Bean
    public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
        return new BasicAuthRequestInterceptor("user", "password");
    }
}
  • 使用配置文件,配置单个FeignClient
feign:
  client:
    config:
      feignName:
        connectTimeout: 5000
        readTimeout: 5000
        loggerLevel: full
        errorDecoder: com.example.SimpleErrorDecoder
        retryer: com.example.SimpleRetryer
        requestInterceptors:
          - com.example.FooRequestInterceptor
          - com.example.BarRequestInterceptor
        decode404: false
        encoder: com.example.SimpleEncoder
        decoder: com.example.SimpleDecoder
        contract: com.example.SimpleContract
  • 所有FeignClient使用一套配置
feign:
  client:
    config:
      default:
        connectTimeout: 5000
        readTimeout: 5000
        loggerLevel: basic

注意:如果配置文件和@Configuration同时存在,优先使用配置文件,如果想使用@Configuration则可使用feign.client.default-to-properties=false

4.2 Feign开启GZIP压缩

具体用法大家可以查看官方Feign官方文档

feign:
  compression:
    response:
      enabled: true
    request:
      enabled: true

4.3Feign日志

日志级别 说明
NONE 不打印日志(默认)
BASIC 只打印请求方式及url和响应码,执行时间
HEADERS 只打印请求及响应头
FULL 表示展示所有信息

Feign默认未开启日志,如果需要开启日志只需执行如下几步
1.调用方启动类或配置文件创建日志Bean,配置日志输出信息
2.配置文件中配置日志级别

在这里插入图片描述

下图为配置日志级别
箭头所指处即为对外提供的api全路径,当服务消费者调用该api时,则会以debug日志级别打印

在这里插入图片描述

debug日志级别打印信息

在这里插入图片描述

4.4 @QueryMap

使用request请求时,接收对象,可使用@SpringQueryMap进行接收

@FeignClient("demo")
public class DemoTemplate {

    @GetMapping(path = "/demo")
    String demoEndpoint(@SpringQueryMap Params params);
}

6.参考资料

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章