Spring Cloud 源码学习之 Feign

 2019-11-23 11:08  阅读(889)
文章分类:Spring Cloud

欢迎访问陈同学博客原文

Spring Cloud Doc: Declarative REST Client: Feign

本文学习了 Spring Cloud 中 openfeign 组件,代码基于 Finchley.SR1 版本。

什么是Feign

spring-cloud-openfeign 在 Github 描述了其特性:

Declarative REST Client: Feign creates a dynamic implementation of an interface decorated with JAX-RS or Spring MVC annotations

Feign 利用注解来描述接口,简化了 Java HTTP Client 的调用过程,隐藏了实现细节。

下面是个小例子,在A服务中调用User Service。

@FeignClient(name = "USER-SERVICE", fallbackFactory = UserServiceFallback.class)
    public interface UserService {
        @GetMapping("/users/{id}")
        User getUser(@PathVariable("id") String id);
    }

这种融合 Spring MVC 注解的声明式调用,结合Ribbon做客户端负载均衡,再加上Hystrix的安全守护,简单又强大。

Feign 的原理

在应用启动时,Feign 会自动为 @FeignClient 标记的接口动态创建实现类。在调用接口时,会根据接口上的注解信息来创建RequestTemplate(可参考附录),结合实际调用时的参数来创建Request,最后完成调用。

具体的Http Client可以自由选择,如:Apache Http Client、OkHttp等都可以。

源码学习

入口

Spring Cloud 相关组件的源码的学习入口一般在两个地方可以找到,一是 EnableXXX的注解,二是SPI机制中的spring.factories。

在使用 Feign时,通过 @EnableFeignClients 来启用。

@Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @Documented
    @Import(FeignClientsRegistrar.class)
    public @interface EnableFeignClients {
        ...
        Class<?>[] defaultConfiguration() default {};
    }

它以 @Import 的方式将FeignClientsRegistrar实例注入到Spring Ioc 容器中。

###动态注册

FeignClientsRegistrar 用于处理 FeignClient 的全局配置和被 @FeignClient 标记的接口,为接口动态创建实现类并添加到Ioc容器。

class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar,
          ResourceLoaderAware, EnvironmentAware {

       @Override
       public void registerBeanDefinitions(AnnotationMetadata metadata,
             BeanDefinitionRegistry registry) {
          // 处理默认配置类 
          registerDefaultConfiguration(metadata, registry);
          // 注册被 @FeignClient 标记的接口
          registerFeignClients(metadata, registry);
       }
    }

@EnableFeignClients 中有个属性 defaultConfiguration,可以用来配置Feign的属性。

public @interface EnableFeignClients {
        Class<?>[] defaultConfiguration() default {};
    }

registerDefaultConfiguration() 方法就是获取defaultConfiguration属性值,如果有则将配置类注入到Ioc容器。

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 标记的接口。首先扫描了classpath中 @FeignClient 标记的接口,然后注册。

public void registerFeignClients(AnnotationMetadata metadata,
            BeanDefinitionRegistry registry) {
        // classpath scan工具
        ClassPathScanningCandidateComponentProvider scanner = getScanner();
        scanner.setResourceLoader(this.resourceLoader);
        ...
        // 利用FeignClient作为过滤条件
        AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
                FeignClient.class);
        for (String basePackage : basePackages) {
            Set<BeanDefinition> candidateComponents = scanner
                    .findCandidateComponents(basePackage);
            for (BeanDefinition candidateComponent : candidateComponents) {
                if (candidateComponent instanceof AnnotatedBeanDefinition) {
                    ...
                    // 注册
                    registerFeignClient(registry, annotationMetadata, attributes);
                }
            }
        }
    }

由于 @FeignClient 标记的是接口,不是普通对象,因此 Feign 利用了 FeignClientFactoryBean 来特殊处理。

接着看 registerFeignClient(),最重要的是FeignClientFactoryBean.

private void registerFeignClient(BeanDefinitionRegistry registry,
            AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
        String className = annotationMetadata.getClassName();
        // 拿到FeignClientFactoryBean的BeanDefinitionBuilder
        BeanDefinitionBuilder definition = BeanDefinitionBuilder
                .genericBeanDefinition(FeignClientFactoryBean.class);
        validate(attributes);
        definition.addPropertyValue("url", getUrl(attributes));
        ...

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

也就是说,FeignClient 标记的接口实例会由 FeignClientFactoryBean.getObject() 来搞定。

调试时在 getObject() 加个断点,在创建具体对象时会进入该方法。getObject()时会根据 @FeignClient 注解的一些属性信息来创建bean。

@Override
    public Object getObject() throws Exception {
        FeignContext context = applicationContext.getBean(FeignContext.class);
        Feign.Builder builder = feign(context);

        // 如果FeignClient没有指定URL(配置的是service)
        if (!StringUtils.hasText(this.url)) {
            String url;
            if (!this.name.startsWith("http")) {
                url = "http://" + this.name;
            }
            else {
                url = this.name;
            }
            url += cleanPath();
            // 结合ribbon使得客户端具备负载均衡的能力
            return loadBalance(builder, context, new HardCodedTarget<>(this.type,
                    this.name, url));
        }
        ...
    }

loadBalance()方法如下,注意 client 是 LoadBalancerFeignClient。

protected <T> T loadBalance(Feign.Builder builder, FeignContext context,
            HardCodedTarget<T> target) {
        // 得到的是 LoadBalancerFeignClient
        Client client = getOptional(context, Client.class);
        if (client != null) {
            builder.client(client);
            // HystrixTargeter
            Targeter targeter = get(context, Targeter.class);
            return targeter.target(this, builder, context, target);
        }
        ...
    }

跟进targeter.target(),最后会发现调用了SynchronousMethodHandler.create()方法。也就是说,FeignClientFactoryBean.getObject() 返回的是一个SynchronousMethodHandler对象。

public MethodHandler create(Target<?> target, MethodMetadata md,
                                RequestTemplate.Factory buildTemplateFromArgs,
                                Options options, Decoder decoder, ErrorDecoder errorDecoder) {
      return new SynchronousMethodHandler(target, client, retryer, requestInterceptors, logger,
                                          logLevel, md, buildTemplateFromArgs, options, decoder,
                                          errorDecoder, decode404);
    }

执行请求

SynchronousMethodHandler是核心类,负责根据参数创建RequestTemplate,然后使用具体的http client执行请求。

看一下SynchronousMethodHandler.invoke()方法。

@Override
    public Object invoke(Object[] argv) throws Throwable {
      // 利用参数构建请求模板, argv 就是被MVC注解描述的各种参数
      RequestTemplate template = buildTemplateFromArgs.create(argv);
      Retryer retryer = this.retryer.clone();
      while (true) {
        try {
          // 执行请求 
          return executeAndDecode(template);
        } catch (RetryableException e) {
          retryer.continueOrPropagate(e);
          if (logLevel != Logger.Level.NONE) {
            logger.logRetry(metadata.configKey(), logLevel);
          }
          continue;
        }
      }
    }

具体执行由executeAndDecode()搞定,targetRequest()就是应用Feign的拦截器,decode()用于处理response,可以自定义Decoder.

Object executeAndDecode(RequestTemplate template) throws Throwable {
      // 应用Feign 的拦截器
      Request request = targetRequest(template);
      Response response;
      long start = System.nanoTime();
      try {
        // 真正发起请求 
        response = client.execute(request, options);
        // ensure the request is set. TODO: remove in Feign 10
        response.toBuilder().request(request).build();
      } catch (IOException e) {
        ...
      }
      try {
        // response 处理机制,可以自定义Decoder来处理response
        if (response.status() >= 200 && response.status() < 300) {
          if (void.class == metadata.returnType()) {
            return null;
          } else {
            return decode(response);
          }
        } else if (decode404 && response.status() == 404 && void.class != metadata.returnType()) {
          return decode(response);
        } else {
          throw errorDecoder.decode(metadata.configKey(), response);
        }
      } ...
    }

拓展机制

Feign 提供了拓展机制用于定制request和response。

  • custom request

Feign的请求拦截器RequestInterceptor在targetRequest()实现。

// 应用所有自定义拦截器
    Request targetRequest(RequestTemplate template) {
      for (RequestInterceptor interceptor : requestInterceptors) {
        interceptor.apply(template);
      }
      // 基于请求模板创建Request对象 
      return target.apply(new RequestTemplate(template));
    }

拦截器结构如下:

public interface RequestInterceptor {
      void apply(RequestTemplate template);
    }

RequestInterceptor 参数是 RequestTemplate,也就是说在发送具体的请求前,提供了拓展机制可以在每个自定义的拦截器中处理请求信息。

  • custom response

自行实现Decoder接口来处理response

public class MyResponseDecoder implements Decoder {
        // type 为 @FeignClient标记接口中方法的返回值类型
        @Override
        public Object decode(Response response, Type type) throws IOException, DecodeException, FeignException {
           // 通过 response.body().asInputStream() 获取返回的数据
           return 处理后的response.
        }
    }

小结

Feign 的精华是一种设计思想,它设计了一种全新的HTTP调用方式,屏蔽了具体的调用细节,与Spring MVC 注解的结合更是极大提高了效率(没有重复造轮子,又设计一套新注解)。

其他的特性列举一下:

  • 利用Spring 的动态注入机制,实现了ImportBeanDefinitionRegistrar接口,为 @FeignClient 标记的接口创建实现类并添加到Ioc容器
  • 设计了良好拓展机制,开发者可以定制request和response。

Feign 其他知识

fallback 与 fallbackFactory

以下内容翻译自Spring Cloud Feign文档

Hystrix 支持 fallback(降级)的概念,在熔断器打开或发生异常时可以执行默认的代码。如果要对某个 @FeignClient 启用 fallback,只需要设置 fallback 属性即可。

@FeignClient(name = "hello", fallback = HystrixClientFallback.class)
    protected interface HystrixClient {
        @RequestMapping(method = RequestMethod.GET, value = "/hello")
        Hello iFailSometimes();
    }

    static class HystrixClientFallback implements HystrixClient {
        @Override
        public Hello iFailSometimes() {
            return new Hello("fallback");
        }
    }

如果需要访问fallback触发的原因,可以使用fallbackFactory属性,它可以提供Throwable对象。

@FeignClient(name = "hello", fallbackFactory = HystrixClientFallbackFactory.class)
    protected interface HystrixClient {
        @RequestMapping(method = RequestMethod.GET, value = "/hello")
        Hello iFailSometimes();
    }

    @Component
    static class HystrixClientFallbackFactory implements FallbackFactory<HystrixClient> {
        @Override
        public HystrixClient create(Throwable cause) {
            return new HystrixClient() {
                @Override
                public Hello iFailSometimes() {
                    return new Hello("fallback; reason was: " + cause.getMessage());
                }
            };
        }
    }

官方小提示:

There is a limitation with the implementation of fallbacks in Feign and how Hystrix fallbacks work. Fallbacks are currently not supported for methods that return com.netflix.hystrix.HystrixCommand and rx.Observable.

Decoder Demo

Add possibility to get body from feign.Response as a decoded object

上面提了一下利用Decoder来处理Response,这里再写个小Demo。

UserService提供了下面接口用于获取用户信息,返回的数据中包含 code(状态码,200表示成功)、message(提示信息)和data(实际的数据)

@GetMapping("/users/{id}")
     public Map getUser(@PathVariable("id") String id) {
         Map<String, Object> result = new HashMap<>();
         result.put("code", "200");
         result.put("message", "ok");
         result.put("data", new User("1001", "张三"));
         return result;
     }

B服务利用Feign调用UserService,注意:返回值为 User,不是Map

@FeignClient(name = "USER-SERVICE")
    public interface Userervice {
        @GetMapping("/users/{id}")
        User getUser(@PathVariable("id") String id) ;
    }

自定义Decoder(仅用简单的代码做演示,不可实际使用)。

public class MyResponseDecoder implements Decoder {

        @Override
        public Object decode(Response response, Type type) throws IOException, DecodeException, FeignException {
            ObjectMapper objectMapper = new ObjectMapper();
            // 将response转为json
            JSONObject jsonObject = objectMapper.readValue(response.body().asInputStream(), JSONObject.class);

            // 如果UserSerivce处理失败, 抛出异常
            if (!"200".equals(jsonObject.getString("code"))) {
                throw new YourException("your error message");
            }
            // 将data转为目标类型
            return jsonObject.getJSONObject("data").toJavaObject(type);
        }
    }

为了让Decoder生效,需要在配置文件中设置 feign.client.config.default.decoder=yourpackage.MyResponseDecoder

通过Decoder,完成了依赖服务返回的数据和目标数据结构的适配,将调用依赖服务变成了和调用应用内方法一样的效果

feign 的常用配置

参考:org.springframework.cloud.openfeign.FeignClientProperties、FeignClientEncodingProperties、FeignHttpClientProperties

以下仅列举一些配置,需要用时可直接查询。

feign:
      hystrix:
        enabled: true
      client:
        config:
          default:
            connectTimeout: 5000
            readTimeout: 5000
            loggerLevel: basic
       # 配置请求和响应压缩 
       compression:
        request:
          enabled: true
          # The list of supported mime types.
          mime-types: text/xml,application/xml,application/json
          # The minimum threshold content size.
          min-request-size: 2048
        response:
          enabled: true

RequestTemplate

RequestTemplate的数据结构如下:

public final class RequestTemplate implements Serializable {
      // 查询参数
      private final Map<String, Collection<String>> queries =
          new LinkedHashMap<String, Collection<String>>();
      // http headers
      private final Map<String, Collection<String>> headers =
          new LinkedHashMap<String, Collection<String>>();
      private String method;
      /* final to encourage mutable use vs replacing the object. */
      private StringBuilder url = new StringBuilder();
      private transient Charset charset;
      private byte[] body;
      private String bodyTemplate;
      private boolean decodeSlash = true;
    }

欢迎关注陈同学的公众号,一起学习,一起成长

20191123100236\_1.png


来源:http://ddrv.cn/a/88268

点赞(0)
版权归原创作者所有,任何形式转载请联系作者; Java 技术驿站 >> Spring Cloud 源码学习之 Feign

相关推荐