文档章节

SpringCloud——声明式调用Feign

d
 devils_os
发布于 2019/10/22 18:26
字数 1794
阅读 43
收藏 0

Feign声明式调用

一、Feign简介

使用Ribbon和RestTemplate消费服务的时候,有一个最麻烦的点在于,每次都要拼接URL,组织参数,所以有了Feign声明式调用,Feign的首要目标是将Java HTTP客户端的调用过程非常简单。它采用声明式API接口的风格,将Java Http客户端绑定到它的内部,以此来方便调用。

二、Feign实践

1、项目组织

从前面Ribbon中拿到项目整体,然后再整改成如下目录
帖子地址:https://my.oschina.net/devilsblog/blog/3115061
码云地址:https://gitee.com/devilscode/cloud-practice/tree/ribbon-test

  1. 修改项目名称为feign-test
  2. 修改原来的子Module ribbon-service为feign-service

2、核心pom


feign-test/pom.xml
<modelVersion>4.0.0</modelVersion>

<groupId>com.calvin.feigb</groupId>
<artifactId>feign-test</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>

<modules>
    <module>common-service</module>
    <module>eureka-server</module>
    <module>feign-service</module>
</modules>

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.3.RELEASE</version>
</parent>

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
</properties>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Dalston.SR4</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

common-service/pom.xml
<parent>
    <groupId>com.calvin.feigb</groupId>
    <artifactId>feign-test</artifactId>
    <version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>common-service</artifactId>

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-eureka</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

eureka-service/pom.xml
<parent>
    <groupId>com.calvin.feigb</groupId>
    <artifactId>feign-test</artifactId>
    <version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>eureka-server</artifactId>
<properties>
    <java.version>1.8</java.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-eureka-server</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

feign-service/pom.xml
<parent>
    <groupId>com.calvin.feigb</groupId>
    <artifactId>feign-test</artifactId>
    <version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>feign-service</artifactId>

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-eureka</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-feign</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

3、feign-service项目改造

(1)修改服务名称

这样就可以从eureka注册中心中区别出我们的服务是feign-service

server:
  port: 8082
spring:
  application:
    name: feign-service
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8080/eureka/

(2)增加配置注解EurekaFeignClient

服务启动的时候,此注解的作用会使得所有使用了注解FeignClient的类,被扫描解析,然后注册到IoC容器中。

/**
 * <p> 
 *     启动类 
 * </p>
 *
 * @author Calvin
 * @date 2019/10/09
 * @since
 */
@EnableEurekaClient
@SpringBootApplication
@EnableFeignClients
public class FeignServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(FeignServerApplication.class);
    }
}

(3)新建FeignConfiguration.java

/**
 * <p> FeignClient的配置类 </p>
 *
 * @author Calvin
 * @date 2019/10/21
 * @since
 */
@Configuration
public class FeignConfiguration {

    /**
     * 覆盖默认的重试
     * 重试间隔100ms
     * 最大重试时间1s
     * 最大重试次数5次
     */
    @Bean
    public Retryer feignRetryer(){
        return new Retryer.Default(100,SECONDS.toMillis(1), 5);
    }
}

关于FeignClient的相关配置,如果我们不主动去配置,都有默认配置,配置类为FeignClientsConfiguration.class,详细内容后续分析

(4)增加CommonFeignClient.java

/**
 * <p> 
 *     远程调用公共服务消费端
 * </p>
 *
 * @author Calvin
 * @date 2019/10/21
 * @since
 */
@FeignClient(value = "common-service", configuration = FeignConfiguration.class)
public interface CommonFeignClient {

    /**
     * 调用 common-service/hello接口
     * @return
     */
    @GetMapping(value = "/hello")
    String sayHi();

}

(5)改造SayHiController.java

/**
 * <p>
 *     测试接口
 * </p>
 *
 * @author Calvin
 * @date 2019/10/09
 * @since
 */
@RestController
public class SayHiController {

//    @Autowired
//    private RemoteCommonService remoteCommonService;

    @Autowired
    private CommonFeignClient commonFeignClient;

    @GetMapping("/hi")
    public String sayHi(){
        return commonFeignClient.sayHi() + ", this is feign service";
    }

}

(6)停用RemoteCommonService

删除RemoteCommonService.java即可

3、启动整体项目

  • step1. EurekaSeverApplicaton
  • step2. CommonServiceApplication
  • step3. CommonServiceApplication2
  • step4. FeignServerApplication

4、调用接口测试

Eureka管理界面 http://localhost:8080/

调用 http://localhost:8082/hi

刷新页面

三、工作原理探索

1. FeignClientsConfiguration

/**
 * @author Dave Syer
 * @author Venil Noronha
 */
@Configuration
public class FeignClientsConfiguration {

	/**
	 * 配置消息转换器
	 */
	@Autowired
	private ObjectFactory<HttpMessageConverters> messageConverters;

	/**
	 * 注入参数解析,用于解析@RequestParam
	 */
	@Autowired(required = false)
	private List<AnnotatedParameterProcessor> parameterProcessors = new ArrayList<>();
	
	/**
	 * 
	 */
	@Autowired(required = false)
	private List<FeignFormatterRegistrar> feignFormatterRegistrars = new ArrayList<>();

	@Autowired(required = false)
	private Logger logger;

	
	/**
	 * 返回值解析
	 */
	@Bean
	@ConditionalOnMissingBean
	public Decoder feignDecoder() {
		return new ResponseEntityDecoder(new SpringDecoder(this.messageConverters));
	}

	/**
	 * url编码
	 */
	@Bean
	@ConditionalOnMissingBean
	public Encoder feignEncoder() {
		return new SpringEncoder(this.messageConverters);
	}

	@Bean
	@ConditionalOnMissingBean
	public Contract feignContract(ConversionService feignConversionService) {
		return new SpringMvcContract(this.parameterProcessors, feignConversionService);
	}

	@Bean
	public FormattingConversionService feignConversionService() {
		FormattingConversionService conversionService = new DefaultFormattingConversionService();
		for (FeignFormatterRegistrar feignFormatterRegistrar : feignFormatterRegistrars) {
			feignFormatterRegistrar.registerFormatters(conversionService);
		}
		return conversionService;
	}

	@Configuration
	@ConditionalOnClass({ HystrixCommand.class, HystrixFeign.class })
	protected static class HystrixFeignConfiguration {
		@Bean
		@Scope("prototype")
		@ConditionalOnMissingBean
		@ConditionalOnProperty(name = "feign.hystrix.enabled", matchIfMissing = false)
		public Feign.Builder feignHystrixBuilder() {
			return HystrixFeign.builder();
		}
	}

	/**
	 * 配置重试,NEVER_RETRY代表从不重试
	 */
	@Bean
	@ConditionalOnMissingBean
	public Retryer feignRetryer() {
		return Retryer.NEVER_RETRY;
	}

	/**
	 * 给Feign绑定重试器
	 */
	@Bean
	@Scope("prototype")
	@ConditionalOnMissingBean
	public Feign.Builder feignBuilder(Retryer retryer) {
		return Feign.builder().retryer(retryer);
	}

	/**
	 * 日志工厂
	 */
	@Bean
	@ConditionalOnMissingBean(FeignLoggerFactory.class)
	public FeignLoggerFactory feignLoggerFactory() {
		return new DefaultFeignLoggerFactory(logger);
	}

}

2. Feign工作原理

(1)步骤

  1. 通过@EnableFeignClients开启注解扫描
  2. 扫描@FeignClient注解修饰的元注解信息,使用BeanDefinitionBuilder解析成BeanDefinition,交给IoC容器中
  3. 通过JDK代理,当发现FeignClient被调用的时候,拦截该方法
  4. 拦截到该方法后,在SynchronousMethodHandler类中,使用生成的RequestTemplate,重新生成Request对象
  5. 使用HttpClient调用请求,获取Response

(2)相关代码

扫描@EnableFeignClients
class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar,
		ResourceLoaderAware, BeanClassLoaderAware, EnvironmentAware {
	/**
	 * <p>
	 *    检查EnableFeignClients注解是否开启,如果开启,则开始注册默认配置
	 * </p>
	 */
	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"));
		}
	}
}
扫描@FeignClient注解,获取到元注解信息
public void registerFeignClients(AnnotationMetadata metadata,
		BeanDefinitionRegistry registry) {
	ClassPathScanningCandidateComponentProvider scanner = getScanner();
	scanner.setResourceLoader(this.resourceLoader);

	Set<String> basePackages;

	Map<String, Object> attrs = metadata
			.getAnnotationAttributes(EnableFeignClients.class.getName());
	AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
			FeignClient.class);
	final Class<?>[] clients = attrs == null ? null
			: (Class<?>[]) attrs.get("clients");
	if (clients == null || clients.length == 0) {
		scanner.addIncludeFilter(annotationTypeFilter);
		basePackages = getBasePackages(metadata);
	}
	else {
		final Set<String> clientClasses = new HashSet<>();
		basePackages = new HashSet<>();
		for (Class<?> clazz : clients) {
			basePackages.add(ClassUtils.getPackageName(clazz));
			clientClasses.add(clazz.getCanonicalName());
		}
		AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() {
			@Override
			protected boolean match(ClassMetadata metadata) {
				String cleaned = metadata.getClassName().replaceAll("\\$", ".");
				return clientClasses.contains(cleaned);
			}
		};
		scanner.addIncludeFilter(
				new AllTypeFilter(Arrays.asList(filter, annotationTypeFilter)));
	}
	for (String basePackage : basePackages) {
		Set<BeanDefinition> candidateComponents = scanner
				.findCandidateComponents(basePackage);
		for (BeanDefinition candidateComponent : candidateComponents) {
			if (candidateComponent instanceof AnnotatedBeanDefinition) {
				// verify annotated class is an interface
				AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
				AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
				Assert.isTrue(annotationMetadata.isInterface(),
						"@FeignClient can only be specified on an interface");

				Map<String, Object> attributes = annotationMetadata
						.getAnnotationAttributes(
								FeignClient.class.getCanonicalName());

				String name = getClientName(attributes);
				registerClientConfiguration(registry, name,
						attributes.get("configuration"));

				registerFeignClient(registry, annotationMetadata, attributes);
			}
		}
	}
}
解析元注解内容,注入到IoC容器中
private void registerFeignClient(BeanDefinitionRegistry registry,
		AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
	String className = annotationMetadata.getClassName();
	BeanDefinitionBuilder definition = BeanDefinitionBuilder
			.genericBeanDefinition(FeignClientFactoryBean.class);
	validate(attributes);
	definition.addPropertyValue("url", getUrl(attributes));
	definition.addPropertyValue("path", getPath(attributes));
	String name = getName(attributes);
	definition.addPropertyValue("name", name);
	definition.addPropertyValue("type", className);
	definition.addPropertyValue("decode404", attributes.get("decode404"));
	definition.addPropertyValue("fallback", attributes.get("fallback"));
	definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
	definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);

	String alias = name + "FeignClient";
	AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();

	boolean primary = (Boolean)attributes.get("primary"); // has a default, won't be null

	beanDefinition.setPrimary(primary);

	String qualifier = getQualifier(attributes);
	if (StringUtils.hasText(qualifier)) {
		alias = qualifier;
	}

	BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
			new String[] { alias });
	BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}
JDK代理,拦截FeignClient的调用
public class ReflectiveFeign extends Feign {
@Override
  public <T> T newInstance(Target<T> target) {
    Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
    Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
    List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();

    for (Method method : target.type().getMethods()) {
      if (method.getDeclaringClass() == Object.class) {
        continue;
      } else 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 = factory.create(target, methodToHandler);
    T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class<?>[]{target.type()}, handler);

    for(DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
      defaultMethodHandler.bindTo(proxy);
    }
    return proxy;
  }
}

拦截进行处理,先解析RequestTemplate,然后使用HttpClient调用网络请求

final class SynchronousMethodHandler implements MethodHandler {

  /**
   * 将传递过来的参数解析成RequestTemplate
   */
  @Override
  public Object invoke(Object[] argv) throws Throwable {
    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;
      }
    }
  }
  /**
   * 将RequestTemplate转换成一个Request,然后使用HttpClient调用请求,拿到返回结果
   */
  Object executeAndDecode(RequestTemplate template) throws Throwable {
  Request request = targetRequest(template);

  Response response;
  long start = System.nanoTime();
  try {
  response = client.execute(request, options);
  response.toBuilder().request(request).build();
  } catch (IOException e) {
  // 处理异常
  }
  //省略返回的代码
}

四、总结

  1. 根据原有的ribbon-test改造feign-test,动手尝试Feign调用的实现
  2. 探究Feign工作原理,以及相关源码阅读
  3. 关于Feign和Hystrix等关系与协作,下一篇出

本文代码:https://gitee.com/devilscode/cloud-practice/tree/feign-test/

© 著作权归作者所有

d
粉丝 0
博文 19
码字总数 33268
作品 0
昌平
后端工程师
私信 提问
加载中

评论(0)

SpringCloud学习系列之二 ----- 服务消费者(Feign)和负载均衡(Ribbon)

前言 本篇主要介绍的是SpringCloud中的服务消费者(Feign)和负载均衡(Ribbon)功能的实现以及使用Feign结合Ribbon实现负载均衡。 SpringCloud Feign Feign 介绍 Feign是一个声明式的Web Servi...

虚无境
2019/01/15
603
0
SpringCloud之声明式服务调用 Feign(三)

一 Feign简介 Feign是一种声明式、模板化的HTTP客户端,也是netflix公司组件。使用feign可以在远程调用另外服务的API,如果调用本地API一样。 我们知道,阿里巴巴的doubbo采用二进制的RPC协议...

2018/07/13
0
0
【Spring Cloud】分布式必学springcloud(七)——声明式服务调用Feign

一、前言 在上篇博客中,小编带大家接触了断路器Hystrix,是不是很好玩。分布式服务之间有了Hystrix,可以很好的提高容错性能。 但是在实际开发中,项目中会有很多的服务间的调用,对于服务的...

kisscatforever
2018/04/23
0
0
python中有没有类似spring cloud feign的库?

python中有没有类似spring cloud feign客户端声明式调用的库?

OS啊哒
2019/03/29
65
0
SpringCloud 基础教程(八)-Hystrix熔断器(上)

  我的博客:程序员笑笑生,欢迎浏览博客!  上一章 SpringCloud基础教程(七)-声明式服务调用Fegign当中,我们介绍了使用Fegin更加简化的实现服务间的调用.本章节我将继续探索Hystrix组件的...

程序员笑笑生
01/31
31
0

没有更多内容

加载失败,请刷新页面

加载更多

Minecraft Fabric Client 教程 #5 添加Event、Sprint和ToggleCommand

首发于Enaium的个人博客 添加Event 下载 放在cn.enaium.excel里 然后在Excel.java里面添加EventManager public enum Excel { [...] public EventManager eventManager; pu......

Enaium
13分钟前
25
0
记 S3Service 代码中的一个低级错误

osgl-storage 是 osgl 工具箱 中用于简化存储的. 其特点是接口简单, 支持多种存储引擎插件, 包括本地文件系统, AWS S3, Azure Blob, 七牛 Kodo 服务. 最近老码农在一次调试中偶然发现了 osgl...

开源老码农
15分钟前
297
1
如何实现Samba文件共享服务

目标:实现Samba文件共享服务 试验环境:两台主机服务端:192.168.56.11客户端:192.168.56.12 配置用户认证共享 服务端操作: 1.关闭防火墙,关闭selunix [root@hejie ~]# setenforce 0[...

linuxprobe2020
17分钟前
18
0
SQL Server Profiler - 如何过滤跟踪以仅显示来自一个数据库的事件?

如何将SQL Server Profiler跟踪限制为特定数据库? 我看不到如何过滤跟踪,看不到我连接的实例上的所有数据库的事件。 #1楼 在Trace properties> Events Selection选项卡下>选择show all co...

技术盛宴
17分钟前
25
0
Kafka配置及常用命令笔记

一、kafka配置 1. 服务端 server.properties #broker 的全局唯一编号,不能重复broker.id=0#删除 topic 功能使能delete.topic.enable=true#处理网络请求的线程数量num.network.thr...

liddblog
22分钟前
27
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部