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.參考資料

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