Spring Cloud OpenFeign 官网地址* https://spring.io/projects/spring-cloud-openfeign#overview 总体概览介绍,最新版本为3.1.2***
Spring Cloud OpenFeign 文档地址* https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/ 介绍OpenFeign的详细使用***
本篇我们将单独来学习OpenFeign。学习OpenFeign之前我们先来了解Feign,在没有Feign之前Java可以通过HttpClient、OkHttp、HttpURLConnection、RestTemplate、WebClient等来操作Http,而Feign是NetFlix公司开发的声明式、模板化的HTTP客户端,使得使用Http请求远程服务时就像调用本地方法一样的体验,Feign出现使得我们更加便捷、优雅的调用HTTP客户端,Feign支持多种注解例如自带的注解和JAX-RS注解。
到此引出本篇主角OpenFeign也是一个声明式REST客户端,使用JAX-RS或Spring MVC注解,还支持可插拔编码器和解码器,集成Spring Cloud LoadBalancer,在使用Feign时提供一个负载均衡的http客户端。简单的说Spring Cloud OpenFeign是对Feign一个增强,使其支持Spring MVC注解,并与SpringCloud完成整合。
大致的步骤为Pom文件加spring-cloud-starter-openfeign启动器依赖、加注解加配置、最后SpringBoot启动类上加启用注解@EnableFeignClients就完成。而Spring MVC注解风格的不同类型请求方法使用示例如下:
@FeignClient("stores")public interface StoreClient { @RequestMapping(method = RequestMethod.GET, value = "/stores") List getStores(); @RequestMapping(method = RequestMethod.GET, value = "/stores") Page getStores(Pageable pageable); @RequestMapping(method = RequestMethod.POST, value = "/stores/{storeId}", consumes = "application/json") Store update(@PathVariable("storeId") Long storeId, Store store); @RequestMapping(method = RequestMethod.DELETE, value = "/stores/{storeId:\\d+}") void delete(@PathVariable Long storeId);}
关于Spring Cloud OpenFeign配置属性的列表详细可查看附录页。而常见的配置属性如下:
feign: client: config: feignName: connectTimeout: 5000 readTimeout: 5000 loggerLevel: full errorDecoder: com.example.SimpleErrorDecoder retryer: com.example.SimpleRetryer defaultQueryParameters: query: queryValue defaultRequestHeaders: header: headerValue requestInterceptors: - com.example.FooRequestInterceptor - com.example.BarRequestInterceptor decode404: false encoder: com.example.SimpleEncoder decoder: com.example.SimpleDecoder contract: com.example.SimpleContract capabilities: - com.example.FooCapability - com.example.BarCapability queryMapEncoder: com.example.SimpleQueryMapEncoder metrics.enabled: false
如果我们项目原来是使用NetFlix的原生Feign注解进行开发,在OpenFeign中可无需修改Feign原生注解,只需进行配置就可以轻易兼容原来代码无需整改。前面文章示例使用OpenFeign声明代码如下
package cn.itxs.ecom.commons.service.openfeign;import org.springframework.cloud.openfeign.FeignClient;import org.springframework.web.bind.annotation.PathVariable;import org.springframework.web.bind.annotation.RequestMapping;@FeignClient("ecom-storage-service")public interface StorageFeignService { @RequestMapping("/deduct/{commodityCode}/{count}") String deduct(@PathVariable("commodityCode") String commodityCode, @PathVariable("count") int count);}
创建FeignConfiguration配置类
@Configurationpublic class FeignConfiguration { @Bean public Contract feignContract() { return new feign.Contract.Default(); }}
feign: client: config: ecom-order-service: loggerLevel: basic contract: feign.Contract.Default
package cn.itxs.ecom.commons.service.openfeign;import feign.Param;import feign.RequestLine;@FeignClient("ecom-storage-service")public interface StorageFeignService { @RequestLine("GET /deduct/{commodityCode}/{count}") String deduct(@Param("commodityCode") String commodityCode, @Param("count") int count);}
启动库存和订单微服务,访问订单服务接口,通过原生feign注解调用库存的服务
在配置文件中设置连接超时时间如下
feign: client: config: # feignName,feign名称 ecom-storage-service: # 连接超时时间,防止由于服务器处理时间过长而阻塞调用方,默认2s connectTimeout: 3000 # 请求处理超时时间,在建立连接时应用,并在返回响应时间过长时触发,默认5s readTimeout: 5000
为了测试效果,我们在库存微服务的方法中添加睡眠7秒,超过超时时间
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z8jCSbqU-1660730255090)(https://upload-images.jianshu.io/upload_images/28369161-f9011311c83bfde4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
然后重新启动库存和订单微服务,访问订单服务创建订单接口后调用库存时出现了请求处理超时提示
在订单微服务中增加自定义拦截器CustomFeignInterceptor
package cn.itxs.ecom.order.intercepter;import feign.RequestInterceptor;import feign.RequestTemplate;import lombok.extern.slf4j.Slf4j;@Slf4jpublic class CustomFeignInterceptor implements RequestInterceptor { @Override public void apply(RequestTemplate requestTemplate) { requestTemplate.header("username","itxs"); requestTemplate.query("id","1001"); requestTemplate.uri("/uri"); log.info("This is a custom feign interceptor"); }}
可以在配置类中通过@Bean放在Spring容器中
@Configurationpublic class FeignConfiguration { @Bean public Contract feignContract() { return new feign.Contract.Default(); } @Bean public CustomFeignInterceptor customFeignInterceptor() { return new CustomFeignInterceptor(); }}
也可以直接在yaml文件配置如下:
feign: client: config: requestInterceptors: - cn.itxs.ecom.order.intercepter.CustomFeignInterceptor
启动订单和库存微服务,访问订单创建接口,订单微服务的日志中出现我们在拦截器中加入参数和uri地址。
记录日志形式同样可以通过配置类或者配置文件参数配置
@Configurationpublic class FeignConfiguration { @Bean Logger.Level feignLoggerLevel() { return Logger.Level.FULL; }}
前面我们在订单微服务的SpringBoot启动类上加启用注解@EnableFeignClients,我们直接来看下这个注解的大致功能
容易看出@EnableFeignClients注解上会通过@Import引入FeignClientsRegistrar,这个类实现了ImportBeanDefinitionRegistrar,在Spring容器启动时会加载这个类中的registerBeanDefinitions方法,在这个方法里又调用了注册feign客户端的registerFeignClients方法:
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { registerDefaultConfiguration(metadata, registry); // 注册feign客户端 registerFeignClients(metadata, registry); } public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { LinkedHashSet candidateComponents = new LinkedHashSet<>(); // 获取标注为@EnableFeignClients注解的属性 Map attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName()); // 获取clients属性中配置的类 final Class>[] clients = attrs == null ? null : (Class>[]) attrs.get("clients"); if (clients == null || clients.length == 0) { // 获取需要扫描包路径下有FeignClient注解的类 ClassPathScanningCandidateComponentProvider scanner = getScanner(); scanner.setResourceLoader(this.resourceLoader); scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class)); Set basePackages = getBasePackages(metadata); for (String basePackage : basePackages) { candidateComponents.addAll(scanner.findCandidateComponents(basePackage)); } } else { for (Class> clazz : clients) { candidateComponents.add(new AnnotatedGenericBeanDefinition(clazz)); } } for (BeanDefinition candidateComponent : candidateComponents) { if (candidateComponent instanceof AnnotatedBeanDefinition) { // 验证带注释的类是一个接口 AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent; AnnotationMetadata annotationMetadata = beanDefinition.getMetadata(); // 断言FeignClient修饰的类必须是接口 Assert.isTrue(annotationMetadata.isInterface(), "@FeignClient can only be specified on an interface"); // 获取FeignClient注解上的属性值 Map attributes = annotationMetadata .getAnnotationAttributes(FeignClient.class.getCanonicalName()); String name = getClientName(attributes); registerClientConfiguration(registry, name, attributes.get("configuration")); // 注册feignClient registerFeignClient(registry, annotationMetadata, attributes); } } }
前面的代码逻辑主要是解析出项目可扫描路径下被@FeignClient修饰的接口,然后调用registerFeignClient方法注入到Spring容器中。registerFeignClient的代码逻辑较多,重点分支如下
我们先抓住重点,在截图中代码段中光标+号收起部分代码内容如下:
BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(clazz, () -> { factoryBean.setUrl(getUrl(beanFactory, attributes)); factoryBean.setPath(getPath(beanFactory, attributes)); factoryBean.setDecode404(Boolean.parseBoolean(String.valueOf(attributes.get("decode404")))); Object fallback = attributes.get("fallback"); if (fallback != null) { factoryBean.setFallback(fallback instanceof Class ? (Class>) fallback : ClassUtils.resolveClassName(fallback.toString(), null)); } Object fallbackFactory = attributes.get("fallbackFactory"); if (fallbackFactory != null) { factoryBean.setFallbackFactory(fallbackFactory instanceof Class ? (Class>) fallbackFactory : ClassUtils.resolveClassName(fallbackFactory.toString(), null)); } return factoryBean.getObject(); });
这里向容器里注册的是一个FeignClientFactoryBean,当我们从容器中获取对应对象时,会调用factoryBean这个类中的getObject()方法,
Feign是一个abstract抽象类,builder()返回的是一个内部类Builder,
Feign的newInstance抽象方法有两个子类,分别是反射的ReflectiveFeign和异步的AsyncFeign。从抽象类Feign的静态内部类Builder中提供target方法
在FeignClientFactoryBean的getTarget方法的最后一行调用target方法,而Targeter是一个接口,有默认实现类DefaultTargeter
return (T) targeter.target(this, builder, context, new HardCodedTarget<>(type, name, url));
从DefaultTargeter实现类可以知道最终调用的是抽象类Feign静态内部类Builder的target()方法
回过头我们再来看下ReflectiveFeign的实现
从里面关键的代码可以看到底层的核心是使用JDK动态代理机制来实现
InvocationHandler handler = factory.create(target, methodToHandler); T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class>[] {target.type()}, handler);
至此我们可以清楚知道使用的FeignClient对象是一个代理对象,当调用相应的方法时会调用到InvocationHandler.invoke方法中,也即是会调用**
**ReflectiveFeign.FeignInvocationHandler.invoke方法
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if ("equals".equals(method.getName())) { try { Object otherHandler = args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null; return equals(otherHandler); } catch (IllegalArgumentException e) { return false; } } else if ("hashCode".equals(method.getName())) { return hashCode(); } else if ("toString".equals(method.getName())) { return toString(); } return dispatch.get(method).invoke(args); }
总体时序图如下:
invoke接口最后一行调用dispatch.get(method).invoke(args),往下调用SynchronousMethodHandler.invoke->SynchronousMethodHandler.executeAndDecode->client.execute
RequestTemplate用来封装HTTP全部内容
客户端client的execute方法有三个实现类,分别是FeignBlockingLoadBalancerClient、RetryableFeignBlockingLoadBalancerClient、Default。我们看下FeignBlockingLoadBalancerClient的execute方法的实现,往下的逻辑就是调用Feign封装的http请求
从前面分析代码我们总结下Spring Cloud OpenFeign原理重要流程如下: