在前面章节中我们介绍了ribbon的使用以及启动原理,在这篇文章中将主要介绍openfeign的原理。在Spring cloud体系中, feign其实有着很重比较重要的地位,因为feign能够大大简化我们对远程请求以及返回结果的处理,帮助我们快速开发。我们还是以一个简单的demo开始我们的代码调试和讲解步骤.
DEMO
对于feign中, spring还是集成的很好的,同时也为我们提供了扩展的地方。
引入依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
启用Feign
@SpringBootApplication @EnableDiscoveryClient @EnableFeignClients public class SpringEurekaClientDemoApplication { public static void main(String[] args) { SpringApplication.run(SpringEurekaClientDemoApplication.class, args); } }
定义FeignClient
package org.spring.learn.eureka.client.feign; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; @FeignClient(value = "SPRING-EUREKA-CLIENT-B") public interface InfoFeignClient { @GetMapping("/info") String info(); }
使用FeignClient类型
package org.spring.learn.eureka.client.controller; import org.spring.learn.eureka.client.feign.InfoFeignClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestTemplate; @RestController public class InfoController { @Autowired private RestTemplate restTemplate; @Autowired private InfoFeignClient infoFeignClient; @GetMapping("/info") public String info() { return restTemplate.getForObject("http://SPRING-EUREKA-CLIENT-B/info", String.class); } @GetMapping("/feign/info") public String feignInfo() { return infoFeignClient.info(); } }
通过以上步骤,我们可以知道,在通过FeignClient
注解定义的接口, 可以通过@Autowired
直接使用。我当前给的示例中,其实是与ribbon
相结合使用的, 因此同时也具备类ribbon
负载均衡的能力。其实对于我们开发而言,大大简化了我们的开发难度。
EnableFeignClients
该注解为启用Feign的一个标识注解,并且在启用的过程中,也会做其他特殊的操作:
- 如果包含
defaultConfiguration
配置,则默认为加载@Configuration
的配置类 - 如果包含了
basePackages
,basePackageClasses
,value
的值设置,则扫表配置下所有类型的FeignClient
- 如果以上配置都为空,则默认从启动类
Application
所在目录查找所有FeignClient
该类的具体源码如下:
package org.springframework.cloud.openfeign; /** * Scans for interfaces that declare they are feign clients (via * {@link org.springframework.cloud.openfeign.FeignClient} <code>@FeignClient</code>). * Configures component scanning directives for use with * {@link org.springframework.context.annotation.Configuration} * <code>@Configuration</code> classes. * * @author Spencer Gibb * @author Dave Syer * @since 1.0 */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented @Import(FeignClientsRegistrar.class) public @interface EnableFeignClients { /** * Alias for the {@link #basePackages()} attribute. Allows for more concise annotation * declarations e.g.: {@code @ComponentScan("org.my.pkg")} instead of * {@code @ComponentScan(basePackages="org.my.pkg")}. * @return the array of 'basePackages'. */ String[] value() default {}; /** * Base packages to scan for annotated components. * <p> * {@link #value()} is an alias for (and mutually exclusive with) this attribute. * <p> * Use {@link #basePackageClasses()} for a type-safe alternative to String-based * package names. * @return the array of 'basePackages'. */ String[] basePackages() default {}; /** * Type-safe alternative to {@link #basePackages()} for specifying the packages to * scan for annotated components. The package of each class specified will be scanned. * <p> * Consider creating a special no-op marker class or interface in each package that * serves no purpose other than being referenced by this attribute. * @return the array of 'basePackageClasses'. */ Class<?>[] basePackageClasses() default {}; /** * A custom <code>@Configuration</code> for all feign clients. Can contain override * <code>@Bean</code> definition for the pieces that make up the client, for instance * {@link feign.codec.Decoder}, {@link feign.codec.Encoder}, {@link feign.Contract}. * * @see FeignClientsConfiguration for the defaults * @return list of default configurations */ Class<?>[] defaultConfiguration() default {}; /** * List of classes annotated with @FeignClient. If not empty, disables classpath * scanning. * @return list of FeignClient classes */ Class<?>[] clients() default {}; }
该注解在使用的过程中,会通过@Import
注入BeanDefinition
对象到容器中, 这部分内容可以参考Import注解使用详解
FeignClientsRegistrar
package org.springframework.cloud.openfeign; /** * @author Spencer Gibb * @author Jakub Narloch * @author Venil Noronha * @author Gang Li */ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware { // patterned after Spring Integration IntegrationComponentScanRegistrar // and RibbonClientsConfigurationRegistgrar // 资源加载器 private ResourceLoader resourceLoader; // 环境变量上下文 private Environment environment; FeignClientsRegistrar() { } static void validateFallback(final Class clazz) { Assert.isTrue(!clazz.isInterface(), "Fallback class must implement the interface annotated by @FeignClient"); } static void validateFallbackFactory(final Class clazz) { Assert.isTrue(!clazz.isInterface(), "Fallback factory must produce instances " + "of fallback classes that implement the interface annotated by @FeignClient"); } static String getName(String name) { if (!StringUtils.hasText(name)) { return ""; } String host = null; try { String url; if (!name.startsWith("http://") && !name.startsWith("https://")) { url = "http://" + name; } else { url = name; } host = new URI(url).getHost(); } catch (URISyntaxException e) { } Assert.state(host != null, "Service id not legal hostname (" + name + ")"); return name; } static String getUrl(String url) { if (StringUtils.hasText(url) && !(url.startsWith("#{") && url.contains("}"))) { if (!url.contains("://")) { url = "http://" + url; } try { new URL(url); } catch (MalformedURLException e) { throw new IllegalArgumentException(url + " is malformed", e); } } return url; } static String getPath(String path) { if (StringUtils.hasText(path)) { path = path.trim(); if (!path.startsWith("/")) { path = "/" + path; } if (path.endsWith("/")) { path = path.substring(0, path.length() - 1); } } return path; } @Override public void setResourceLoader(ResourceLoader resourceLoader) { this.resourceLoader = resourceLoader; } @Override public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { // 注册Configuration registerDefaultConfiguration(metadata, registry); // 扫描并注册所有FeignClient registerFeignClients(metadata, registry); } private void registerDefaultConfiguration(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { // 获取注解EnableFeignClients下所有属性 Map<String, Object> defaultAttrs = metadata .getAnnotationAttributes(EnableFeignClients.class.getName(), true); // 判断是否包含了defaultConfiguration配置 if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) { String name; if (metadata.hasEnclosingClass()) { name = "default." + metadata.getEnclosingClassName(); } else { name = "default." + metadata.getClassName(); } // 注册defaultConfiguration中的配置类到Spring Context中 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); // 获取注解中clients定义列表 final Class<?>[] clients = attrs == null ? null : (Class<?>[]) attrs.get("clients"); // 当clients不为空时,则读取basepackges, basePackageClasses, value属性,并返回扫描路径列表 if (clients == null || clients.length == 0) { // 新增按照类型扫描过滤器 scanner.addIncludeFilter(annotationTypeFilter); basePackages = getBasePackages(metadata); } else { // 当包含了Clients指定类时 final Set<String> clientClasses = new HashSet<>(); basePackages = new HashSet<>(); // 变量并保存Client类型的名称 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); } }; // 新增注解过滤器,以及Client类型过滤器 scanner.addIncludeFilter( new AllTypeFilter(Arrays.asList(filter, annotationTypeFilter))); } for (String basePackage : basePackages) { // 扫描并获取package下所有包含@FeignClient注解的类型,并解析为BeanDefinition Set<BeanDefinition> candidateComponents = scanner.findCandidateComponents(basePackage); // 遍历BeanDefinition for (BeanDefinition candidateComponent : candidateComponents) { // 判断BeanDefinition类型 if (candidateComponent instanceof AnnotatedBeanDefinition) { // verify annotated class is an interface AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent; // 获取BeanDefinition元数据信息 AnnotationMetadata annotationMetadata = beanDefinition.getMetadata(); // 判断当前类型是否为接口 Assert.isTrue(annotationMetadata.isInterface(), "@FeignClient can only be specified on an interface"); // 获取FeignClient注解所有元数据信息 Map<String, Object> attributes = annotationMetadata .getAnnotationAttributes( FeignClient.class.getCanonicalName()); // 获取FeignClient客户端名称 String name = getClientName(attributes); // 注册客户端信息配置信息 registerClientConfiguration(registry, name, attributes.get("configuration")); // 注册客户端 registerFeignClient(registry, annotationMetadata, attributes); } } } } private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map<String, Object> attributes) { // 获取注解所在的class全限定名称 String className = annotationMetadata.getClassName(); // 创建FeignClientFactoryBean对应BeanDefinition BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class); // 验证参数配置 validate(attributes); // 获取并解析url信息 definition.addPropertyValue("url", getUrl(attributes)); // 获取并解析path配置 definition.addPropertyValue("path", getPath(attributes)); // 获取client名称 String name = getName(attributes); // 设置name属性 definition.addPropertyValue("name", name); // 获取contextId String contextId = getContextId(attributes); // 设置contextId属性 definition.addPropertyValue("contextId", contextId); // 设置type属性 definition.addPropertyValue("type", className); // 设置decode404属性 definition.addPropertyValue("decode404", attributes.get("decode404")); // 设置fallback属性 definition.addPropertyValue("fallback", attributes.get("fallback")); // 设置FallbackFactory属性 definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory")); // 设置自动注入类型 definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); // 设置beanDefinition别名信息 String alias = contextId + "FeignClient"; AbstractBeanDefinition beanDefinition = definition.getBeanDefinition(); // 获取primary属性 boolean primary = (Boolean) attributes.get("primary"); // has a default, won't be // null // 设置BeanDefinition primary属性 beanDefinition.setPrimary(primary); String qualifier = getQualifier(attributes); if (StringUtils.hasText(qualifier)) { alias = qualifier; } // 创建BeanDefinitionHolder BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, new String[] { alias }); // 注册BeanDefinition BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry); } ... // 注册FeignClientSpecification配置到spring容器中 private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name, Object configuration) { // 创建BeanDefinition对象 BeanDefinitionBuilder builder = BeanDefinitionBuilder .genericBeanDefinition(FeignClientSpecification.class); // 设置构造器入参信息 builder.addConstructorArgValue(name); builder.addConstructorArgValue(configuration); // 注入到Spring 上下文 registry.registerBeanDefinition( name + "." + FeignClientSpecification.class.getSimpleName(), builder.getBeanDefinition()); } @Override public void setEnvironment(Environment environment) { this.environment = environment; } /** * Helper class to create a {@link TypeFilter} that matches if all the delegates * match. * * @author Oliver Gierke */ private static class AllTypeFilter implements TypeFilter { private final List<TypeFilter> delegates; /** * Creates a new {@link AllTypeFilter} to match if all the given delegates match. * @param delegates must not be {@literal null}. */ AllTypeFilter(List<TypeFilter> delegates) { Assert.notNull(delegates, "This argument is required, it must not be null"); this.delegates = delegates; } @Override public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException { for (TypeFilter filter : this.delegates) { if (!filter.match(metadataReader, metadataReaderFactory)) { return false; } } return true; } } }
以上就是Feign将FeignClient注解的类型扫描转换成BeanDefinitionHolder然后注册到spring的过程。在这过程中有几个点需要注意:
- 通过
FeignClient
注解的类型一定需要为接口 - 在注入到Spring中的
BeanDefinition
使用的是BeanDefinitionHolder
- Feign是注入到Spring是关于
FeingClientFactoryBean
对象, 并不是直接通过代理的方式创建的
FeignContext
@Bean public FeignContext feignContext() { FeignContext context = new FeignContext(); context.setConfigurations(this.configurations); return context; } @Configuration @ConditionalOnMissingClass("feign.hystrix.HystrixFeign") protected static class DefaultFeignTargeterConfiguration { @Bean @ConditionalOnMissingBean public Targeter feignTargeter() { return new DefaultTargeter(); } }
在Feign自动装配的过程中,最重要的一个类型则是FeignContext
对象,该对象在后面的实现中充当着很重要的作用
FeignClientFactoryBean
接下来是作为Feign至关重要的实现,在上面的解析过程中我们介绍了,Feign并没有直接创建代理实现功能,而是通过Spring FactoryBean
的功能,实现对代理类型的延迟初始化。这里我们可以看下该类的主要源码:
package org.springframework.cloud.openfeign; import java.util.Map; import java.util.Objects; import feign.Client; import feign.Contract; import feign.Feign; import feign.Logger; import feign.Request; import feign.RequestInterceptor; import feign.Retryer; import feign.Target.HardCodedTarget; import feign.codec.Decoder; import feign.codec.Encoder; import feign.codec.ErrorDecoder; import org.springframework.beans.BeanUtils; import org.springframework.beans.BeansException; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.cloud.openfeign.ribbon.LoadBalancerFeignClient; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** * @author Spencer Gibb * @author Venil Noronha * @author Eko Kurniawan Khannedy * @author Gregor Zurowski */ class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean, ApplicationContextAware { /*********************************** * WARNING! Nothing in this class should be @Autowired. It causes NPEs because of some * lifecycle race condition. ***********************************/ private Class<?> type; private String name; private String url; private String contextId; private String path; private boolean decode404; private ApplicationContext applicationContext; private Class<?> fallback = void.class; private Class<?> fallbackFactory = void.class; @Override public void afterPropertiesSet() throws Exception { Assert.hasText(this.contextId, "Context id must be set"); Assert.hasText(this.name, "Name must be set"); } protected Feign.Builder feign(FeignContext context) { // 获取FeignLoggerFactory对象 FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class); Logger logger = loggerFactory.create(this.type); // 创建并设置Encoder, Decoder, Contract类型bean信息 // @formatter:off Feign.Builder builder = get(context, Feign.Builder.class) // required values .logger(logger) .encoder(get(context, Encoder.class)) .decoder(get(context, Decoder.class)) .contract(get(context, Contract.class)); // @formatter:on // 配置feign configureFeign(context, builder); return builder; } protected void configureFeign(FeignContext context, Feign.Builder builder) { // 获取关于Feign的配置信息列表 FeignClientProperties properties = this.applicationContext.getBean(FeignClientProperties.class); if (properties != null) { if (properties.isDefaultToProperties()) { // 设置正在使用中的配置 configureUsingConfiguration(context, builder); // 配置使用默认配置 configureUsingProperties( properties.getConfig().get(properties.getDefaultConfig()), builder); // 配置使用当前配置 configureUsingProperties(properties.getConfig().get(this.contextId), builder); } else { configureUsingProperties( properties.getConfig().get(properties.getDefaultConfig()), builder); configureUsingProperties(properties.getConfig().get(this.contextId), builder); configureUsingConfiguration(context, builder); } } else { configureUsingConfiguration(context, builder); } } protected void configureUsingConfiguration(FeignContext context, Feign.Builder builder) { // 获取日志等级 Logger.Level level = getOptional(context, Logger.Level.class); if (level != null) { builder.logLevel(level); } // 获取重试器 Retryer retryer = getOptional(context, Retryer.class); if (retryer != null) { builder.retryer(retryer); } // 获取错误解码器 ErrorDecoder errorDecoder = getOptional(context, ErrorDecoder.class); if (errorDecoder != null) { builder.errorDecoder(errorDecoder); } // 请求参数信息 Request.Options options = getOptional(context, Request.Options.class); if (options != null) { builder.options(options); } // 请求拦截器 Map<String, RequestInterceptor> requestInterceptors = context .getInstances(this.contextId, RequestInterceptor.class); if (requestInterceptors != null) { builder.requestInterceptors(requestInterceptors.values()); } // 是否解码404 if (this.decode404) { builder.decode404(); } } protected void configureUsingProperties( FeignClientProperties.FeignClientConfiguration config, Feign.Builder builder) { if (config == null) { return; } if (config.getLoggerLevel() != null) { builder.logLevel(config.getLoggerLevel()); } if (config.getConnectTimeout() != null && config.getReadTimeout() != null) { builder.options(new Request.Options(config.getConnectTimeout(), config.getReadTimeout())); } if (config.getRetryer() != null) { Retryer retryer = getOrInstantiate(config.getRetryer()); builder.retryer(retryer); } if (config.getErrorDecoder() != null) { ErrorDecoder errorDecoder = getOrInstantiate(config.getErrorDecoder()); builder.errorDecoder(errorDecoder); } if (config.getRequestInterceptors() != null && !config.getRequestInterceptors().isEmpty()) { // this will add request interceptor to builder, not replace existing for (Class<RequestInterceptor> bean : config.getRequestInterceptors()) { RequestInterceptor interceptor = getOrInstantiate(bean); builder.requestInterceptor(interceptor); } } if (config.getDecode404() != null) { if (config.getDecode404()) { builder.decode404(); } } if (Objects.nonNull(config.getEncoder())) { builder.encoder(getOrInstantiate(config.getEncoder())); } if (Objects.nonNull(config.getDecoder())) { builder.decoder(getOrInstantiate(config.getDecoder())); } if (Objects.nonNull(config.getContract())) { builder.contract(getOrInstantiate(config.getContract())); } } private <T> T getOrInstantiate(Class<T> tClass) { try { return this.applicationContext.getBean(tClass); } catch (NoSuchBeanDefinitionException e) { return BeanUtils.instantiateClass(tClass); } } protected <T> T get(FeignContext context, Class<T> type) { T instance = context.getInstance(this.contextId, type); if (instance == null) { throw new IllegalStateException( "No bean found of type " + type + " for " + this.contextId); } return instance; } protected <T> T getOptional(FeignContext context, Class<T> type) { return context.getInstance(this.contextId, type); } protected <T> T loadBalance(Feign.Builder builder, FeignContext context, HardCodedTarget<T> target) { Client client = getOptional(context, Client.class); if (client != null) { builder.client(client); Targeter targeter = get(context, Targeter.class); return targeter.target(this, builder, context, target); } throw new IllegalStateException( "No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-netflix-ribbon?"); } @Override public Object getObject() throws Exception { // 当在Spring声明周期中,如果判断类型为FactoryBean的时候,则会调用该方法创建对象 return getTarget(); } /** * @param <T> the target type of the Feign client * @return a {@link Feign} client created with the specified data and the context * information */ <T> T getTarget() { // 获取FeignContext对象 FeignContext context = this.applicationContext.getBean(FeignContext.class); // 生成Feign.Builder对象 Feign.Builder builder = feign(context); // 判断当前url是否存在, 当url不存在时,则认为需要通过负载均衡的方式请求,也就是需要与ribbon整合 if (!StringUtils.hasText(this.url)) { if (!this.name.startsWith("http")) { this.url = "http://" + this.name; } else { this.url = this.name; } this.url += cleanPath(); // 返回LoadBalancer对象 return (T) loadBalance(builder, context, new HardCodedTarget<>(this.type, this.name, this.url)); } // 当url存在的时候, 则拼接http信息 if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) { this.url = "http://" + this.url; } String url = this.url + cleanPath(); Client client = getOptional(context, Client.class); if (client != null) { // 如果Client为ribbon支持时,也不使用ribbon if (client instanceof LoadBalancerFeignClient) { // not load balancing because we have a url, // but ribbon is on the classpath, so unwrap client = ((LoadBalancerFeignClient) client).getDelegate(); } builder.client(client); } Targeter targeter = get(context, Targeter.class); // 创建 return (T) targeter.target(this, builder, context, new HardCodedTarget<>(this.type, this.name, url)); } private String cleanPath() { String path = this.path.trim(); if (StringUtils.hasLength(path)) { if (!path.startsWith("/")) { path = "/" + path; } if (path.endsWith("/")) { path = path.substring(0, path.length() - 1); } } return path; } }
以上就是Feign的初始化过程,通过源码我们可以知道, Feign使用的JDK自身的代码方式进行代码,而spring feign只是通过集成的方式让我们能够更好的使用Feign的特性。