最近项目中用到了 Spring Cloud OpenFeign,OpenFeign 在服务远程调用的时候非常方便,在这里分享给大家。
01. Feign介绍
Feign 是 Spring Cloud Netiflix 组件中的一个轻量级 Restful 的 HTTP 服务客户端,实现了 webservice 面向接口编程,进一步降低了项目的耦合度。Feign 可以与 Eureka、Nacos 和Ribbon 组合使用以支持负载均衡。
Feign 是声明式服务调用组件,它的强大之处在于,实现了像调用本地方法一样调用远程方法,无感知远程 HTTP 请求。类似于 Dubbo 的思想,Consumer 直接调用 Provider 的接口方法,而不需要通过常规的 Http Client 构造请求,再解析返回数据。
02. 使用方法
spring cloud 工程中添加依赖
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency>
定义远程接口 …
创建 RemoteService 接口,来定义 OpenFeign 要调用的远程服务接口。
同时通过 @FeginClient 注解指定被调用方的服务名,通过 fallback 属性指定 FeignHystrix 类,来进行远程调用的熔断和降级处理。
@FeginClient 中 name 是要调用的服务的名字。
@FeignClient(name = "CF-Service", fallback = FeignHystrix.class)public interface IRemoteService {@RequestLine("POST /CFS/001/{id}")@Headers({"TraceId: {traceId}","SpanId: {spanId}","AreaCode: 010"})@Body("{body}")public Object send(@Param("id") String id, @Param("traceId") String traceId,@Param("spanId") String spanId,@Param("body") String requestBody);}
FeignHystrix.java 代码如下:
@Componentpublic class FeignHystrix implements RemoteService {@Overridepublic Object send() {return "请求超时了";}}
在启动类 Application.java 中添加注解 @EnableDiscoveryClient 开启服务注册、添加注解 @EnableFeignClients 开启 OpenFeign,启动类通过 OpenFeign 调用服务代码如下:
@SpringBootApplication@RestController@EnableDiscoveryClient@EnableFeignClientspublic class Application {public static void main(String[] args) {SpringApplication.run(Application.class, args);}@Autowiredprivate IRemoteService remoteService;@GetMapping("/feign")public String test() {return remoteService.send();}}
FeignClient 注解的属性定义 …

Feign 的注解的定义 …

03. 源码分析
源码入口 …
在使用 Feign时,通过 @EnableFeignClients 来启用。它以 @Import 的方式将FeignClientsRegistrar实例注入到Spring Ioc 容器中。
@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE)@Documented@Import(FeignClientsRegistrar.class)public @interface EnableFeignClients {...Class<?>[] defaultConfiguration() default {};}
FeignClientsRegistrar 用于处理 FeignClient 的全局配置和被 @FeignClient 标记的接口,为接口动态创建实现类并添加到 IOC 容器。
class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar,ResourceLoaderAware, EnvironmentAware {@Overridepublic 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 标记的接口,然后注册。
由于 @FeignClient 标记的是接口,不是普通对象,因此 Feign 利用了 FeignClientFactoryBean 来特殊处理。
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 标记的接口实例会由 FeignClientFactoryBean.getObject() 来搞定。
private void registerFeignClient(BeanDefinitionRegistry registry,AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {String className = annotationMetadata.getClassName();// 拿到 FeignClientFactoryBean 的 BeanDefinitionBuilderBeanDefinitionBuilder 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);}
getObject() 会根据 @FeignClient 注解的一些属性信息来创建 bean。
@Overridepublic 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() 方法:
protected <T> T loadBalance(Feign.Builder builder, FeignContext context,HardCodedTarget<T> target) {// 得到的是 LoadBalancerFeignClientClient client = getOptional(context, Client.class);if (client != null) {builder.client(client);// HystrixTargeterTargeter targeter = get(context, Targeter.class);return targeter.target(this, builder, context, target);}...}
调用了 SynchronousMethodHandler.create() 方法。
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 执行请求。
@Overridepublic 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 10response.toBuilder().request(request).build();} catch (IOException e) {...}try {// response 处理机制,可以自定义 Decoder 来处理 responseif (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);}} ...}
04. Feign的配置
项目中用 Feign 就是为了可以方便做负载均衡,可以只根据服务名进行负载均衡。
Ribbon 配置 …
Feign 使用 Ribbon 配置非常简单:
ribbon.ConnectTimeout = 500ribbon.ReadTimeout = 500
由于 Spring Cloud Feign 会根据注解的 name 和 value 属性,自动创建一个同名的 Ribbon 客户端。这样我们就可以使用 @FeignClient 注解中的服务名属性来设置 Ribbon 参数,这样就可以只针对这一个服务配置相应的负载均衡参数。
CF-Service.ribbon.ConnectTimeout=500CF-Service.ribbon.ReadTimeout=500// 还可配置一些重试机制CF-Service.ribbon.OkToRetryOnAllOperations=trueCF-Service.ribbon.MaxAutoRetriesNextServer=2CF-Service.ribbon.MaxAutoRetries=1
Hystrix 配置 …
Hystrix 配置同 Ribbon 配置一样,直接使用前缀 hystrix.command.default 就可以进行配置,比如全局超时时间:
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=5000
禁用单个FeginClient的Hystrix的支持,只针对此服务进行 Hystrix的禁用配置。
// Configuration表示feign的自定义配置类@FeignClient(name = "CF-Service", configuration = Configuration.class)
@Configurationpublic class Configuration {//禁用当前配置的 hystrix,局部禁用@Bean@Scope("prototype")public Feign.Builder feignBuilder() {return Feign.builder();}}
参考文献
https://blog.csdn.net/qq_33619378/article/details/95353326
https://blog.csdn.net/Anonymous_L/article/details/103991903
https://mp.weixin.qq.com/s/CQgZ-TyhE50cFrEwAHFQWA
(责任编辑:张子鑫)
作者/ 彭博
彭博,银行转型业务开发部,专注于分布式、后端开发、系统优化等领域,目前主要负责银行核心系统架构、代码设计和开发工作。


招聘启事
北银金融科技有限责任公司根植于北京银行,是一家致力于大数据、人工智能、云计算、区块链、物联网等新技术创新与金融科技应用的科技企业,公司充分发挥北京银行企业文化和技术积淀先天优势,通过对技术、场景、生态的完美融合,输出科技创新产品和技术服务。
现诚邀优秀人才加盟
共享金融科技时代硕果

扫描此二维码
期待您的加入





