欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

OpenFeign---常用功能及源码解析

程序员文章站 2024-02-14 13:00:40
...

先手总结一波:

  • Feign的源码实现过程是:

(1)首先通过@EnableFeignClients注解开启FeignClient。只有这个注解存在,才会在程序启动的时候开启@FeignClient注解的包扫描。
(2)根据Feign的规则实现接口,并在接口上面加上@FeignClient注解。
(3)程序启动之后,会进行包扫描,并扫描所有的@FeignClient的注解的类,并将这些信息注入的IOC容器中。
(4)当接口的方法被调用的时候,通过JDK的代理来生成具体的RequestTemplate模板对象。
(5)根据RequestTemplate再生成Http请求的Request对象。
(6)Request对象交给Client对象去处理,其中Client的网络请求框架可以是HttpURLConnection,HttpClient和OkHttp。
(7)最后Client被封装到LoadBalanceClient。该类结合了Ribbon做到了负载均衡。

1.1 FeignClient的配置

该配置是在

@FeignClient(configuration="XXXXX.class")

Feign Client默认的配置类是FeignClientsConfiguration,该类注入了很多的Feign的相关配置Bean,包括FeignReTryer(重试机制), FeignLoggerFactory和FormattingConversionService等。另外,Decoder,Encoder,Contract这三个类在没有Bean被注入的情况下,会自动注入默认配置的Bean,即ResponseEntityDecoder,SpringEncoder,SpringMvcContract。默认注入的配置:

  • Decoder feignDecoder: ResponseEntityDecoder\
  • Encoder feignEncoder: SpringEncoder
  • Logger feignLogger: Slf4jLogger
  • Contract feignContract:SpringMvcContract
  • Feign.Builder feignBuilder: HystrixFeign.Builder

源码:

@Configuration
public class FeignClientsConfiguration {

	@Autowired
	private ObjectFactory<HttpMessageConverters> messageConverters;

	@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 OptionalDecoder(
				new ResponseEntityDecoder(new SpringDecoder(this.messageConverters)));
	}

	@Bean
	@ConditionalOnMissingBean
	@ConditionalOnMissingClass("org.springframework.data.domain.Pageable")
	public Encoder feignEncoder() {
		return new SpringEncoder(this.messageConverters);
	}

	@Bean
	@ConditionalOnClass(name = "org.springframework.data.domain.Pageable")
	@ConditionalOnMissingBean
	public Encoder feignEncoderPageable() {
		return new PageableSpringEncoder(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 : this.feignFormatterRegistrars) {
			feignFormatterRegistrar.registerFormatters(conversionService);
		}
		return conversionService;
	}

	@Bean
	@ConditionalOnMissingBean
	public Retryer feignRetryer() {
		return Retryer.NEVER_RETRY;
	}

	@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(this.logger);
	}

	@Bean
	@ConditionalOnClass(name = "org.springframework.data.domain.Page")
	public Module pageJacksonModule() {
		return new PageJacksonModule();
	}

	@Configuration
	@ConditionalOnClass({ HystrixCommand.class, HystrixFeign.class })
	protected static class HystrixFeignConfiguration {

		@Bean
		@Scope("prototype")
		@ConditionalOnMissingBean
		@ConditionalOnProperty(name = "feign.hystrix.enabled")
		public Feign.Builder feignHystrixBuilder() {
			return HystrixFeign.builder();
		}

	}

}

通过上面的代码可以看到我们在decoder/encoder/contract中使用了@ConditionOnMissingBean

举例说明:
我们自己生成一个bean,自定义配置。该配置的作用是在调用服务失败的时候可以进行重试机制。

@Configuration
public class FeignConfig{
	@Bean
	public Retryer feignRetryer() {
		return new Retryer.Default(100, SECONDS.toMills(1), 5)
	}
}

上面大的代码更改了FeignClient的请求重试机制,最大间隔100ns, 最大重试1s,重试次数5次。

1.2 从源码的角度讲解Feign

  • Feign是一个伪Java Http客户端,Feign不做任何的请求处理。Feign通过处理注解生成Request模板,从而简化Http API的开发。开发人员可以使用注解的方式定制Request API模板。在发送Http Request请求之前,Feign通过注解的方式将请求拼接完成,用参数替换掉模板中的参数,生成真正的Request,并将其交给Java Http客户端处理。
    Feign通过扫描注入FeignClient的Bean,该源码在FeignClientsRegister类中。
    首先在启动的时候会检查是否有@EnableFeignClients注解,如果有该注解,则开启包扫描,扫描@FeignClient注解的接口
	private void registerDefaultConfiguration(AnnotationMetadata metadata,
			BeanDefinitionRegistry registry) {
		// 得到@FeignClients注解中的属性,包括value,basePackages,basePackageClasses,defaultConfiguration,clients
		Map<String, Object> defaultAttrs = metadata
				.getAnnotationAttributes(EnableFeignClients.class.getName(), true);

		if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) {
			// 得到@FeignClients注解在哪个类上
			String name;
			if (metadata.hasEnclosingClass()) {
				name = "default." + metadata.getEnclosingClassName();
			}
			else {
				name = "default." + metadata.getClassName();
			}
			registerClientConfiguration(registry, name,
					defaultAttrs.get("defaultConfiguration"));
		}
	}
  • 当程序的启动类上有@EnableFeignClients注解。在程序启动后,程序就会通过包扫描将所有的@FeignClient注解修饰的接口连同接口名和注解的信息一起取出,赋值给BeanDefinitionBuilder。然后根据此得到BeanDefinition,最后将BeanDefinition注入到IOC容器中,程序如下
	private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name,
			Object configuration) {
		BeanDefinitionBuilder builder = BeanDefinitionBuilder
				.genericBeanDefinition(FeignClientSpecification.class);
		builder.addConstructorArgValue(name);
		builder.addConstructorArgValue(configuration);
		registry.registerBeanDefinition(
				name + "." + FeignClientSpecification.class.getSimpleName(),
				builder.getBeanDefinition());
	}

	/**
	 * 开始处理@FeignClient包下的所有的注解信息
	 **/ 
	private void registerFeignClient(BeanDefinitionRegistry registry,
			AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
		// 得到的是@FeignClient的类名
		String className = annotationMetadata.getClassName();
		BeanDefinitionBuilder definition = BeanDefinitionBuilder
				.genericBeanDefinition(FeignClientFactoryBean.class);
		// 验证各个属性(即@FeignClient注解中的属性)
		validate(attributes);
		definition.addPropertyValue("url", getUrl(attributes));
		definition.addPropertyValue("path", getPath(attributes));
		String name = getName(attributes);
		definition.addPropertyValue("name", name);
		String contextId = getContextId(attributes);
		definition.addPropertyValue("contextId", contextId);
		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 = contextId + "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);
	}

  • 注入BeanDefinition之后,通过jdk的代理,当调用Feign Client接口里面的方法的时候,该方法会被拦截,源码如下:
  @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;
  }
  • 在SynchronousMethodHandler类进行拦截处理,即调用@FeignClient注解中的方法时具体执行的处理。该类会进行拦截处理。会根据参数生成完整的RequestTemplate对象,该对象是Http请求的模板,代码如下:
  @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) {
        try {
          retryer.continueOrPropagate(e);
        } catch (RetryableException th) {
          Throwable cause = th.getCause();
          if (propagationPolicy == UNWRAP && cause != null) {
            throw cause;
          } else {
            throw th;
          }
        }
        if (logLevel != Logger.Level.NONE) {
          logger.logRetry(metadata.configKey(), logLevel);
        }
        continue;
      }
    }
  }
  • 经过上述的代码,我们已经将restTemplate拼装完成,上面的代码中有一个==executeAndDecode()==方法,该方法通过RequestTemplate生成Request请求对象,然后利用Http Client获取response,来获取响应信息。
    代码:(这里只讲解最重要的一行代码)
response = client.execute(request, options);

注意: 这里的client对象是个重点,他负责发送之前方法一步一步拼装好的restTemplate生成的request请求,client在后面会详细讲解,该对象也是Feign实现负载均衡的关键

1.3 在Feign中使用HttpClient和OkHttp

在Feign中,Client是一个非常重要的组件(上面源码已经介绍到了Client,这里将详细介绍),Feign最终发送Request请求以及接受Response响应都是由Client组件完成的。Client在Feign中是一个接口,在默认的情况下,Client的实现类是Client.Default,Client.Default是由HttpURLConnection来实现网络请求的。另外,Client还支持HttpClient和OkHttp来进行网络请求的。

  • 首先查看FeignRibbonClient的自动配置类FeignRibbonClientAutoConfiguration,该类在工程启动的时候注入了一些Bean,其中BeanName是FeignClient的Client类型的Bean。代码如下
		@Bean
		@ConditionalOnMissingBean(Client.class)
		public Client feignClient(...) {
			return new LoadBalancerFeignClient(...);
		}

上面的类省略方法的参数和返回的参数,这里主要观察的是其返回的是一个loadBalancerFeignClient

  • 在缺省配置BeanName为FeignClient的Bean的情况下,会自动注入Client.Default这个对象,跟踪Client.Default源码,发现其使用的网络请求框架为HttpURLConnection,他是用HttpURLConnection的convertResponse方法得到response的。

  • 我们如何更改Feign中的网络框架为HttpClient呢?
    (1)在pom文件中加入HttpClient的依赖
    (2)在application.yml中配置feign.httpclient.enabled修改为true

  • 我们如何更改Feign中的网络框架为OkHttp呢?
    (1)只需要在pom文件中导入其相关依赖就行,其他的都不用改变

1.4 Feign是如何实现负载均衡的

FeignRibbonClientAutoConfiguration类配置了Client的类型(包括了HttpURLConnection,OkHttp和HttpClient),最终向容器注入的是Client的实现类LoadBalancerFeignClient,该类就是实现负载均衡的关键,即负载均衡客户端。

  • 查看LoadBalancerFeignClient类中的execute方法,即执行请求的方法,代码如下:
	@Override
	public Response execute(Request request, Request.Options options) throws IOException {
		try {
			URI asUri = URI.create(request.url());
			String clientName = asUri.getHost();
			URI uriWithoutHost = cleanUrl(request.url(), clientName);
			FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(
					this.delegate, request, uriWithoutHost);

			IClientConfig requestConfig = getClientConfig(options, clientName);
			return lbClient(clientName)
					.executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse();
		}
		catch (ClientException e) {
			IOException io = findIOException(e);
			if (io != null) {
				throw io;
			}
			throw new RuntimeException(e);
		}
	}

  • 其中executeWithLoadBalancer方法,即通过负载均衡的方法执行网络的请求,代码如下:
    public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException {
        LoadBalancerCommand<T> command = buildLoadBalancerCommand(request, requestConfig);

        try {
            return command.submit(
                new ServerOperation<T>() {
                    @Override
                    public Observable<T> call(Server server) {
                        URI finalUri = reconstructURIWithServer(server, request.getUri());
                        S requestForServer = (S) request.replaceUri(finalUri);
                        try {
                            return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig));
                        } 
                        catch (Exception e) {
                            return Observable.error(e);
                        }
                    }
                })
                .toBlocking()
                .single();
        } catch (Exception e) {
            Throwable t = e.getCause();
            if (t instanceof ClientException) {
                throw (ClientException) t;
            } else {
                throw new ClientException(e);
            }
        }
        
    }

其submit方法是LoadBalancerCommand类中的方法,其submit方法中有一个selectServer的方法,该方法的作用就是选择服务器进行负载均衡的操作,代码如下:

    private Observable<Server> selectServer() {
        return Observable.create(new OnSubscribe<Server>() {
            @Override
            public void call(Subscriber<? super Server> next) {
                try {
                    Server server = loadBalancerContext.getServerFromLoadBalancer(loadBalancerURI, loadBalancerKey);   
                    next.onNext(server);
                    next.onCompleted();
                } catch (Exception e) {
                    next.onError(e);
                }
            }
        });
    }
    

从上面的代码可以看出,最终请求交给了LoadBalancerContext来进行处理,其会先选择一个合适的server进行返回,然后会用Feign的Client对象调用其request请求,得到响应。