Spring Cloud——OpenFeign

一.简介

Java项目中接口调用:

  • HttpClient: Apache Jakarta Common下的子项目,用来提供高效的、最新的、功能丰富的支持 Http 协议的客户端编程工具包,并且它支持 HTTP 协议最新版本和建议。
  • Okhttp:一个处理网络请求的开源项目,是安卓端最火的轻量级框架,由 Square 公司贡献,用于替代 HttpUrlConnection 和 Apache HttpClient。OkHttp 拥有简洁的 API、高效的性能,并支持多种协议(HTTP/2 和 SPDY)。
  • HttpURLConnection:是 Java的标准类,它继承自 URLConnection,可用于向指定网站发送 GET 请求、POST 请求。HttpURLConnection 使用比较复杂,不像 HttpClient 那样容易使用。
  • RestTemplate: Spring提供的用于访问 Rest 服务的客户端,RestTemplate 提供了多种便捷访问远程 HTTP 服务的方法,能够大大提高客户端的编写效率。

上面是常用的方法,我们下面介绍一个简单的Feign。Feign是一个声明式的REST的客户端,能让REST调用更加简单。Feign提供HTTP模板,通过编写简单的接口和插入注解,就可以定义好HTTP请求的参数,格式,地址等信息。Feign完全代理http请求,只需要像调用方法一样调用就可以完成服务请求。

Spring Cloud对Feign进行了封装,使其支持SpringMVC注解和HttpMessageConverters。Feign可以与Eureka和Ribbon组合用于支持负载均衡。

Feign是Spring Cloud组件中的一个轻量级RESTful的HTTP服务客户端Feign内置了Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。Feign的使用方式是:使用Feign的注解定义接口,调用这个接口,就可以调用服务注册中心的服务。

OpenFeign是Spring Cloud在Feign的基础上支持了SpringMVC的注解,如@RequesMapping等等。OpenFeign的@Feignclient可以解析SpringMVc的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。

二.OpenFeign

2.1 使用

1.增加依赖:


    org.springframework.cloud
    spring-cloud-starter-openfeign

2.定义一个Feign的客户端,以接口形式存在:

// 使用 @FeignClient 注解来指定提供者的名字
@FeignClient(value = "eureka-client-provider")
public interface TestClient {
    // 这里一定要注意需要使用的是提供者那端的请求相对路径,这里就相当于映射了
    @RequestMapping(value = "/provider/xxx",
    method = RequestMethod.POST)
    CommonResponse> getPlans(@RequestBody planGetRequest request);
}

3.在Controller层中像调用Service层一样调用(想要共用,可以单独创建一个API Client的公共项目,基于约定的形式,每写一个接口就写一个调用的Client,后面打成共用的Jar,无论哪个项目想使用,只需要调用公共的SDK jar,就可以了)

@RestController
public class TestController {
    // 这里就相当于原来自动注入的 Service
    @Autowired
    private TestClient testClient;
    // controller 调用 service 层代码
    @RequestMapping(value = "/test", method = RequestMethod.POST)
    public CommonResponse> get(@RequestBody planGetRequest request) {
        return testClient.getPlans(request);
    }
}

4.在启动类上加上@EnableFeignClients注解,如果Feign定义的接口和启动类不在一个包名下,,还需要定制扫描的包名@EnableFeignClients(basePackages="")

2.2 源码分析

查看FeiginClient注解的源码:

@Target({ElementType.TYPE})  //表明注解使用在接口上
@Retention(RetentionPolicy.RUNTIME)  //表示该注解会在Class字节码文件中存在,在运行时可以通过反射获取到
@Documented  //该注解将被包含在JavaDoc中
public @interface FeignClient {
    @AliasFor("name")
    String value() default "";

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

    String contextId() 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注解用于创建声明式API接口,该接口是RESTFul风格的。在源码中,value()和name()一样,是被调用服务的ServiceId。url()直接填写硬编码的URL地址,decode404()是被解码,还是抛异常。configuration()指明FeignClient的配置类,默认的配置类为FeignClientsConfiguration类, 在缺省的情况下,这个类默认注入默认的Decoder,Encoder等配置类。fallback()是配置熔断器的处理类。

Feign通过处理注解生成Request模板,从而简化HTTP API的开发。开发人员可以使用注解的方式定制Request API模板。在发送Http Request请求之前,Feign通过处理注解的方法替换到Request模板的参数,生成真正的Request,并交给Java Http客户端去处理。利用这种方式,开发者只需要关注Feign注解模板的开发,而不用关注Http请求本身,简化HTTP请求的过程。

                                                         Spring Cloud——OpenFeign_第1张图片

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

 private void registerDefaultConfiguration(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        Map 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,然后根据BenDefinition得到BeanDeifinition,注入IOC容器中。

//扫描FeignClient注解的  
public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        ClassPathScanningCandidateComponentProvider scanner = this.getScanner();
        scanner.setResourceLoader(this.resourceLoader);
        Map 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 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 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 attributes = annotationMetadata.getAnnotationAttributes(FeignClient.class.getCanonicalName());
                    String name = this.getClientName(attributes);
                    this.registerClientConfiguration(registry, name, attributes.get("configuration"));
                    this.registerFeignClient(registry, annotationMetadata, attributes);
                }
            }
        }

    }

//获取BeanDeifition 注入IOC容器中
 private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map 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);
        String contextId = this.getContextId(attributes);
        definition.addPropertyValue("contextId", contextId);
        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 = contextId + "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代理,当调用FeignClient 接口中的方法,该方法被拦截,源码在ReflectiveFeign类,源码如下:

 public  T newInstance(Target target) {
        Map nameToHandler = this.targetToHandlersByName.apply(target);
        Map methodToHandler = new LinkedHashMap();
        List defaultMethodHandlers = new LinkedList();
        Method[] var5 = target.type().getMethods();
        int var6 = var5.length;

        for(int var7 = 0; var7 < var6; ++var7) {
            Method method = var5[var7];
            if (method.getDeclaringClass() != Object.class) {
                if (Util.isDefault(method)) {
                    DefaultMethodHandler handler = new DefaultMethodHandler(method);
                    defaultMethodHandlers.add(handler);
                    methodToHandler.put(method, handler);
                } else {
                    methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
                }
            }
        }

        InvocationHandler handler = this.factory.create(target, methodToHandler);
        T proxy = Proxy.newProxyInstance(target.type().getClassLoader(), new Class[]{target.type()}, handler);
        Iterator var12 = defaultMethodHandlers.iterator();

        while(var12.hasNext()) {
            DefaultMethodHandler defaultMethodHandler = (DefaultMethodHandler)var12.next();
            defaultMethodHandler.bindTo(proxy);
        }

        return proxy;
    }

在SynchronousMethodHandler类进行拦截处理,会根据参数生成RequestTemplate对象,该对象时Http请求的模板,代码如下:

    public Object invoke(Object[] argv) throws Throwable {
        RequestTemplate template = this.buildTemplateFromArgs.create(argv);
        Options options = this.findOptions(argv);
        Retryer retryer = this.retryer.clone();

        while(true) {
            try {
                return this.executeAndDecode(template, options);
            } catch (RetryableException var9) {
                RetryableException e = var9;

                try {
                    retryer.continueOrPropagate(e);
                } catch (RetryableException var8) {
                    Throwable cause = var8.getCause();
                    if (this.propagationPolicy == ExceptionPropagationPolicy.UNWRAP && cause != null) {
                        throw cause;
                    }

                    throw var8;
                }

                if (this.logLevel != Level.NONE) {
                    this.logger.logRetry(this.metadata.configKey(), this.logLevel);
                }
            }
        }
    }

有一个executeAndDecode方法,该方法通过RequestTemplate生成Request请求对象,然后通过Http Client获取Response,即通过Http Client来获取响应。


    Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
        Request request = this.targetRequest(template);
        if (this.logLevel != Level.NONE) {
            this.logger.logRequest(this.metadata.configKey(), this.logLevel, request);
        }

        long start = System.nanoTime();

        Response response;
        try {
            response = this.client.execute(request, options);
        } catch (IOException var16) {
            if (this.logLevel != Level.NONE) {
                this.logger.logIOException(this.metadata.configKey(), this.logLevel, var16, this.elapsedTime(start));
            }

            throw FeignException.errorExecuting(request, var16);
        }

        long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
        boolean shouldClose = true;

    //省略代码
    }

在Feign中Client是一个非常重要的组件,Feign最终发送Requet请求以及接受Response响应都是由Client组件完成的。Client在Feign源码中是一个接口,在默认情况下,Client实现类默认是Client.Default,Client.Default是由HttpUrlConnection来实现网络请求的。另外,Client还支持HttpClient和OkhHttp进行网络请求。

                             Spring Cloud——OpenFeign_第2张图片

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.openfeign.ribbon.FeignRibbonClientAutoConfiguration,\
org.springframework.cloud.openfeign.hateoas.FeignHalAutoConfiguration,\
org.springframework.cloud.openfeign.FeignAutoConfiguration,\
org.springframework.cloud.openfeign.encoding.FeignAcceptGzipEncodingAutoConfiguration,\
org.springframework.cloud.openfeign.encoding.FeignContentGzipEncodingAutoConfiguration,\
org.springframework.cloud.openfeign.loadbalancer.FeignLoadBalancerAutoConfiguration

在上面我们知道了默认的使用Client.Default,Default使用的HttpUrlConnection。如何在Feign中使用HttpClient或者OkhHttp网络请求框架呢?查看上面FeignAutoConfiguration的源码:

 @Configuration(
        proxyBeanMethods = false
    )
    @ConditionalOnClass({ApacheHttpClient.class})
    @ConditionalOnMissingClass({"com.netflix.loadbalancer.ILoadBalancer"})
    @ConditionalOnMissingBean({CloseableHttpClient.class})
    @ConditionalOnProperty(
        value = {"feign.httpclient.enabled"},
        matchIfMissing = true
    )
    protected static class HttpClientFeignConfiguration {
        private final Timer connectionManagerTimer = new Timer("FeignApacheHttpClientConfiguration.connectionManagerTimer", true);
        @Autowired(
            required = false
        )
        private RegistryBuilder registryBuilder;
        private CloseableHttpClient httpClient;

        protected HttpClientFeignConfiguration() {
        }
     ...
  }

    @ConditionalOnClass({ApacheHttpClient.class}),只需要在pom文件上加上HttpClient的Classpath即可。另外在配置文件中applictaion.yml文件中配置feign.httpclient.enable为true,也可以不用写,因为@CondittionalProperty注解可知,默认值就是true。

同理OkhHttp也是同样配置。

那么Feign是如何实现负载均衡的?我们之前知道Client,它有三个实现类:

Spring Cloud——OpenFeign_第3张图片 

其中LoadBalancerFeignClient作为Ribbon负载均衡实现的客户端。Feign是如何注册LoadBanlancerFeignClient,看FeignRibbonClientAutoConfiguration:


@ConditionalOnClass({ILoadBalancer.class, Feign.class})
@ConditionalOnProperty(
    value = {"spring.cloud.loadbalancer.ribbon.enabled"},
    matchIfMissing = true
)
@Configuration(
    proxyBeanMethods = false
)
@AutoConfigureBefore({FeignAutoConfiguration.class})
@EnableConfigurationProperties({FeignHttpClientProperties.class})
@Import({HttpClientFeignLoadBalancedConfiguration.class, OkHttpFeignLoadBalancedConfiguration.class, DefaultFeignLoadBalancedConfiguration.class})
public class FeignRibbonClientAutoConfiguration {
    public FeignRibbonClientAutoConfiguration() {
    }

    @Bean
    @Primary
    @ConditionalOnMissingBean
    @ConditionalOnMissingClass({"org.springframework.retry.support.RetryTemplate"})
    public CachingSpringLoadBalancerFactory cachingLBClientFactory(SpringClientFactory factory) {
        return new CachingSpringLoadBalancerFactory(factory);
    }

    @Bean
    @Primary
    @ConditionalOnMissingBean
    @ConditionalOnClass(
        name = {"org.springframework.retry.support.RetryTemplate"}
    )
    public CachingSpringLoadBalancerFactory retryabeCachingLBClientFactory(SpringClientFactory factory, LoadBalancedRetryFactory retryFactory) {
        return new CachingSpringLoadBalancerFactory(factory, retryFactory);
    }

    @Bean
    @ConditionalOnMissingBean
    public Options feignRequestOptions() {
        return LoadBalancerFeignClient.DEFAULT_OPTIONS;
    }
}
  • @ConditionalOnClass({ILoadBalancer.class, Feign.class}) 其中ILoadBalancer.class是Ribbon依赖的类,换句话说该配置需要引入Ribbon和Feign依赖才会生效

  • @AutoConfigureBefore({FeignAutoConfiguration.class}) 如果该自动配置类生效,则在FeignAutoConfiguration之前进行配置,因为FeignAutoConfiguration关联到Feigin Client代理对象的实例化,而其中真是发生请求调用的Client 对线实例在Feign Client调用Feign.build进行实例化Client实现对象,需要提前实例化对象。换句话来说,如果Client对应Ribbon的实现类LoadBalancerFeignClient存在,就是用Ribbon的负载均衡客户端进行调用处理,反之,使用默认的feign.Client.Default。

  • @Import({DefaultFeignLoadBalancedConfiguration.class})
    DefaultFeignLoadBalancedConfiguration 是一个 Bean 配置类,对 LoadBalancerFeignClient 进行了实例化配置。

总结

     1、 Feign Client 在执行调用最终执行的是 Client#execute。
     2、 Client 是一个接口,其实现类 LoadBalancerFeignClient 和 Default。在 Feign 代理对象实例化作为属性存入 Feign Client 代理对象中。
    3、 当应用依赖中引入 Ribbon 相关依赖时,在 Feign 代理对象实例化前,会先生成 Client Ribbon 实现类 LoadBalancerFeignClient 的实例对象。反之,使用默认的 feign.Default 。

查看LoadBalancerFeignClient类中的execute方法,即执行请求的方法:

    public Response execute(Request request, Options options) throws IOException {
        try {
            URI asUri = URI.create(request.url());
            String clientName = asUri.getHost();
            URI uriWithoutHost = cleanUrl(request.url(), clientName);
            RibbonRequest ribbonRequest = new RibbonRequest(this.delegate, request, uriWithoutHost);
            IClientConfig requestConfig = this.getClientConfig(options, clientName);
            return ((RibbonResponse)this.lbClient(clientName).executeWithLoadBalancer(ribbonRequest, requestConfig)).toResponse();
        } catch (ClientException var8) {
            IOException io = this.findIOException(var8);
            if (io != null) {
                throw io;
            } else {
                throw new RuntimeException(var8);
            }
        }
    }

其中的executeWithLoadBalancer方法,即通过负载均衡的方式进行网络请求,代码如下:

public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException {
        LoadBalancerCommand command = this.buildLoadBalancerCommand(request, requestConfig);

        try {
            return (IResponse)command.submit(new ServerOperation() {
                public Observable call(Server server) {
                    URI finalUri = AbstractLoadBalancerAwareClient.this.reconstructURIWithServer(server, request.getUri());
                    ClientRequest requestForServer = request.replaceUri(finalUri);

                    try {
                        return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig));
                    } catch (Exception var5) {
                        return Observable.error(var5);
                    }
                }
            }).toBlocking().single();
        } catch (Exception var6) {
            Throwable t = var6.getCause();
            if (t instanceof ClientException) {
                throw (ClientException)t;
            } else {
                throw new ClientException(var6);
            }
        }
    }

进入这里面的summit方法,进入这个submit可以看出是LoadBalancerCommand类的方法:

 public Observable submit(final ServerOperation operation) {
      //省略代码
        Observable o = (this.server == null ? this.selectServer() : Observable.just(this.server)).concatMap(new Func1>() {
            public Observable call(Server server) {
   //.....
}

有一个selectServer方法,该方法时选择负载均衡的方法,代码如下:

    private Observable selectServer() {
        return Observable.create(new OnSubscribe() {
            public void call(Subscriber next) {
                try {
                    Server server = LoadBalancerCommand.this.loadBalancerContext.getServerFromLoadBalancer(LoadBalancerCommand.this.loadBalancerURI, LoadBalancerCommand.this.loadBalancerKey);
                    next.onNext(server);
                    next.onCompleted();
                } catch (Exception var3) {
                    next.onError(var3);
                }

            }
        });
    }

最终负载均衡交给LoadBalancerContext来处理。

总结:

  1. 首先通过@EnableFeignClients注解开启FeignClient功能,只有这个注解存在,才会在程序中开启对@FeignClient注解的包扫描

  2. 根据FeinClient规则实现接口,并在接口加上@FeignClient注解

  3. 程序启动后,会进行包扫描,扫描被@FeignClient注解的类,并将这些信息注入IOC容器中

  4. 当接口的方法被调用的时候,通过JDK代理对象生成具体的RestTemplate模板对象

  5. 根据RestTemplate再生成HTTP请求的Request对象

  6. Request交给Client去处理,Client的网络请求框架可以使HTTPURLConnection,HttpClient和OkhHttp

  7. 最后Client被封装到LoadBalancerFeignClient,这个类结合了Ribbon做到了负载均衡

2.3 自定义配置

2.3.1 日志配置

Feign日志等级源码如下:

public enum Level {
    NONE, //不输出日志
    BASIC, //只输出方法的URL和响应状态码以及接口的执行时间
    HEADERS, //将BASIC信息和请求头信息输出
    FULL  //输出完整的请求信息
}

自定义一个配置类:

@Configuration
public class FeignConfiguration {
    /**
     * 日志级别
     *
     * @return
     */
    @Bean
    Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }
}

需要在Feign Client中的@FeignClient增加配置类:

@FeignClient(value = "eureka-client-user-service", configuration = FeignConfiguration. class)
public interface UserRemoteClient {
    // ...
}

在配置文件中执行 Client 的日志级别才能正常输出日志,格式是“logging.level.client 类地址=级别”

logging.level.net.biancheng.feign_demo.remote.UserRemoteClient=DEBUG

其他自定义配置:自定义配置

你可能感兴趣的:(Spring,Cloud)