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 做到了负载均衡。

    

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