科普文:微服务之Spring Cloud OpenFeign服务调用调用过程分析

news2025/1/24 8:24:21

概叙

Feign和OpenFeign的关系

其实到现在,至少是2018年之后,我们说Feign,或者说OpenFeign其实是一个东西,就是OpenFeign,是2018.12 Netflix停止维护后,Spring cloud整合Netflix生态系统的延续。后面其实都很混淆Feign和OpenFeign,提到他们我知道是Spring cloud服务调用组件即可。

其实Feign有一堆自定义的注解,只是后来OpenFeign在其基础上做了简化,更加负荷springboot的约定大于配置大于编码的思想。

OpenFeign是什么

 Feign是一个声明式的Web服务客户端(Web服务客户端就是Http客户端),让编写Web服务客户端变得非常容易,只需创建一个接口并在接口上添加注解即可。

cloud官网介绍Feign:Spring Cloud OpenFeign 

OpenFeign能干什么

Java当中常见的Http客户端有很多,除了Feign,类似的还有Apache 的 HttpClient 以及OKHttp3,还有SpringBoot自带的RestTemplate这些都是Java当中常用的HTTP 请求工具。 

什么是Http客户端?

当我们自己的后端项目中 需要 调用别的项目的接口的时候,就需要通过Http客户端来调用。在实际开发当中经常会遇到这种场景,比如微服务之间调用,除了微服务之外,可能有时候会涉及到对接一些第三方接口也需要使用到 Http客户端 来调用 第三方接口。 

所有的客户端相比较,Feign更加简单一点,在Feign的实现下,我们只需创建一个接口并使用注解的方式来配置它(以前是Dao接口上面标注Mapper注解,现在是一个微服务接口上面标注一个Feign注解即可),即可完成对服务提供方的接口绑定。 

OpenFeign的特点和优势

  1. 声明式的调用:使用OpenFeign,开发人员只需要定义接口并添加注解,就可以轻松地调用HTTP API,而无需手动编写HTTP请求和处理响应的代码。
  2. 自动序列化和反序列化:OpenFeign可以自动处理请求和响应的序列化和反序列化,无需手动转换JSON数据。
  3. 请求和响应拦截器:OpenFeign提供了拦截器的支持,开发人员可以在请求和响应的过程中添加自定义的逻辑。
  4. 整合Netflix生态系统:OpenFeign可以与Netflix的其他库(如Hystrix和Ribbon)无缝集成,提供容错和负载均衡的功能。

OpenFeign的使用

 ① openFeign的依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
② 编写Feign客户端接口

/**
 * 调用商品服务的Feign客户端
 * @FeignClient需要设置调用的服务名
 */
@FeignClient("product-service")
public interface ProductServiceFeignClient{
 
    /**
     * 调用商品id查询
     * @param id
     * @return
     */
    @GetMapping("/product/{id}")
    ResponseEntity<Product> getProductById(@PathVariable Long id);
}
③ 启动类加注解扫描feign接口@EnableFeignClients(basePackages = "com.blb.orderservice.feign")

④ 调用服务时

@Autowired
private ProductServiceFeignClient productServiceFeignClient; 
 
//使用feign客户端调用服务
ResponseEntity<Product> entity = productServiceFeignClient.getProductById(order.getProductId());

Spring Cloud OpenFeign 调用过程分析

两个Spring Cloud 组件:

  • Eureka/Nacos:实现服务注册功能;
  • Ribbon:提供基于RestTemplate的HTTP客户端并且支持服务负载均衡功能。

通过这两个组件我们暂时可以完成服务注册和可配置负载均衡的服务调用。今天我们要学习的是OpenFeign,那么OpenFeign解决了什么问题呢?

相对于Eureka,Ribbon来说,OpenFeign的地位好像不是那么重要,OpenFeign是一个声明式的REST客户端,它的目的就是让REST调用更加简单。通过提供HTTP请求模板,让Ribbon请求的书写更加简单和便捷。另外,在OpenFeign中整合了Ribbon,从而不需要显式的声明Ribbon的jar包。

前面在使用Ribbon+RestTemplate时,利用RestTemplate对http请求的封装处理,形成了一套模版化的调用方法。但是在实际开发中,由于对服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以通常都会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用。

所以,OpenFeign在此基础上做了进一步封装,由他来帮助我们定义和实现依赖服务接口的定义。在Feign的实现下,我们只需创建一个接口并使用注解的方式来配置它(以前是Dao接口上面标注Mapper注解,现在是一个微服务接口上面标注一个Feign注解即可),即可完成对服务提供方的接口绑定,简化了使用Spring Cloud Ribbon时,自动封装服务调用客户端的开发量。

从openFeign的使用来分析其调用过程

1.引入依赖:另外,我们需要添加spring-cloud-dependencies

dependency> 
<groupId>org.springframework.cloud</groupId> 
<artifactId>spring-cloud-starter-openfeign</artifactId> 
</dependency> 
2.接下来,我们需要在主类中添加 @EnableFeignClients
 

3.再来看一下用Feign写的HTTP请求的格式:

 
@FeignClient(name= "eureka-client")
public interface HelloRemote {


    @RequestMapping(value = "/hello/{name}")
    String hello(@PathVariable(value = "name") String name);

    @PostMapping(value ="/add",produces = "application/json; charset=UTF-8")
    String addPerson(@RequestBody Person person);

    @GetMapping("/getPerson/{id}")
    String getPerson(@PathVariable("id") Integer id);

}

用FeignClient注解申明要调用的服务是哪个,该服务中的方法都有我们常见的Restful方式的API来声明,这种方式大家是不是感觉像是在写Restful接口一样。

note:

示例代码的正确打开方式:先启动服务端,然后启动一个client端,再次启动 feign-consumer,调用 feign-consumer中的接口即可。

还记得在Ribbon学习的时候使用RestTemplate发起HTTP请求的方式吗:

restTemplate.getForEntity("http://eureka-client/hello/" + name, String.class).getBody(); 

将整个的请求URL和参数都放在一起,虽然没有什么问题,总归不是那么优雅。使用Feign之后你可以使用Restful方式进行调用,写起来也会更加清晰。

4.Feign Client 也支持文件属性的配置:

feign:
  client:
    config:                                         
    # 默认为所有的feign client做配置(注意和上例github-client是同级的)
      default:                                      
        connectTimeout: 5000                        # 连接超时时间
        readTimeout: 5000                           # 读超时时间设置  

5 两个重要的注解:EnableFeignClients 和 FeignClient

EnableFeignClients

package org.springframework.cloud.openfeign;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.context.annotation.Import;

/**
 * Scans for interfaces that declare they are feign clients (via {@link 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 {

    //等价于basePackages属性,更简洁的方式
	String[] value() default {};
    //指定多个包名进行扫描
	String[] basePackages() default {};

    //指定多个类或接口的class,扫描时会在这些指定的类和接口所属的包进行扫描
	Class<?>[] basePackageClasses() default {};

	 //为所有的Feign Client设置默认配置类
	Class<?>[] defaultConfiguration() default {};

	 //指定用@FeignClient注释的类列表。如果该项配置不为空,则不会进行类路径扫描
	Class<?>[] clients() default {};
}

注释上说了该注解用于扫描 FeignClient 声明的类。我们用 FeignClient 注解去声明一个 Eureka 客户端,那么猜想这里应该是取到我们声明的Eureka client名称,然后去访问Eureka server获取服务提供者。

注 : 如果通过Java代码进行了配置,又通过配置文件进行了配置,则配置文件的中的Feign配置会覆盖Java代码的配置。

但也可以设置feign.client.defalult-to-properties=false,禁用掉feign配置文件的方式让Java配置生效。

注意到类头声明的 @Import 注解引用的 FeignClientsRegistrar 类,这个类的作用是在 EnableFeignClients 初始化的时候扫描该注解对应的配置。

FeignClient

package org.springframework.cloud.openfeign;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.core.annotation.AliasFor;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FeignClient {

    //指定Feign Client的名称,如果项目使用了 Ribbon,name属性会作为微服务的名称,用于服务发现
	@AliasFor("name")
	String value() default "";
	//用serviceId做服务发现已经被废弃,所以不推荐使用该配置
	@Deprecated
	String serviceId() default "";
	//指定Feign Client的serviceId,如果项目使用了 Ribbon,将使用serviceId用于服务发现,但上面可以看到serviceId做服务发现已经被废弃,所以也不推荐使用该配置
	@AliasFor("value")
	String name() default "";
	//为Feign Client 新增注解@Qualifier
	String qualifier() default "";
    //请求地址的绝对URL,或者解析的主机名
	String url() default "";
    //调用该feign client发生了常见的404错误时,是否调用decoder进行解码异常信息返回,否则抛出FeignException
	boolean decode404() default false;
     //Feign Client设置默认配置类
	Class<?>[] configuration() default {};
    //定义容错的处理类,当调用远程接口失败或超时时,会调用对应接口的容错逻辑,fallback 指定的类必须实现@FeignClient标记的接口。实现的法方法即对应接口的容错处理逻辑
	Class<?> fallback() default void.class;
    //工厂类,用于生成fallback 类示例,通过这个属性我们可以实现每个接口通用的容错逻辑,减少重复的代码
	Class<?> fallbackFactory() default void.class;
    //定义当前FeignClient的所有方法映射加统一前缀
	String path() default "";
    //是否将此Feign代理标记为一个Primary Bean,默认为ture
	boolean primary() default true;
}

同样在 FeignClientsRegistrar 类中也会去扫描 FeignClient 注解对应的配置信息。我们直接看 FeignClientsRegistrar 的逻辑:

FeignClientsRegistrar

class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar,
		ResourceLoaderAware, EnvironmentAware {

	// patterned after Spring Integration IntegrationComponentScanRegistrar
	// and RibbonClientsConfigurationRegistgrar

	private ResourceLoader resourceLoader;

	private Environment environment;

	public FeignClientsRegistrar() {
	}

	@Override
	public void setResourceLoader(ResourceLoader resourceLoader) {
		this.resourceLoader = resourceLoader;
	}

   //在这个重载的方法里面做了两件事情:
   //1.将EnableFeignClients注解对应的配置属性注入
   //2.将FeignClient注解对应的属性注入
	@Override
	public void registerBeanDefinitions(AnnotationMetadata metadata,
			BeanDefinitionRegistry registry) {
        //注入EnableFeignClients注解对应的配置属性
		registerDefaultConfiguration(metadata, registry);
       //注入FeignClient注解对应的属性
		registerFeignClients(metadata, registry);
	}

   /**
   * 拿到 EnableFeignClients注解 defaultConfiguration 字段的值
   * 然后进行注入
   *
   **/
	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"));
		}
	}

	public void registerFeignClients(AnnotationMetadata metadata,
								 BeanDefinitionRegistry registry) {
        // 获取ClassPath扫描器
        ClassPathScanningCandidateComponentProvider scanner = getScanner();
        // 为扫描器设置资源加载器
        scanner.setResourceLoader(this.resourceLoader);

        Set<String> basePackages;
        // 1. 从@EnableFeignClients注解中获取到配置的各个属性值
        Map<String, Object> attrs = metadata
                .getAnnotationAttributes(EnableFeignClients.class.getName());
        // 2. 注解类型过滤器,只过滤@FeignClient   
        AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
                FeignClient.class);
        // 3. 从1. 中的属性值中获取clients属性的值        
        final Class<?>[] clients = attrs == null ? null
                : (Class<?>[]) attrs.get("clients");
        if (clients == null || clients.length == 0) {
            // 扫描器设置过滤器且获取需要扫描的基础包集合
            scanner.addIncludeFilter(annotationTypeFilter);
            basePackages = getBasePackages(metadata);
        }else {
            // clients属性值不为null,则将其clazz路径转为包路径
            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)));
        }

        // 3. 扫描基础包,且满足过滤条件下的接口封装成BeanDefinition
        for (String basePackage : basePackages) {
            Set<BeanDefinition> candidateComponents = scanner
                    .findCandidateComponents(basePackage);
            // 遍历扫描到的bean定义        
            for (BeanDefinition candidateComponent : candidateComponents) {
                if (candidateComponent instanceof AnnotatedBeanDefinition) {
                    // 并校验扫描到的bean定义类是一个接口
                    AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
                    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());

                    String name = getClientName(attributes);
                    // 可以看到这里也注册了一个FeignClient的配置bean
                    registerClientConfiguration(registry, name,
                            attributes.get("configuration"));
                    // 注册bean定义到spring中
                    registerFeignClient(registry, annotationMetadata, attributes);
                }
            }
        }
	}

   /**
   * 注册bean
   **/
	private void registerFeignClient(BeanDefinitionRegistry registry,
       AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
        // 1.获取类名称,也就是本例中的FeignService接口
        String className = annotationMetadata.getClassName();

        // 2.BeanDefinitionBuilder的主要作用就是构建一个AbstractBeanDefinition
        // AbstractBeanDefinition类最终被构建成一个BeanDefinitionHolder
        // 然后注册到Spring中
        // 注意:beanDefinition类为FeignClientFactoryBean,故在Spring获取类的时候实际返回的是
        // FeignClientFactoryBean类
        BeanDefinitionBuilder definition = BeanDefinitionBuilder
            .genericBeanDefinition(FeignClientFactoryBean.class);
        validate(attributes);

        // 3.添加FeignClientFactoryBean的属性,
        // 这些属性也都是我们在@FeignClient中定义的属性
        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);

        // 4.设置别名 name就是我们在@FeignClient中定义的name属性
        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;
        }

        // 5.定义BeanDefinitionHolder,
        // 在本例中 名称为FeignService,类为FeignClientFactoryBean
        BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, new String[] { alias });
        BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
	}

	private void validate(Map<String, Object> attributes) {
		AnnotationAttributes annotation = AnnotationAttributes.fromMap(attributes);
		// This blows up if an aliased property is overspecified
		// FIXME annotation.getAliasedString("name", FeignClient.class, null);
		Assert.isTrue(
			!annotation.getClass("fallback").isInterface(),
			"Fallback class must implement the interface annotated by @FeignClient"
		);
		Assert.isTrue(
			!annotation.getClass("fallbackFactory").isInterface(),
			"Fallback factory must produce instances of fallback classes that implement the interface annotated by @FeignClient"
		);
	}

   ......
   ......
   ......
}

在这里做了两件事情:

  1. 将EnableFeignClients注解对应的配置属性注入;
  2. 将FeignClient注解对应的属性注入。

生成FeignClient对应的bean,注入到Spring 的IOC容器。

在registerFeignClient方法中构造了一个BeanDefinitionBuilder对象,BeanDefinitionBuilder的主要作用就是构建一个AbstractBeanDefinition,AbstractBeanDefinition类最终被构建成一个BeanDefinitionHolder 然后注册到Spring中。

beanDefinition类为FeignClientFactoryBean,故在Spring获取类的时候实际返回的是FeignClientFactoryBean类。

FeignClientFactoryBean作为一个实现了FactoryBean的工厂类,那么每次在Spring Context 创建实体类的时候会调用它的getObject()方法。

public Object getObject() throws Exception {
    FeignContext context = applicationContext.getBean(FeignContext.class);
    Feign.Builder builder = feign(context);

    if (!StringUtils.hasText(this.url)) {
        String url;
        if (!this.name.startsWith("http")) {
            url = "http://" + this.name;
        }
        else {
            url = this.name;
        }
        url += cleanPath();
        return loadBalance(builder, context, new HardCodedTarget<>(this.type, this.name, url));
    }
    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) {
        if (client instanceof LoadBalancerFeignClient) {
            // not lod 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 targeter.target(this, builder, context, new HardCodedTarget<>(
        this.type, this.name, url));
}

这里的getObject()其实就是将@FeinClient中设置value值进行组装起来,此时或许会有疑问,因为在配置FeignClientFactoryBean类时特意说过并没有将Configuration传过来,那么Configuration中的属性是如何配置的呢?看其第一句是:

eignContext context = applicationContext.getBean(FeignContext.class); 

从Spring容器中获取FeignContext.class的类,我们可以看下这个类是从哪加载的。点击该类查看被引用的地方,可以找到在FeignAutoConfiguration类中有声明bean:

@Configuration
@ConditionalOnClass(Feign.class)
@EnableConfigurationProperties({FeignClientProperties.class, FeignHttpClientProperties.class})
public class FeignAutoConfiguration {

	@Autowired(required = false)
	private List<FeignClientSpecification> configurations = new ArrayList<>();

	@Bean
	public HasFeatures feignFeature() {
		return HasFeatures.namedFeature("Feign", Feign.class);
	}

	@Bean
	public FeignContext feignContext() {
		FeignContext context = new FeignContext();
		context.setConfigurations(this.configurations);
		return context;
	}
    ......
    ......
    ......
        
}     

从上面的代码中可以看到在set属性的时候将 FeignClientSpecification 类型的类全部加入此类的属性中。还记得在上面分析registerFeignClients方法的时候里面有一行代码调用:registerClientConfiguration()方法:

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());
}

在注册BeanDefinition的时候, configuration 其实也被作为参数,传给了 FeignClientSpecification。 所以这时候在FeignContext中是带着configuration配置信息的。

至此我们已经完成了配置属性的装配工作,那么是如何执行的呢?我们可以看getObject()最后一句可以看到返回了Targeter.target的方法。

return targeter.target(this, builder, context, new HardCodedTarget<>(this.type, this.name, url)); 

那么这个Targeter是哪来的?我们还是看上面的FeignAutoConfiguration类,可以看到其中有两个Targeter类,一个是DefaultTargeter,一个是HystrixTargeter。当配置了feign.hystrix.enabled = true的时候,Spring容器中就会配置HystrixTargeter此类,如果为false那么Spring容器中配置的就是DefaultTargeter

我们以DefaultTargeter为例介绍一下接下来是如何通过创建代理对象的:

class DefaultTargeter implements Targeter {

	@Override
	public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context,
						Target.HardCodedTarget<T> target) {
		return feign.target(target);
	}
}

public static class Builder {

    public <T> T target(Target<T> target) {
      return build().newInstance(target);
    }

    public Feign build() {
      SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =
          new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,
                                               logLevel, decode404);
      ParseHandlersByName handlersByName =
          new ParseHandlersByName(contract, options, encoder, decoder,
                                  errorDecoder, synchronousMethodHandlerFactory);
      return new ReflectiveFeign(handlersByName, invocationHandlerFactory);
    }
 }

在target方法中有个参数:Feign.Builder:

public static class Builder {

    private final List<RequestInterceptor> requestInterceptors =
        new ArrayList<RequestInterceptor>();
    private Logger.Level logLevel = Logger.Level.NONE;
    private Contract contract = new Contract.Default();
    private Client client = new Client.Default(null, null);
    private Retryer retryer = new Retryer.Default();
    private Logger logger = new NoOpLogger();
    private Encoder encoder = new Encoder.Default();
    private Decoder decoder = new Decoder.Default();
    private ErrorDecoder errorDecoder = new ErrorDecoder.Default();
    private Options options = new Options();
    private InvocationHandlerFactory invocationHandlerFactory =
        new InvocationHandlerFactory.Default();
    private boolean decode404;
    
    ......
    ......
    ......
}

构建feign.builder时会向FeignContext获取配置的Encoder,Decoder等各种信息 。Builder中的参数来自于配置文件的 feign.client.config里面的属性。

查看ReflectiveFeign类中newInstance方法是返回一个代理对象:

public <T> T newInstance(Target<T> target) {
    //为每个方法创建一个SynchronousMethodHandler对象,并放在 Map 里面
    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)) {
            //如果是 default 方法,说明已经有实现了,用 DefaultHandler
            DefaultMethodHandler handler = new DefaultMethodHandler(method);
            defaultMethodHandlers.add(handler);
            methodToHandler.put(method, handler);
        } else {
            //否则就用上面的 SynchronousMethodHandler
            methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
        }
    }
    // 设置拦截器
    // 创建动态代理,factory 是 InvocationHandlerFactory.Default,创建出来的是 
    // ReflectiveFeign.FeignInvocationHanlder,也就是说后续对方法的调用都会进入到该对象的 inovke 方法
    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;
}

这个方法大概的逻辑是:

  1. 根据target,解析生成MethodHandler对象;
  2. MethodHandler对象进行分类整理,整理成两类:default 方法和 SynchronousMethodHandler 方法;
  3. 通过jdk动态代理生成代理对象,这里是最关键的地方;
  4. DefaultMethodHandler绑定到代理对象。

最终都是执行了SynchronousMethodHandler拦截器中的invoke方法:

@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;
      }
    }
  }

invoke方法方法首先生成 RequestTemplate 对象,应用 encoder,decoder 以及 retry 等配置,下面有一个死循环调用:executeAndDecode,从名字上看就是执行调用逻辑并对返回结果解析。

Object executeAndDecode(RequestTemplate template) throws Throwable {
    //根据  RequestTemplate生成Request对象
    Request request = targetRequest(template);

    if (logLevel != Logger.Level.NONE) {
        logger.logRequest(metadata.configKey(), logLevel, request);
    }

    Response response;
    long start = System.nanoTime();
    try {
        // 调用client对象的execute()方法执行http调用逻辑,
        //execute()内部可能设置request对象,也可能不设置,所以需要response.toBuilder().request(request).build();这一行代码
        response = client.execute(request, options);
        // ensure the request is set. TODO: remove in Feign 10
        response.toBuilder().request(request).build();
    } catch (IOException e) {
        if (logLevel != Logger.Level.NONE) {
            logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start));
        }
        // IOException的时候,包装成 RetryableException异常,上面的while循环 catch里捕捉的就是这个异常
        throw errorExecuting(request, e);
    }
    //统计 执行调用花费的时间
    long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);

    boolean shouldClose = true;
    try {
        if (logLevel != Logger.Level.NONE) {
            response =
                logger.logAndRebufferResponse(metadata.configKey(), logLevel, response, elapsedTime);
            // ensure the request is set. TODO: remove in Feign 10
            response.toBuilder().request(request).build();
        }
        //如果元数据返回类型是 Response,直接返回回去即可,不需要decode()解码
        if (Response.class == metadata.returnType()) {
            if (response.body() == null) {
                return response;
            }
            if (response.body().length() == null ||
                response.body().length() > MAX_RESPONSE_BUFFER_SIZE) {
                shouldClose = false;
                return response;
            }
            // Ensure the response body is disconnected
            byte[] bodyData = Util.toByteArray(response.body().asInputStream());
            return response.toBuilder().body(bodyData).build();
        }
        //主要对2xx和404等进行解码,404需要特别的开关控制。其他情况,使用errorDecoder进行解码,以异常的方式返回
        if (response.status() >= 200 && response.status() < 300) {
            if (void.class == metadata.returnType()) {
                return null;
            } else {
                return decode(response);
            }
        } else if (decode404 && response.status() == 404 && void.class != metadata.returnType()) {
            return decode(response);
        } else {
            throw errorDecoder.decode(metadata.configKey(), response);
        }
    } catch (IOException e) {
        if (logLevel != Logger.Level.NONE) {
            logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime);
        }
        throw errorReading(request, response, e);
    } finally {
        if (shouldClose) {
            ensureClosed(response.body());
        }
    }
}

这里主要就是使用:client.execute(request, options) 来发起调用,下面基本都是处理返回结果的逻辑。到此我们的整个调用生态已经解析完毕。

我们可以整理一下上面的分析:

首先调用接口为什么会直接发送请求?

原因就是Spring扫描了@FeignClient注解,并且根据配置的信息生成代理类,调用的接口实际上调用的是生成的代理类。

其次请求是如何被Feign接管的?

  1. Feign通过扫描@EnableFeignClients注解中配置包路径,扫描@FeignClient注解并将注解配置的信息注入到Spring容器中,类型为FeignClientFactoryBean
  2. 然后通过FeignClientFactoryBeangetObject()方法得到不同动态代理的类并为每个方法创建一个SynchronousMethodHandler对象;
  3. 为每一个方法创建一个动态代理对象, 动态代理的实现是 ReflectiveFeign.FeignInvocationHanlder,代理被调用的时候,会根据当前调用的方法,转到对应的 SynchronousMethodHandler

这样我们发出的请求就能够被已经配置好各种参数的Feign handler进行处理,从而被Feign托管。

小结openFeign的调用过程

从上面使用过程来分析可知,OpenFeign 使用其自己封装远程服务调用的过程,它是由 OpenFeign 组件中的 FeignClient 注解所声明的接口。

这种封装其实是一种动态代理技术,在上面源码分析过程种可以看到反射 ReflectiveFeign.FeignInvocationHanlder和invoke。

远程服务调用的信息被写在了 FeignClient 接口中。在上面的代码里,可以看到,服务的名称、接口类型、访问路径已经通过注解做了声明。OpenFeign 通过解析这些注解标签生成一个动态代理类FeignClientFactoryBean,这个代理类会将接口调用转化为一个远程服务调用的 Request,并发送给目标服务。

科普文:微服务之Spring Cloud 服务调用组件Openfeign-CSDN博客

OpenFeign的动态代理

在项目初始化阶段,OpenFeign 会生成一个代理类,对所有通过FeignClient 接口发起的远程调用进行动态代理。如图:

在项目启动阶段加载完成的是 1 ~ 3步 ,只有第 4 步(调用远程服务)是发生在项目的运行阶段。
关键步骤描述:

  • 在项目启动阶段,OpenFeign 框架会发起一个主动的扫包流程,从指定的目录下扫描并加载所有被 @FeignClient 注解修饰的接口。
  • OpenFeign 会针对每一个 FeignClient 接口生成一个动态代理对象,即图中的 FeignProxyService,这个代理对象在继承关系上属于 FeignClient 注解所修饰的接口的实例。
  • 这个动态代理对象会被添加到 Spring 上下文中,并注入到对应的服务里,也就是图中的 LocalService 服务。
  • LocalService 会发起底层方法调用。实际上这个方法调用会被 OpenFeign 生成的代理对象接管,由代理对象发起一个远程服务调用,并将调用的结果返回给 LocalService。

OpenFeign 是如何通过动态代理技术创建代理对象的?

OpenFeign 组件加载过程的重要阶段。

OpenFeign 其实底层调用的是 Feign 的方法,生成了代理类,使用的是 JDK 的动态代理,然后 bean 注入。
调用过程,就是代理类作为客户端向被调用方发送请求,接收相应的过程。其中,feign 自行封装了 JDK java.net 相关的网络请求方法,请求过程中还有 Loadbalancer 进行负载均衡;收到响应后,还会对响应类进行解析,取出正确的响应信息。

1.项目加载:
在项目的启动阶段,EnableFeignClients 注解扮演了启动开关的角色,它使用 Spring 框架的 Import 注解导入了 FeignClientsRegistrar 类,开始了 OpenFeign 组件的加载过程。
2.扫包:
FeignClientsRegistrar 负责 FeignClient 接口的加载,它会在指定的包路径下扫描所有的 FeignClients 类,并构造 FeignClientFactoryBean 对象来解析 FeignClient 接口。
3.解析 FeignClient 注解:
FeignClientFactoryBean 有两个重要的功能,一个是解析 FeignClient 接口中的请求路径和降级函数的配置信息;另一个是触发动态代理的构造过程。
4.构建动态代理对象
ReflectiveFeign 包含了 OpenFeign 动态代理的核心逻辑,它主要负责创建出 FeignClient 接口的动态代理对象。ReflectiveFeign 在这个过程中有两个重要任务:一个是解析 FeignClient 接口上各个方法级别的注解。将其中的远程接口 URL、接口类型(GET、POST 等)、各个请求参数等封装成元数据,并为每一个方法生成一个对应的 MethodHandler 类作为方法级别的代理;另一个重要任务是将这些 MethodHandler 方法代理做进一步封装,通过 Java 标准的动态代理协议,构建一个实现了 InvocationHandler 接口的动态代理对象,并将这个动态代理对象绑定到 FeignClient 接口上。这样一来,所有发生在 FeignClient 接口上的调用,最终都会由它背后的动态代理对象来承接。
MethodHandler 的构建过程涉及到了复杂的元数据解析,OpenFeign 组件将 FeignClient 接口上的各种注解封装成元数据,并利用这些元数据把一个方法调用“翻译”成一个远程调用的 Request 请求。
元数据的解析是依赖于 OpenFeign 组件中的 Contract 协议解析功能。Contract 是 OpenFeign 组件中定义的顶层抽象接口,它有一系列的具体实现,其中 SpringMvcContract 这个类名字中就能看出来,它是专门用来解析 Spring MVC 标签的。
SpringMvcContract 的继承结构是 :SpringMvcContract->BaseContract->Contract。
这里拿一段 SpringMvcContract 的代码块,理解它是如何将注解解析为元数据的。这段代码的主要功能是解析 FeignClient 方法级别上定义的 Spring MVC 注解。

openfeign优化

上面说了那么多源码和调用过程,其实是为了更好的使用openFeign。

OpenFeign 是一个声明式的 HTTP 客户端,可以让我们用形式类似于调用本地方法的方式去调用远程服务。优化 OpenFeign 主要有以下几个方面:

  1. 使用 Hystrix 作为熔断器,减少服务雪崩效应。

  2. 使用 Feign 的 decoder 和 encoder 优化请求和响应。

  3. 使用 OkHttp 或者 Apache HttpClient 替换默认的 HTTP 客户端,提高请求效率。

  4. 开启日志功能,查看 Feign 的请求和响应详情。

  5. 开启懒加载:优化Feign第一次调用慢的问题

下面是一个使用 OpenFeign 的示例代码,并对其进行优化的过程:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1977937.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ComfyUI-BrushNet(局部重绘)节点安装及效果、模型下载及详细使用方法✨

&#x1f35c;背景介绍 ComfyUI 中BrushNet的节点已经发布了三个月左右的时间了&#xff0c;后来陆续更新了更多的功能和模型接入&#xff0c;整体效果看起来还是不错的。这个节点随着能力的更新&#xff0c;接入了更多的模型&#xff0c;而每个模型默认的名字又比较相似&…

RoboDK的插件

目录 collision-free-planner&#xff1a; opc-ua&#xff1a; collision-free-planner&#xff1a; RoboDK 的无碰撞规划器插件使用概率路线图 (PRM) 自动在机器人工作空间内创建无碰撞路径。 有关无碰撞规划器的更多信息&#xff0c;请访问我们的 文档。 生成参数无碰撞…

揭秘:查询大数据信用报告的三大作用

相信很多朋友都听说过大数据信用是什么&#xff0c;其实早在几年前&#xff0c;不少的网贷平台都是用人行征信加上大数据信用作为平台风控审核的依据&#xff0c;直到大数据技术的发展&#xff0c;现在不少的银行等机构都将大数据信用作为银行放贷风控审核的重要指标&#xff0…

趋势跟踪策略 文华财经指标公式源码 九稳量化系统 多空趋势指标神器 期货起爆点买入指标源码

斯坦利•克罗 1、赢利时是长线&#xff0c;亏损时就是短线。 2、50%回调位金字塔加码&#xff0c;“坐”着赚钱&#xff0c;甚至不惜用鸵鸟政策&#xff0c;眼不见心不烦。 3、阻碍长线操作成功的最主要原因是觉得单调乏味和失去纪律。 4、把自己的止损点设在远离绝大多数投…

detr论文解读

参考&#xff1a;https://www.bilibili.com/video/BV1md4y1s7nW/?spm_id_from333.788&vd_source156234c72054035c149dcb072202e6be 补充&#xff1a;decoder更关注边缘特征 补充&#xff1a; spatial pos.enc.&#xff1a;空间位置编码。包含encoder和decoder的空间位置…

Flutter 2024: Impeller引擎引领渲染新纪元

本文首发于公众号“AntDream”&#xff0c;欢迎微信搜索“AntDream”或扫描文章底部二维码关注&#xff0c;和我一起每天进步一点点 Flutter 2024: Impeller引擎引领渲染新纪元 在移动应用开发领域&#xff0c;Flutter凭借其跨平台能力、丰富的组件库和高性能的渲染引擎&#…

OpenStack概述

一、初识OpenStack OpenStack Docs: 概况 一&#xff09;OpenStack架构简述 1、理解OpenStack OpenStack既是一个社区&#xff0c;也是一个项目和一个开源软件&#xff0c;提供开放源码软件&#xff0c;建立公共和私有云&#xff0c;它提供了一个部署云的操作平台或工具集&…

Linux 内核源码分析---通用文件模型及 VFS 结构

通用文件模型 通常一个完整的 Linux 系统由数千到数百万个文件组成&#xff0c;文件中存储了程序、数据和各种信息。层次化的目录结构用于对文件进行编排和分组。 ReiserFS&#xff08;新型的文件系统&#xff09;–>Reiser4 它通过一种与众不同的方式—完全平衡树结构来容…

高并发下的分布式缓存 | 设计和实现LRU缓存

LRU缓存介绍 什么是LRU缓存&#xff1f; LRU缓存是一种缓存策略&#xff0c;当缓存满了&#xff0c;需要腾出空间存放新数据时&#xff0c;它会删除最近最少使用的数据。换句话说&#xff0c;它会优先淘汰那些最久没有被访问的元素&#xff0c;以确保缓存中的数据是最近使用的…

学习安卓开发遇到的问题

问题1&#xff1a;学习禁用与恢复按钮中&#xff1a; java代码报错&#xff1a;报错代码是 R.id.btn_enable;case R.id.btn_disable;case R.id.btn_test: 代码如下&#xff1a;&#xff08;实现功能在代码后面&#xff09; package com.example.apptest;import static java.…

04 表的操作

目录 创建查看修改删除 1. 创建 语法&#xff1a; CREATE TABLE table_name ( field1 datatype, field2 datatype, field3 datatype ) character set 字符集 collate 校验规则 engine 存储殷勤; 说明&#xff1a; field&#xff0c;表示列名 datatype&#xff0c;表示列的类型…

专题十_哈希表

目录 题型总结 1. 两数之和 解析 题解 面试题 01.02. 判定是否互为字符重排 解析 题解 217. 存在重复元素 解析 题解 219. 存在重复元素 II 解析 题解 49. 字母异位词分组 解析 题解 题型总结 1. 两数之和 1. 两数之和 解析 题解 class Solution { public:ve…

ECMAScript 12 (ES12, ES2021) 新特性

还是大剑师兰特&#xff1a;曾是美国某知名大学计算机专业研究生&#xff0c;现为航空航海领域高级前端工程师&#xff1b;CSDN知名博主&#xff0c;GIS领域优质创作者&#xff0c;深耕openlayers、leaflet、mapbox、cesium&#xff0c;canvas&#xff0c;webgl&#xff0c;ech…

Rewrite功能配置

Rewrite Rewrite是Nginx服务器提供的一个重要基本功能。主要的作用是用来实现URL的重写。 Nginx服务器的Rewrite功能的实现依赖于PCRE的支持&#xff0c;因此在编译安装Nginx服务器之前&#xff0c;需要安装PCRE库。 Nginx使用的是ngx_http_rewrite_module模块来解析和处理Re…

用VBA在Word中随机打乱单词表,进行分列

一、效果展示&#xff08;以下是三次随机打乱的结果&#xff09; 二、代码 Sub 随机分单词到后面的单元格()Dim C1 As CellDim str, str1, aDim shuffledArray() As VariantSet C1 Selection.Range.Tables(1).Cell(1, 1)str C1.Range.textstr mid(str, 3, Len(str) - 4)str…

微信小程序版NetAssist局域网工具使用

微信小程序搜《TCPUDP局域网小助手》即可使用&#xff0c;电脑端&#xff0c;安卓端&#xff0c;苹果端都可以使用

【前端面试】七、算法-递归、拷贝等

目录 1.常考算法 2.遍历方法 3.链式调用 4.递归 5.拷贝和比较 1.常考算法 排序算法&#xff1a;快速排序、归并排序、堆排序等。 查找算法&#xff1a;二分查找、哈希表查找等。 动态规划&#xff1a;解决最优化问题&#xff0c;如斐波那契数列、最长公共子序列等。 图…

8.3 day bug

bug1 文件名字写错了&#xff0c;找了半天bug原因 freecodecamp的致敬页的测试验证不通过bug 已经写了display: block;和max-width: 100% 以及通过margin: 0 auto;居中&#xff0c;可是却通不过验证 问了通义千问 通义帮我修改后的html代码为 <!DOCTYPE html> 2<h…

打扫朋友圈

我把上周写的一篇文章&#xff0c;发到了老家的一个群里&#xff0c;结果有个多年没联系的亲戚&#xff0c;立马私信给我说&#xff0c;让我不要在群里发&#xff0c;说我写的东西不行&#xff0c;他自己看了两行就看不下去了&#xff0c;然后给我讲了一堆大道理。 哎呦我去&a…

从零开始的CPP(34)——字符串乘法

给定两个以字符串形式表示的非负整数 num1 和 num2&#xff0c;返回 num1 和 num2 的乘积&#xff0c;它们的乘积也表示为字符串形式。 注意&#xff1a;不能使用任何内置的 BigInteger 库或直接将输入转换为整数。 示例 1: 输入: num1 "2", num2 "3" …