SpringCloud OpenFeign 源码分析

目录

前言

流程说明

源码分析

一、动态注册Bean

二、实例初始化

三、服务调用

实战演练

一、问题:

二、源码解析:

三、解决方案

总结

引用


前言

        为了更加符合企业级微服务定位及未来发展趋势的目标,我们需要对老技术框架的升级,比如老应用SpringBoot 1.x升级2.x,其中老的Neflix Feign也要升级成Spring Cloud OpenFeign,由于各系统框架的高低版本以及服务之间调用等存在一些兼容性问题,我对OpenFeign的源码进行了流程梳理总结,本文区别于传统的文章介绍,而是采用UML时序图+文字描述+代码描述+代码注释的形式,来对OpenFeign的完整生命周期做剖析,希望能对大家做类似框架升级中,遇到feign相关的问题提供更快的解决办法。

流程说明

  1. 服务启动类上添加@EnableFeignClients注解;

  2. 当程序启动时,会进行包扫描,扫描所有@FeignClients的注解的类,并且将这些信息注入Spring IOC容器中;

  3. 当定义的的Feign接口中的方法被实例化时,通过JDK动态代理方式从容器中获取到bean;

  4. 当服务被调用时,FeignClient 接口类的实例生成具体的RequestTemplate。当生成代理时,Feign会为每个接口方法创建一个RequestTemplate对象,该对象封装HTTP请求所需要的全部信息,如请求参数名,请求方法等信息都是在这个过程中确定的,然后RequestTemplate生成Request,然后把Request交给Client去处理,完成服务的调用。

源码分析

一、动态注册Bean

SpringCloud OpenFeign 源码分析_第1张图片

1.1、入口从启动类开始,引入了@EnableFeignClients注解,如下:

@Slf4j
@EnableFeignClients({"com.xxxx.xxxx.feign"})
@SpringBootApplication
@ImportResource(locations={"classpath:beanRefContext.xml"})
public class xxxxApplication {
	
	
    /**
     * 项目启动类
     *
     * @param args 启动参数
     */
    public static void main(String[] args) {
        BootApplication.run(log, false, xxxxApplication.class, args);
        String deployEnv = DeployEnvEnum.getDeployEnv();
        log.info("xxxxApplication start completed...  ");
    }
    
}

1.2、点进去,发现通过import注解导入了FeignClientsRegistrar配置类,代码如下:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({FeignClientsRegistrar.class})
public @interface EnableFeignClients {
    String[] value() default {};

    String[] basePackages() default {};

    Class[] basePackageClasses() default {};

    Class[] defaultConfiguration() default {};

    Class[] clients() default {};
}

1.3、FeignClientsRegistrar 点进去,这个类实现了ImportBeanDefinitionRegistrar接口,重写registerBeanDefinitions方法,代码如下:

class FeignClientsRegistrar
		implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {

	// patterned after Spring Integration IntegrationComponentScanRegistrar
	// and RibbonClientsConfigurationRegistgrar

	private ResourceLoader resourceLoader;

	private Environment environment;

    //省略代码...

	@Override
	public void registerBeanDefinitions(AnnotationMetadata metadata,
			BeanDefinitionRegistry registry) {
        //注册全局配置,解析EnableFeignClients注解上配置的defaultConfiguration属性
		registerDefaultConfiguration(metadata, registry);

        //扫描指定的所有包名下的被@FeignClient注解注释的接口,将扫描出来的接口调用registerFeignClient方法注册到spring容器
		registerFeignClients(metadata, registry);
	}

    //省略代码...

1.4、先执行registerDefaultConfiguration方法,如下:

private void registerDefaultConfiguration(AnnotationMetadata metadata,
			BeanDefinitionRegistry registry) {
        //解析EnableFeignClients属性
		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"));
		}
	}

1.5、然后再执行registerFeignClients方法,如下:

	public void registerFeignClients(AnnotationMetadata metadata,
			BeanDefinitionRegistry registry) {

		LinkedHashSet candidateComponents = new LinkedHashSet<>();

        // 扫描带有FeignClient注解的类
		Map attrs = metadata
				.getAnnotationAttributes(EnableFeignClients.class.getName());
		AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
				FeignClient.class);
        
        //获取@EnableFeignClients 中clients的值
		final Class[] clients = attrs == null ? null
				: (Class[]) attrs.get("clients");
		if (clients == null || clients.length == 0) {
			ClassPathScanningCandidateComponentProvider scanner = getScanner();
			scanner.setResourceLoader(this.resourceLoader);
			scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));
            // 如果没有设置,则扫描的包路径为 @EnableFeignClients 注解所在的包
			Set basePackages = getBasePackages(metadata);
			for (String basePackage : basePackages) {
				candidateComponents.addAll(scanner.findCandidateComponents(basePackage));
			}
		}
		else {
            //设置了则使用注解属性来进行扫描注册
			for (Class clazz : clients) {
				candidateComponents.add(new AnnotatedGenericBeanDefinition(clazz));
			}
		}

        //循环扫描注册
		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"));
                //注册 FeignClient
				registerFeignClient(registry, annotationMetadata, attributes);
			}
		}
	}

1.6、registerFeignClients方法到最后调用了registerFeignClient方法,registerFeignClient方法代码如下:

//注册 FeignClient,组装BeanDefinition,实质是一个FeignClientFactoryBean,然后注册到Spring IOC容器	
private void registerFeignClient(BeanDefinitionRegistry registry,
			AnnotationMetadata annotationMetadata, Map attributes) {
		String className = annotationMetadata.getClassName();
        构建FeignClientFactoryBean类型的BeanDefinitionBuilder
		BeanDefinitionBuilder definition = BeanDefinitionBuilder
				.genericBeanDefinition(FeignClientFactoryBean.class);
		validate(attributes);
        //将属性设置到 FeignClientFactoryBean中
		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();
		beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);

		// has a default, won't be null
		boolean primary = (Boolean) attributes.get("primary");

		beanDefinition.setPrimary(primary);

		String qualifier = getQualifier(attributes);
		if (StringUtils.hasText(qualifier)) {
			alias = qualifier;
		}
        //将BeanDefinition包装成BeanDefinitionHolder,用于注册
		BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
				new String[] { alias });
        //注册BeanDefinition
		BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
	}

        可以看到,这个方法就是将FeignClient注解上的属性信息,封装到BeanDefinition中,并注册到Spring容器中。但是在这个方法中,有一个关键信息,就是真实注册的FeignClientFactoryBean,它实现了FactoryBean接口,表明这是一个工厂bean,用于创建代理Bean,真正执行的逻辑是FactoryBean的getObject方法。至此,FeignClient的动态注册Bean就完成了。

二、实例初始化

       FeignClientFactoryBean 是工厂类, Spring 容器通过调用它的getObject 方法来获取对应的Bean 实例。被@FeignClient 修饰的接口类都是通过FeignClientFactoryBean 的getObject方法来进行实例化的。

SpringCloud OpenFeign 源码分析_第2张图片

2.1、先看方法getObject,如下:

	@Override
	public Object getObject() throws Exception {
		return getTarget();
	}

2.2、然后调用getTarget方法,代码如下:

    	 T getTarget() {
        //实例化Feign上下文对象FeignContext
		FeignContext context = applicationContext.getBean(FeignContext.class);
        //生成Builder对象,用来生成Feign
		Feign.Builder builder = feign(context);

        //如果url为空,则走负载均衡,生成有负载均衡功能的代理类
		if (!StringUtils.hasText(url)) {
			if (!name.startsWith("http")) {
				url = "http://" + name;
			}
			else {
				url = name;
			}
			url += cleanPath();
            //@FeignClient没有配置url属性,返回有负载均衡功能的代理对象
			return (T) loadBalance(builder, context,
					new HardCodedTarget<>(type, name, url));
		}
        //如果指定了url,则生成默认的代理类
		if (StringUtils.hasText(url) && !url.startsWith("http")) {
			url = "http://" + url;
		}
		String url = this.url + cleanPath();
		Client client = getOptional(context, Client.class);
		if (client != null) {
			if (client instanceof LoadBalancerFeignClient) {
				// not load balancing because we have a url,
				// but ribbon is on the classpath, so unwrap
				client = ((LoadBalancerFeignClient) client).getDelegate();
			}
			if (client instanceof FeignBlockingLoadBalancerClient) {
				// not load balancing because we have a url,
				// but Spring Cloud LoadBalancer is on the classpath, so unwrap
				client = ((FeignBlockingLoadBalancerClient) client).getDelegate();
			}
			builder.client(client);
		}
		Targeter targeter = get(context, Targeter.class);
        //生成默认代理类
		return (T) targeter.target(this, builder, context,
				new HardCodedTarget<>(type, name, url));
	}

2.3、getTarget方法最后调用targeter.target方法,Targeter.target方法代码如下:

interface Targeter {

	 T target(FeignClientFactoryBean factory, Feign.Builder feign,
			FeignContext context, Target.HardCodedTarget target);

}

其中target接口有两个实现类,如下截图:

d3c31d97995d418b81efc3b4dc940e15.png

2.4、我们没有使用Hystrix,走DefaultTargeter方法,代码如下:

class DefaultTargeter implements Targeter {

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

}

2.5、然后feign.target点进去,代码如下:

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

2.6、然后newInstance方法点进去,如下:

public abstract  T newInstance(Target var1);

2.7、然后调用ReflectiveFeign.newInstance,代码如下:

    public  T newInstance(Target target) {
        // 解析接口注解信息,根据接口类和Contract协议解析方式,解析接口类上的方法和注解,转换成内部的MethodHandler处理方式
        Map nameToHandler = this.targetToHandlersByName.apply(target);
        Map methodToHandler = new LinkedHashMap();
        List defaultMethodHandlers = new LinkedList();
        Method[] var5 = target.type().getMethods();
        int var6 = var5.length;

        for(int var7 = 0; var7 < var6; ++var7) {
            Method method = var5[var7];
            if (method.getDeclaringClass() != Object.class) {
                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 = this.factory.create(target, methodToHandler);
        // 基于Proxy.newProxyInstance 为接口类创建动态实现,将所有的请求转换给InvocationHandler 处理
        T proxy = Proxy.newProxyInstance(target.type().getClassLoader(), new Class[]{target.type()}, handler);
        Iterator var12 = defaultMethodHandlers.iterator();

        while(var12.hasNext()) {
            DefaultMethodHandler defaultMethodHandler = (DefaultMethodHandler)var12.next();
            defaultMethodHandler.bindTo(proxy);
        }

        return proxy;
    }

        最后到这里生成了动态代理,把接口中的方法和默认实现放到Map中,然后使用InvocationHandlerFactory.Default()创建InvocationHandler,最后使用jdk动态代理生成接口的代理并返回;

到这里二方服务定义的接口就被@EnableFeignClient通过动态代理的方式注入到了服务中;

三、服务调用

SpringCloud OpenFeign 源码分析_第3张图片

3.1、前面通过@Autowired或者@Resource注入的时候,注入的是被封装之后的代理类实现,jdk动态代理持有的是ReflectiveFeign.FeignInvocationHandler类型的InvocationHandler,那么具体调用的时候,会调用FeignInvocationHandler#invoke,代码如下:

        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            if (!"equals".equals(method.getName())) {
                if ("hashCode".equals(method.getName())) {
                    return this.hashCode();
                } else {
                    // 利用分发器筛选方法,找到对应的handler 进行处理
                    return "toString".equals(method.getName()) ? this.toString() : ((InvocationHandlerFactory.MethodHandler)this.dispatch.get(method)).invoke(args);
                }
            } else {
                try {
                    Object otherHandler = args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
                    return this.equals(otherHandler);
                } catch (IllegalArgumentException var5) {
                    return false;
                }
            }
        }

3.2、然后在invoke方法中,会调用 this.dispatch.get(method)).invoke(args) ,this.dispatch.get(method)会返回一个SynchronousMethodHandler,进行拦截处理,这个方法会根据参数生成完成的RequestTemplate对象,这个对象是Http请求的模版, 看SynchronousMethodHandler中的invoke类,代码如下:

    public Object invoke(Object[] argv) throws Throwable {
        //根据参数生成RequestTemplate对象
        RequestTemplate template = this.buildTemplateFromArgs.create(argv);
        Request.Options options = this.findOptions(argv);
        Retryer retryer = this.retryer.clone();

        while(true) {
            try {
                //通过RequestTemplate生成Request请求对象
                return this.executeAndDecode(template, options);
            } catch (RetryableException var9) {
                RetryableException e = var9;

                try {
                    retryer.continueOrPropagate(e);
                } catch (RetryableException var8) {
                    Throwable cause = var8.getCause();
                    if (this.propagationPolicy == ExceptionPropagationPolicy.UNWRAP && cause != null) {
                        throw cause;
                    }

                    throw var8;
                }

                if (this.logLevel != Level.NONE) {
                    this.logger.logRetry(this.metadata.configKey(), this.logLevel);
                }
            }
        }
    }

3.3、然后再看executeAndDecode()方法,该方法通过RequestTemplate生成Request请求对象,然后利用Http Client(默认)获取response,来获取响应信息,代码如下:

    Object executeAndDecode(RequestTemplate template, Request.Options options) throws Throwable {
        //转化为Http请求报文
        Request request = this.targetRequest(template);
        if (this.logLevel != Level.NONE) {
            this.logger.logRequest(this.metadata.configKey(), this.logLevel, request);
        }

        long start = System.nanoTime();

        Response response;
        try {
            //发起远程通信
            response = this.client.execute(request, options);
            //获取返回结果
            response = response.toBuilder().request(request).requestTemplate(template).build();
        } catch (IOException var13) {
            if (this.logLevel != Level.NONE) {
                this.logger.logIOException(this.metadata.configKey(), this.logLevel, var13, this.elapsedTime(start));
            }

            throw FeignException.errorExecuting(request, var13);
        }

        long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
        if (this.decoder != null) {
            return this.decoder.decode(response, this.metadata.returnType());
        } else {
            CompletableFuture resultFuture = new CompletableFuture();
            this.asyncResponseHandler.handleResponse(resultFuture, this.metadata.configKey(), response, this.metadata.returnType(), elapsedTime);

            try {
                if (!resultFuture.isDone()) {
                    throw new IllegalStateException("Response handling not done");
                } else {
                    return resultFuture.join();
                }
            } catch (CompletionException var12) {
                Throwable cause = var12.getCause();
                if (cause != null) {
                    throw cause;
                } else {
                    throw var12;
                }
            }
        }
    } 
  

到此,整个流程就结束了...

实战演练

例如下面的一个案例,是我们在升级框架过程中很容易遇到的一个典型问题

一、问题:

Description:

The bean 'xxxx.FeignClientSpecification' could not be registered. A bean with that name has already been defined and overriding is disabled.

Action:

Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true

分析原因:

由于多个接口定义了同样的名称(例如下面的两个接口"接口一"和"接口二"的name的属性一样),导致SpringBoot启动时就会报错:

//接口一
@FeignClient(name = "demo",url = "http://localhost:8086")
public interface ServerFeignClientOne {

    @GetMapping("/index")
    IndexResDto index(@RequestBody IndexReqDto indexReqDto);
}

//接口二
@FeignClient(name = "demo",url = "http://localhost:8086")
public interface ServerFeignClientTwo {

    @GetMapping("/index")
    IndexResDto index(@RequestBody IndexReqDto indexReqDto);
}

二、源码解析:

1、查看方法org.springframework.cloud.openfeign.FeignClientsRegistrar#registerFeignClients 代码如下:

    public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        ClassPathScanningCandidateComponentProvider scanner = this.getScanner();
        scanner.setResourceLoader(this.resourceLoader);
        Map attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());
        AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(FeignClient.class);
        Class[] clients = attrs == null ? null : (Class[])((Class[])attrs.get("clients"));
        Object basePackages;
        if (clients != null && clients.length != 0) {
            final Set clientClasses = new HashSet();
            basePackages = new HashSet();
            Class[] var9 = clients;
            int var10 = clients.length;

            for(int var11 = 0; var11 < var10; ++var11) {
                Class clazz = var9[var11];
                ((Set)basePackages).add(ClassUtils.getPackageName(clazz));
                clientClasses.add(clazz.getCanonicalName());
            }

            AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() {
                protected boolean match(ClassMetadata metadata) {
                    String cleaned = metadata.getClassName().replaceAll("\\$", ".");
                    return clientClasses.contains(cleaned);
                }
            };
            scanner.addIncludeFilter(new AllTypeFilter(Arrays.asList(filter, annotationTypeFilter)));
        } else {
            scanner.addIncludeFilter(annotationTypeFilter);
            basePackages = this.getBasePackages(metadata);
        }

        Iterator var17 = ((Set)basePackages).iterator();

        while(var17.hasNext()) {
            String basePackage = (String)var17.next();
            Set candidateComponents = scanner.findCandidateComponents(basePackage);
            Iterator var21 = candidateComponents.iterator();

            while(var21.hasNext()) {
                BeanDefinition candidateComponent = (BeanDefinition)var21.next();
                if (candidateComponent instanceof AnnotatedBeanDefinition) {
                    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 = this.getClientName(attributes);
                    //这段注册配置
                    this.registerClientConfiguration(registry, name, attributes.get("configuration"));
                    this.registerFeignClient(registry, annotationMetadata, attributes);
                }
            }
        }

    }

2、在看getClientName方法

//name的获取顺序是contextId->value->name->serviceId    
private String getClientName(Map client) {
        if (client == null) {
            return null;
        } else {
            String value = (String)client.get("contextId");
            if (!StringUtils.hasText(value)) {
                value = (String)client.get("value");
            }

            if (!StringUtils.hasText(value)) {
                value = (String)client.get("name");
            }

            if (!StringUtils.hasText(value)) {
                value = (String)client.get("serviceId");
            }

            if (StringUtils.hasText(value)) {
                return value;
            } else {
                throw new IllegalStateException("Either 'name' or 'value' must be provided in @" + FeignClient.class.getSimpleName());
            }
        }
    }

3、最后看报错的地方,这里说明一下,因为这里的name重名了,导致feignClient注册FeignClientSpecification到bean工厂的时候报错了,报错位置代码如下:

三、解决方案

在所有涉及到FeignClient注解的接口类加上contextId,保证唯一即可解决;

最后说明一下,如果看了上面的源码解析过程(参考标题序号1.5的内容),其实这个问题自然就很好解决了;

总结

1、FeignClientsRegistrar:

扫描@FeignClient注解的接口并注册成BeanDefinition到应用容器;

2、FeignClientFactoryBean:

注入远程服务接口代理实现;

3、FeignContext:

维护服务维度上下文,存储服务调用相关工具;

4、Feign.Builder:

用于构建服务调用的Client,维护服务调用相关组件(编解码器、重试组件、代理创建工厂等);

5、Targeter:

用于组装服务接口代理;

6、Feign:

用于创建服务接口代理;

7、ReflectiveFeign:

接口代理实现;

8、Client:

封装网络调用工具,并提供请求调用能力(Default、ApacheHttpClient和OkHttpClient等);

引用

一文看懂Openfeign服务调用原理_叔牙的博客-CSDN博客_openfeign调用原理

Spring Cloud OpenFeign源码解析 - 知乎

Spring Cloud——OpenFeign源码解析 - 简书

Spring Cloud OpenFeign源码分析_51CTO博客_spring-cloud-starter-openfeign

你可能感兴趣的:(Spring,Cloud,Java,spring,cloud,微服务,分布式,java,spring)