Spring Could-04-聲明式調用Feign

        Feign 受Retrofit、JAXRS-2.0 和Web Socket 的影響, 採用了聲明式API 接口的風格, 將Java Http 客戶端綁定到它的內部。 Feign 的首要目標是將Java Http客戶端調用過程變得簡單。 Feign 的源碼地址:https://github.com/OpenFeign/feign。

1. 寫一個Feign客戶端

源碼:

鏈接:https://pan.baidu.com/s/1g22v0EJC0BesWhbjfWDXpw 
提取碼:9gwu

        本案例是在上章代碼的基礎上進行改造的,主要講解了如何使用Feign進行遠程調用。
        新建一個工程eureka-feign-client工程,
pom.xml中引入依賴:

    <dependencies>
        <!--Feign的起步依賴-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!--Eureka Client的起步依賴-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!--Web功能的起步依賴-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <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>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>


    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

配置application.yml:

spring:
  application:
    name: eureka-feign-client
server:
  port: 8765

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/   # 服務註冊地址

程序的啓動類:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableEurekaClient   //開啓Eureka Client功能
@EnableFeignClients  //開啓Feign Client功能
public class FeignClientApplication {

    public static void main(String[] args) {
        SpringApplication.run(FeignClientApplication.class,args);
    }
}

        新建一個EurekaClientFeign的接口,在接口上加上@FeignClient註解來聲明一個Feign Client,其中value爲遠程調用其他服務的服務名,FeignConfig.class爲Feign Client的配置類。sayHiFromClientEureka()方法,該方法通過Feign來調用eureka-client服務的"hi"的API接口,代碼如下:

import com.wyc.config.FeignConfig;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

/**
 * Created by : wyc
 * Created time 2020/6/29 23:11
 */
@FeignClient(value = "eureka-client", configuration = FeignConfig.class)
public interface EurekaClientFeign {

    @GetMapping("/hi")
    String sayHiFromClientEureka(@RequestParam(value = "name") String name);
}

創建FeignConfig

import feign.Retryer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import static java.util.concurrent.TimeUnit.SECONDS;

/**
 * Created by : wyc
 * Created time 2020/6/30 6:30
 */
@Configuration  //表明該類是一個配置類
public class FeignConfig {
    @Bean
    public Retryer feignRetryer(){
        return new Retryer.Default(100,SECONDS.toMillis(1),5);
    }
}

創建HiService

import com.wyc.client.EurekaClientFeign;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * Created by : wyc
 * Created time 2020/6/30 7:00
 */
@Service
public class HiService {
    @Autowired
    private EurekaClientFeign eurekaClientFeign;
    
    public String sayHi(String name){
        return eurekaClientFeign.sayHiFromClientEureka(name);
    }
}

        在Hi Controller 上加上@RestController 註解,開啓RestController 的功能,寫一個API 接口“/hi”,在該接口調用了Hi Service 的sayHi ()方法。HiService 通過EurekaClientFeign 遠程調用eureka-client 服務的API 接口"/hi"

import com.wyc.service.HiService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * Created by : wyc
 * Created time 2020/6/30 7:07
 */
@RestController
public class HiController {

    @Autowired
    private HiService hiService;

    public String sayHi(@RequestParam(defaultValue = "wyc", required = false) String name){
        return hiService.sayHi(name);
    }

}

        啓動Eureka工程,端口號爲8761,啓動兩個eureka-client工程,端口號分別爲8762,8763,啓動eureka-feign-client工程,端口號爲8765
在瀏覽器上多次訪問 http://localhost:8765/hi

hi wyc,i am from port:8762
hi wyc,i am from port:8763

        由此可見,Feign Client遠程調用了eureka-client服務(存在端口爲8762和8763的兩個實例)的“/hi”API接口,Feign Client油浮在均衡的能力。

2. Feign 詳解

        爲了深入理解Feign , 下面將從源碼的角度來講解Feign 。首先來查看FeignClient 註解@FeignClient 的源碼, 其代碼如下:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FeignClient {
    @AliasFor("name")
    String value() default "";

    /** @deprecated */
    @Deprecated
    String serviceId() default "";

    @AliasFor("value")
    String name() default "";

    String qualifier() default "";

    String url() default "";

    boolean decode404() default false;

    Class<?>[] configuration() default {};

    Class<?> fallback() default void.class;

    Class<?> fallbackFactory() default void.class;

    String path() default "";

    boolean primary() default true;
}

        FeignClient 註解被@Target(ElementType.TYPE)修飾,表示FeignClient 註解的作用目標在接口上。@Retention(RetentionPolicy.RUNTIME)註解表明該註解會在Class 字節碼文件中存在,在運行時可以通過反射獲取到。@Documented 表示該註解將被包含在Javadoc 中。
        @FeignClient 註解用於創建聲明式API 接口,該接口是RESTful 風格的。Feign 被設計成插拔式的,可以注入其他組件和Feign 一起使用。最典型的是如果Ribbon 可用, Feign 會和 Ribbon 相結合進行負載均衡。
        在代碼中,value()和name()一樣,是被調用的服務的ServiceId 。url()直接填寫硬編碼Url地址。decode404()即404 是被解碼,還是拋異常。configuration()指明FeignClient 的配置類,默認的配置類爲FeignClientsConfiguration 類,在缺省的情況下, 這個類注入了默認的Decoder 、 Encoder 和Contract 等配置的Bean 。fallback()爲配置熔斷器的處理類。

3. FeignClient的配置

         Feign Client 默認的配置類爲FeignClientsConfiguration ,這個類在spring-cloud-openfeign-core的jar 包下。可以發現這個類注入了很多Feign 相關的配置Bean ,包括FeignRetryer 、F eignLoggerF actory 和FormattingConversionService 等。另外, Decoder 、Encoder 和Contract這3 個類在沒有Bean 被注入的情況下,會自動注入默認配置的Bean ,即ResponseEntity Decoder 、SpringEncoder 和SpringMvcContract。默認注入的配置如下。

  • Decoder feignDecoder: ResponseEntityDecoder。
  • Encoder feignEncoder: SpringEncoder 。
  • Logger feignLogger: Slf4jLogger 。
  • Contract feignContract: SpringMvcContract 。
  • Feign.Builder feignBuilder: HystrixFeign.Builder 。
            FeignClientsConfiguration 的配置類部分代碼如下, @ConditionalOnMissingBean 註解表示如果沒有注入該類的Bean 就會默認注入一個Bean 。
@Configuration
public class FeignClientsConfiguration {
    //省略代碼...........
    @Bean
    @ConditionalOnMissingBean
    public Decoder feignDecoder() {
        return new OptionalDecoder(new ResponseEntityDecoder(new SpringDecoder(this.messageConverters)));
    }

    @Bean
    @ConditionalOnMissingBean
    public Encoder feignEncoder() {
        return new SpringEncoder(this.messageConverters);
    }

    @Bean
    @ConditionalOnMissingBean
    public Contract feignContract(ConversionService feignConversionService) {
        return new SpringMvcContract(this.parameterProcessors, feignConversionService);
    }
    //省略代碼.........
}

        重寫FeignClientsConfiguration 類中的Bean , 覆蓋掉默認的配置Bean ,從而達到自定義配置的目的。例如Feign 默認的配置在請求失敗後, 重試次數爲0 ,即不重試( Retry er.NEVER_RETRY )。現在希望在請求失敗後能夠重試,這時需要寫一個配置FeignConfig 類,在該類中注入Retryer的Bean ,覆蓋掉默認的Retryer的Bean , 並將FeignConfig 指定爲FeignClient 的配置類。代碼如下:

import feign.Retryer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import static java.util.concurrent.TimeUnit.SECONDS;

/**
 * Created by : wyc
 * Created time 2020/6/30 6:30
 */
@Configuration  //表明該類是一個配置類
public class FeignConfig {
    @Bean
    public Retryer feignRetryer(){
        return new Retryer.Default(100,SECONDS.toMillis(1),5);
    }
}

        在上面的代碼中,通過覆蓋了默認的Retryer的Bean,更改了FeignClient的請求失敗重試的策略,重試間隔爲100毫秒,最大重試時間爲1秒,重試次數爲5次。

4. 從源碼的角度講解Feign的工作原理

        Feign 是一個僞Java Http 客戶端, Feign 不做任何的請求處理。Feign 通過處理註解生成Request 模板,從而簡化了Http API 的開發。 開發人員可以使用註解的方式定製Request API模板。在發送HttpRequest 請求之前, Feign 通過處理註解的方式替換掉Request 模板中的參數,生成真正的Request ,並交給Java Http 客戶端去處理。利用這種方式,開發者只需要關注Feign註解模板的開發,而不用關注Http 請求本身,簡化了Http 請求的過程,使得Http請求變得簡單和容易理解。
         Feign 通過包掃描注入FeignClient 的Bean ,該源碼在FeignClientsRegistrar 類中。首先在程序啓動時,會檢查是否有@EnableFeignClients 註解,如果有該註解,則開啓包掃描,掃描被@FeignClient 註解的接口。

    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();
            }

            this.registerClientConfiguration(registry, name, defaultAttrs.get("defaultConfiguration"));
        }

    }

        當程序的啓動類上有@EnableFeignClients註解.在程序啓動後,程序會通過包掃描將有@FeignClient註解修飾的接口連同接口名和註解的信息一起取出,賦給BeanDefinitionBuilder,然後根據BeanDefinitionBuilder得到BeanDefinition,最後將BeanDefinition注入IOC容器中,源碼如下:

    public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        ClassPathScanningCandidateComponentProvider scanner = this.getScanner();
        scanner.setResourceLoader(this.resourceLoader);
        Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());
        AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(FeignClient.class);
        Class<?>[] clients = attrs == null ? null : (Class[])((Class[])attrs.get("clients"));
        Object basePackages;
        if (clients != null && clients.length != 0) {
            final Set<String> clientClasses = new HashSet();
            basePackages = new HashSet();
            Class[] var9 = clients;
            int var10 = clients.length;

            for(int var11 = 0; var11 < var10; ++var11) {
                Class<?> clazz = var9[var11];
                ((Set)basePackages).add(ClassUtils.getPackageName(clazz));
                clientClasses.add(clazz.getCanonicalName());
            }

            AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() {
                protected boolean match(ClassMetadata metadata) {
                    String cleaned = metadata.getClassName().replaceAll("\\$", ".");
                    return clientClasses.contains(cleaned);
                }
            };
            scanner.addIncludeFilter(new FeignClientsRegistrar.AllTypeFilter(Arrays.asList(filter, annotationTypeFilter)));
        } else {
            scanner.addIncludeFilter(annotationTypeFilter);
            basePackages = this.getBasePackages(metadata);
        }

        Iterator var17 = ((Set)basePackages).iterator();

        while(var17.hasNext()) {
            String basePackage = (String)var17.next();
            Set<BeanDefinition> candidateComponents = scanner.findCandidateComponents(basePackage);
            Iterator var21 = candidateComponents.iterator();

            while(var21.hasNext()) {
                BeanDefinition candidateComponent = (BeanDefinition)var21.next();
                if (candidateComponent instanceof AnnotatedBeanDefinition) {
                    AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition)candidateComponent;
                    AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
                    Assert.isTrue(annotationMetadata.isInterface(), "@FeignClient can only be specified on an interface");
                    Map<String, Object> attributes = annotationMetadata.getAnnotationAttributes(FeignClient.class.getCanonicalName());
                    String name = this.getClientName(attributes);
                    this.registerClientConfiguration(registry, name, attributes.get("configuration"));
                    this.registerFeignClient(registry, annotationMetadata, attributes);
                }
            }
        }

    }

    private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
        String className = annotationMetadata.getClassName();
        BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);
        this.validate(attributes);
        definition.addPropertyValue("url", this.getUrl(attributes));
        definition.addPropertyValue("path", this.getPath(attributes));
        String name = this.getName(attributes);
        definition.addPropertyValue("name", name);
        definition.addPropertyValue("type", className);
        definition.addPropertyValue("decode404", attributes.get("decode404"));
        definition.addPropertyValue("fallback", attributes.get("fallback"));
        definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
        definition.setAutowireMode(2);
        String alias = name + "FeignClient";
        AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
        boolean primary = (Boolean)attributes.get("primary");
        beanDefinition.setPrimary(primary);
        String qualifier = this.getQualifier(attributes);
        if (StringUtils.hasText(qualifier)) {
            alias = qualifier;
        }

        BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, new String[]{alias});
        BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
    }

        注入BeanDefinition之後,通過JDK代理,當調用Feign Client接口裏面的方法的時候,該方法被攔截,源碼在ReflectiveFeign類。
        在SynchronousMethodHandler類進行攔截處理,會根據參數生成RequestTemplate對象,該對象是Http請求的模板。

5. Feign如何實現負載均衡的

        FeignRibbonClientAutoConfiguration 類配置了Client 的類型(包括HttpURLConnection 、OkHIttp 和HttpClient ),最終向容器注入的是Client 的實現類LoadBalancerFeignClient ,即負載均衡客戶端。

6. Feign流程總結

(1) 首先通過@EnableFeignClients 註解開啓Feign Client 的功能。只有這個註解存在,纔會在程序啓動時開啓對@FeignClient 註解的包掃描。
(2) 根據Feign 的規則實現接口,井在接口上面加上@FeignClient 註解。
(3) 程序啓動後,會進行包掃描,掃描所有的@FeignClient 的註解的類,並將這些信息注入IoC 容器中。
(4) 當接口的方法被調用時, 通過JDK 的代理來生成具體的RequestTemplate模板對象。
(5) 根據RequestTemplate 再生成Http 請求的Request 對象。
(6) Request 對象交給Client 去處理, 其中Client 的網絡請求框架可以是HttpURLConnection 、HttpClient 和OkHttp 。
(7) 最後Client 被封裝到LoadBalanceClient 類,這個類結合類Ribbon 做到了負載均衡。

    

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