SpringCloud——声明式调用Feign

原创
2019/10/22 18:26
阅读数 170

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/

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部