陈同学
微服务
Accelerator
About
# Spring Cloud 源码学习之 Feign > [Spring Cloud Doc: Declarative REST Client: Feign](https://cloud.spring.io/spring-cloud-netflix/multi/multi_spring-cloud-feign.html) 本文学习了 Spring Cloud 中 openfeign 组件,代码基于 *Finchley.SR1* 版本。 ## 什么是Feign [spring-cloud-openfeign 在 Github 描述了其特性:](https://github.com/spring-cloud/spring-cloud-openfeign) ``` 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。 ```java @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` 来启用。 ```java @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented @Import(FeignClientsRegistrar.class) public @interface EnableFeignClients { ... Class<?>[] defaultConfiguration() default {}; } ``` 它以 `@Import` 的方式将FeignClientsRegistrar实例注入到Spring Ioc 容器中。 ###动态注册 FeignClientsRegistrar 用于处理 FeignClient 的全局配置和被 `@FeignClient` 标记的接口,为接口动态创建实现类并添加到Ioc容器。 ```java class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware { @Override public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { // 处理默认配置类 registerDefaultConfiguration(metadata, registry); // 注册被 @FeignClient 标记的接口 registerFeignClients(metadata, registry); } } ``` `@EnableFeignClients` 中有个属性 **defaultConfiguration**,可以用来配置Feign的属性。 ```java public @interface EnableFeignClients { Class<?>[] defaultConfiguration() default {}; } ``` registerDefaultConfiguration() 方法就是获取**defaultConfiguration**属性值,如果有则将配置类注入到Ioc容器。 ```java 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` 标记的接口,然后注册。 ```java 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. ```java 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。 ```java @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。 ```java 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对象。 ```java 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()方法。 ```java @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. ```java 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()实现。 ```java // 应用所有自定义拦截器 Request targetRequest(RequestTemplate template) { for (RequestInterceptor interceptor : requestInterceptors) { interceptor.apply(template); } // 基于请求模板创建Request对象 return target.apply(new RequestTemplate(template)); } ``` 拦截器结构如下: ```java public interface RequestInterceptor { void apply(RequestTemplate template); } ``` RequestInterceptor 参数是 RequestTemplate,也就是说在发送具体的请求前,提供了拓展机制可以在每个自定义的拦截器中处理请求信息。 * **custom response** 自行实现Decoder接口来处理response ```java 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** 属性即可。 ```java @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对象。 ```java @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](https://github.com/OpenFeign/feign/issues/402) 上面提了一下利用Decoder来处理Response,这里再写个小Demo。 UserService提供了下面接口用于获取用户信息,返回的数据中包含 code(状态码,200表示成功)、message(提示信息)和data(实际的数据) ```java @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**。 ```java @FeignClient(name = "USER-SERVICE") public interface Userervice { @GetMapping("/users/{id}") User getUser(@PathVariable("id") String id) ; } ``` 自定义Decoder(仅用简单的代码做演示,不可实际使用)。 ```java 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** * 在 `@EnableFeignClients` 注解中配置,全局生效 先创建一个配置类CustomFeignClientsConfiguration ```java public class CustomFeignClientsConfiguration { @Bean public Decoder feignResponseDecoder() { return new FeignResponseDecoder(); } } ``` 全局设置。 ```java @EnableFeignClients(defaultConfiguration = CustomFeignClientsConfiguration.class) ``` 也可以为CustomFeignClientsConfiguration加上 @Configuaration 注解,这样每个子服务就不用单独配置。 * 在配置文件中单独配置某个FeignClient:feign.client.config.feignName.decoder=xxx * 通过代码显示配置某个FeignClient ```java @FeignClient(name = "USER-SERVICE", configuration = CustomFeignClientsConfiguration.class) ``` **通过Decoder,完成了依赖服务返回的数据和目标数据结构的适配,将调用依赖服务变成了和调用应用内方法一样的效果**。 ### feign 的常用配置 > 参考:org.springframework.cloud.openfeign.FeignClientProperties、FeignClientEncodingProperties、FeignHttpClientProperties 以下仅列举一些配置,需要用时可直接查询。 ```yaml 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的数据结构如下: ```java 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; } ```
本文由
cyj
创作,可自由转载、引用,但需署名作者且注明文章出处。
文章标题:
Spring Cloud 源码学习之 Feign
文章链接:
https://chenyongjun.vip/articles/94
扫码或搜索 cyjrun 关注微信公众号, 结伴学习, 一起努力