SpringCloud(三):OpenFeign的使用及原理

3,336 阅读6分钟

在前面我们分别学习Spring Cloud RibbonSpring Cloud Eureka两个组件,今天我们学习Spring Cloud的第三个组件OpenFeign

今天的文章分为如下两部分:

  • Spring Cloud OpenFeign的使用
  • Spring Cloud OpenFeign的原理

文章中的示例代码可自行到github下载:Spring Cloud Sample

1 OpenFeign的使用

今天的代码我们还在之前的示例代码中进行修改,在product-service模块中加入如下依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

在product-service模块的启动类上添加@EnableFeignClients注解。

创建UserFeignClient类,内容如下:

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
​
@FeignClient(value = "user-service")
public interface UserFeignClient {
    @GetMapping(value = "/user/{name}")
    String getByName(@PathVariable String name);
}

在这个模块的启动类中注入UserFeignClient并创建一个接口,如下

@Resource
private UserFeignClient userFeignClient;
​
@GetMapping(value = "/product/user/{name}/feign")
public String getUserByNameFromFeign(@PathVariable String name) {
    return userFeignClient.getByName(name);
}

至此我们通过OpenFeign调用其他服务的示例就完成了,大家可以自行测试,效果会和之前的示例一致。

OpenFeign封装了http客户端,让我们像调用本地方法的方式调用远程服务。

2 OpenFeign的原理

通过上面的示例可以看出,我们使用OpenFeign可以很方便的调用一个http接口。那么OpenFeign是如何实现帮我们实现这些功能的呢?我们在下面内容中将通过查看源码的方式来学习下其工作原理。

使用OpenFeign我们需要在服务的启动类上添加@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;
​
@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 {};
​
}

这个注解上会通过@Import引入FeignClientsRegistrar,这个类实现了ImportBeanDefinitionRegistrar,在Spring容器启动时会加载这个类中的registerBeanDefinitions方法,这个方法的逻辑如下:

public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
    registerDefaultConfiguration(metadata, registry);
    // 注册feign客户端
    registerFeignClients(metadata, registry);
}

这个主要看registerFeignClients方法,其逻辑如下:

public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
​
    LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet<>();
    // 获取EnableFeignClients注解的属性
    Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());
    // clients属性中配置的类
    final Class<?>[] clients = attrs == null ? null : (Class<?>[]) attrs.get("clients");
    if (clients == null || clients.length == 0) {
        // 获取需要扫描包路径下有FeignClient注解的类
        ClassPathScanningCandidateComponentProvider scanner = getScanner();
        scanner.setResourceLoader(this.resourceLoader);
        scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));
        Set<String> 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();
            // FeignClient修饰的类必须是接口
            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);
            registerClientConfiguration(registry, name, attributes.get("configuration"));
            // 注册feignClient
            registerFeignClient(registry, annotationMetadata, attributes);
        }
    }
}

在上面这段代码中主要的逻辑就是解析出项目可扫描路径下被@FeignClient修饰的接口,然后调用registerFeignClient方法,注入到Spring容器中,其逻辑如下:

private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata,
                                 Map<String, Object> attributes) {
    String className = annotationMetadata.getClassName();
    Class clazz = ClassUtils.resolveClassName(className, null);
    ConfigurableBeanFactory beanFactory = registry instanceof ConfigurableBeanFactory
        ? (ConfigurableBeanFactory) registry : null;
    String contextId = getContextId(beanFactory, attributes);
    String name = getName(attributes);
    FeignClientFactoryBean factoryBean = new FeignClientFactoryBean();
    factoryBean.setBeanFactory(beanFactory);
    factoryBean.setName(name);
    factoryBean.setContextId(contextId);
    factoryBean.setType(clazz);
    factoryBean.setRefreshableClient(isClientRefreshEnabled());
    BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(clazz, () -> {
        factoryBean.setUrl(getUrl(beanFactory, attributes));
        factoryBean.setPath(getPath(beanFactory, attributes));
        factoryBean.setDecode404(Boolean.parseBoolean(String.valueOf(attributes.get("decode404"))));
        Object fallback = attributes.get("fallback");
        if (fallback != null) {
            factoryBean.setFallback(fallback instanceof Class ? (Class<?>) fallback
                                    : ClassUtils.resolveClassName(fallback.toString(), null));
        }
        Object fallbackFactory = attributes.get("fallbackFactory");
        if (fallbackFactory != null) {
            factoryBean.setFallbackFactory(fallbackFactory instanceof Class ? (Class<?>) fallbackFactory
                                           : ClassUtils.resolveClassName(fallbackFactory.toString(), null));
        }
        return factoryBean.getObject();
    });
    definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
    definition.setLazyInit(true);
    validate(attributes);
​
    AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
    beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);
    beanDefinition.setAttribute("feignClientsRegistrarFactoryBean", factoryBean);
​
    // has a default, won't be null
    boolean primary = (Boolean) attributes.get("primary");
​
    beanDefinition.setPrimary(primary);
​
    String[] qualifiers = getQualifiers(attributes);
    if (ObjectUtils.isEmpty(qualifiers)) {
        qualifiers = new String[] { contextId + "FeignClient" };
    }
​
    BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, qualifiers);
    BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
​
    registerOptionsBeanDefinition(registry, contextId);
}

上面这段代码看着很长,其实不用全部看,这段代码中我们可以看见向容器里注册的是一个FeignClientFactoryBean,当我们从容器中获取对应对象时,会调用这个类中的getObject方法,其逻辑如下:

public Object getObject() {
    return getTarget();
}
​
<T> T getTarget() {
    FeignContext context = beanFactory != null ? beanFactory.getBean(FeignContext.class)
        : applicationContext.getBean(FeignContext.class);
    Feign.Builder builder = feign(context);
​
    if (!StringUtils.hasText(url)) {
        if (url != null && LOG.isWarnEnabled()) {
            LOG.warn("The provided URL is empty. Will try picking an instance via load-balancing.");
        }
        else if (LOG.isDebugEnabled()) {
            LOG.debug("URL not provided. Will use LoadBalancer.");
        }
        if (!name.startsWith("http")) {
            url = "http://" + name;
        }
        else {
            url = name;
        }
        url += cleanPath();
        return (T) loadBalance(builder, context, new HardCodedTarget<>(type, name, 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 FeignBlockingLoadBalancerClient) {
            // not load balancing because we have a url,
            // but Spring Cloud LoadBalancer is on the classpath, so unwrap
            client = ((FeignBlockingLoadBalancerClient) client).getDelegate();
        }
        if (client instanceof RetryableFeignBlockingLoadBalancerClient) {
            // not load balancing because we have a url,
            // but Spring Cloud LoadBalancer is on the classpath, so unwrap
            client = ((RetryableFeignBlockingLoadBalancerClient) client).getDelegate();
        }
        builder.client(client);
    }
    Targeter targeter = get(context, Targeter.class);
    return (T) targeter.target(this, builder, context, new HardCodedTarget<>(type, name, url));
}

上面的这段代码中两个分支最后都会调用到ReflectiveFeign.newInstance方法,这个方法的逻辑如下:

public <T> T newInstance(Target<T> target) {
    Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
    // 方法和SynchronousMethodHandler对象Map
    Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
    List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();
​
    for (Method method : target.type().getMethods()) {
        // 这个方法是Object中的方法,不进行处理
        if (method.getDeclaringClass() == Object.class) {
            continue;
        // 是否为default方法
        } 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对象 ReflectiveFeign.FeignInvocationHandler
    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;
}

通过上面的代码我们看到,这里会返回一个通过动态代理创建的代理对象。这部分的调用时序图如下图所示。

image-20220429150626671

通过上面的代码我们知道我们使用的FeignClient对象是一个代理对象,当我们调用相应的方法时会调用到InvocationHandler.invoke方法中,所以会调用ReflectiveFeign.FeignInvocationHandler.invoke方法,这个方法的逻辑如下:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    // equals方法
    if ("equals".equals(method.getName())) {
        try {
            Object otherHandler =
                args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
            return equals(otherHandler);
        } catch (IllegalArgumentException e) {
            return false;
        }
    // hashCode方法
    } else if ("hashCode".equals(method.getName())) {
        return hashCode();
    // toString方法
    } else if ("toString".equals(method.getName())) {
        return toString();
    }
    // SynchronousMethodHandler中的invoke方法
    return dispatch.get(method).invoke(args);
}

再往下会调用到SynchronousMethodHandler.invokeSynchronousMethodHandler.executeAndDecodeFeignBlockingLoadBalancerClient.execute方法,前两个方法的代码这里就不进行粘贴了,第三个方法的逻辑如下:

public Response execute(Request request, Request.Options options) throws IOException {
    final URI originalUri = URI.create(request.url());
    // 获取调用的服务id
    String serviceId = originalUri.getHost();
    Assert.state(serviceId != null, "Request URI does not contain a valid hostname: " + originalUri);
    String hint = getHint(serviceId);
    DefaultRequest<RequestDataContext> lbRequest = new DefaultRequest<>(
        new RequestDataContext(buildRequestData(request), hint));
    Set<LoadBalancerLifecycle> supportedLifecycleProcessors = LoadBalancerLifecycleValidator
        .getSupportedLifecycleProcessors(
        loadBalancerClientFactory.getInstances(serviceId, LoadBalancerLifecycle.class),
        RequestDataContext.class, ResponseData.class, ServiceInstance.class);
    supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStart(lbRequest));
    // 通过负载均衡器选择出一个服务
    ServiceInstance instance = loadBalancerClient.choose(serviceId, lbRequest);
    org.springframework.cloud.client.loadbalancer.Response<ServiceInstance> lbResponse = new DefaultResponse(
        instance);
    // 未找到服务节点
    if (instance == null) {
        String message = "Load balancer does not contain an instance for the service " + serviceId;
        if (LOG.isWarnEnabled()) {
            LOG.warn(message);
        }
        supportedLifecycleProcessors.forEach(lifecycle -> lifecycle
                                             .onComplete(new CompletionContext<ResponseData, ServiceInstance, RequestDataContext>(
                                                 CompletionContext.Status.DISCARD, lbRequest, lbResponse)));
        return Response.builder().request(request).status(HttpStatus.SERVICE_UNAVAILABLE.value())
            .body(message, StandardCharsets.UTF_8).build();
    }
    // 真正的请求地址
    String reconstructedUrl = loadBalancerClient.reconstructURI(instance, originalUri).toString();
    Request newRequest = buildRequest(request, reconstructedUrl);
    return executeWithLoadBalancerLifecycleProcessing(delegate, options, newRequest, lbRequest, lbResponse,
                                                      supportedLifecycleProcessors);
}

再往下的逻辑就是调用Feign封装的http请求,这里就不进行粘贴了。这个方法的处理逻辑如下:

  • 通过负载均衡器选择出一个服务节点 我使用的SpringCloud版本比较高,这里的负载均衡器不是Ribbon了
  • 获取真正的请求地址
  • 发起请求并返回结果

3 总结

到这里我们这片关于Spring Cloud OpenFeign的文章就结束了,在这片文章中我们介绍了Spring Cloud OpenFeign的使用及原理。

  • 通过@EnableFeignClients注解导入FeignClientsRegistrar对象,当Spring容器启动时会调用这个类中的registerBeanDefinitions方法,在这里会将@FeignClient修饰的类进行注册。
  • 注册到Spring容器中的是一个FeignClientFactoryBean对象
  • FeignClientFactoryBean实现了FactoryBean,当我们使用FeignClient时,会调用到这个类中的getObject方法,在这里是通过动态代理创建一个代理对象
  • Spring Cloud OpenFeign集成了负载均衡器,发送请求前,会先通过负载均衡器选择出一个需要调用的实例

欢迎关注公众号【Bug搬运小能手】持续更新Java相关知识