云社区 博客 博客详情

SpringCloud——声明式调用Feign

心底荷花 发表于 2019-10-22 21:45:16 2019-10-22
0
0

【摘要】 Feign声明式调用一、Feign简介使用Ribbon和RestTemplate消费服务的时候,有一个最麻烦的点在于,每次都要拼接URL,组织参数,所以有了Feign声明式调用,Feign的首要目标是将JavaHTTP客户端的调用过程非常简单。它采用声明式API接口的风格,将JavaHttp客户端绑定到它的内部,以此来方便调用。二、Feign实践1、项目组织从前面Ribbon中...

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
4.0.0

com.calvin.feigb
feign-test
pom
1.0-SNAPSHOT


    common-service
    eureka-server
    feign-service



    org.springframework.boot
    spring-boot-starter-parent
    1.5.3.RELEASE



    UTF-8
    UTF-8
    1.8



    
        
            org.springframework.cloud
            spring-cloud-dependencies
            Dalston.SR4
            pom
            import
        
    


common-service/pom.xml

    com.calvin.feigb
    feign-test
    1.0-SNAPSHOT

4.0.0

common-service


    
        org.springframework.cloud
        spring-cloud-starter-eureka
    
    
        org.springframework.boot
        spring-boot-starter-web
    



    
        
            org.springframework.boot
            spring-boot-maven-plugin
        
    


eureka-service/pom.xml

    com.calvin.feigb
    feign-test
    1.0-SNAPSHOT

4.0.0
eureka-server

    1.8



    
        org.springframework.cloud
        spring-cloud-starter-eureka-server
    

    
        org.springframework.boot
        spring-boot-starter-test
        test
    



    
        
            org.springframework.boot
            spring-boot-maven-plugin
        
    


feign-service/pom.xml

    com.calvin.feigb
    feign-test
    1.0-SNAPSHOT

4.0.0

feign-service


    
        org.springframework.cloud
        spring-cloud-starter-eureka
    
    
        org.springframework.boot
        spring-boot-starter-web
    
    
        org.springframework.cloud
        spring-cloud-starter-feign
    
    
        org.springframework.boot
        spring-boot-starter-test
        test
    



    
        
            org.springframework.boot
            spring-boot-maven-plugin
        
    

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容器中。

/**
 * 

* 启动类 *

* * @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

/**
 * 

FeignClient的配置类

* * @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

/**
 * 

* 远程调用公共服务消费端 *

* * @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

/**
 * 

* 测试接口 *

* * @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 messageConverters;

	/**
	 * 注入参数解析,用于解析@RequestParam
	 */
	@Autowired(required = false)
	private List parameterProcessors = new ArrayList<>();
	
	/**
	 * 
	 */
	@Autowired(required = false)
	private List 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 {
	/**
	 * 

* 检查EnableFeignClients注解是否开启,如果开启,则开始注册默认配置 *

*/ private void registerDefaultConfiguration(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { Map 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 basePackages;

	Map 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 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 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 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 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 newInstance(Target target) {
    Map nameToHandler = targetToHandlersByName.apply(target);
    Map methodToHandler = new LinkedHashMap();
    List defaultMethodHandlers = new LinkedList();

    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/

文章来源: www.oschina.net,作者:devils_os,版权归原作者所有,如需转载,请联系作者。

原文链接:https://my.oschina.net/devilsblog/blog/3120790

登录后可下载附件,请登录或者注册

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件至:huaweicloud.bbs@huawei.com进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容。
评论文章 //点赞 收藏 0
点赞
分享文章到微博
分享文章到朋友圈

上一篇:idea添加注释配置

下一篇:STL中有关deque、stack、queue、priority_queue

评论 (0)


登录后可评论,请 登录注册

评论