在前面我们分别学习 Spring Cloud Ribbon Spring 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;
    

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

    通过上面的代码我们知道我们使用的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了
  • 获取真正的请求地址
  • 发起请求并返回结果
  • 到这里我们这片关于Spring Cloud OpenFeign的文章就结束了,在这片文章中我们介绍了Spring Cloud OpenFeign的使用及原理。

  • 通过@EnableFeignClients注解导入FeignClientsRegistrar对象,当Spring容器启动时会调用这个类中的registerBeanDefinitions方法,在这里会将@FeignClient修饰的类进行注册。
  • 注册到Spring容器中的是一个FeignClientFactoryBean对象
  • FeignClientFactoryBean实现了FactoryBean,当我们使用FeignClient时,会调用到这个类中的getObject方法,在这里是通过动态代理创建一个代理对象
  • Spring Cloud OpenFeign集成了负载均衡器,发送请求前,会先通过负载均衡器选择出一个需要调用的实例
  • 欢迎关注公众号【Bug搬运小能手】持续更新Java相关知识

    大飞学习笔记 Spring Cloud
    私信