springconsulnetflix-feignspring-cloud-feignnetflix-ribbon

Feign with RibbonClient and Consul discovery without Spring Cloud


I was trying to setup Feign to work with RibbonClient, something like MyService api = Feign.builder().client(RibbonClient.create()).target(MyService.class, "https://myAppProd");, where myAppProd is an application which I can see in Consul. Now, if I use Spring annotations for the Feign client (@FeignClient("myAppProd"), @RequestMapping), everything works as Spring Cloud module will take care of everything.

If I want to use Feign.builder() and @RequestLine, I get the error: com.netflix.client.ClientException: Load balancer does not have available server for client: myAppProd.

My first initial thought was that Feign was built to work with Eureka and only Spring Cloud makes the integration with Consul, but I am unsure about this.

So, is there a way to make Feign work with Consul without Spring Cloud?

Thanks in advance.


Solution

  • In my opinion, it's not feign work with consul, its feign -> ribbon -> consul.

    RibbonClient needs to find myAppProd's serverList from its LoadBalancer. Without ServerList, error: 'does not have available server for client'.

    This job has been done by SpringCloudConsul and SpringCloudRibbon project, of course you can write another adaptor, it's just some glue code. IMHO, you can import this spring dependency into your project, but use it in non-spring way . Demo code:

    just write a new feign.ribbon.LBClientFactory, that generate LBClient with ConsulServerList(Spring's class).

    public class ConsulLBFactory implements LBClientFactory {
    
        private ConsulClient client;
        private ConsulDiscoveryProperties properties;
    
        public ConsulLBFactory(ConsulClient client, ConsulDiscoveryProperties consulDiscoveryProperties) {
            this.client = client;
            this.properties = consulDiscoveryProperties;
        }
    
        @Override
        public LBClient create(String clientName) {
            IClientConfig config =
                ClientFactory.getNamedConfig(clientName, DisableAutoRetriesByDefaultClientConfig.class);
    
            ConsulServerList consulServerList = new ConsulServerList(this.client, properties);
            consulServerList.initWithNiwsConfig(config);
    
            ZoneAwareLoadBalancer<ConsulServer> lb = new ZoneAwareLoadBalancer<>(config);
    
            lb.setServersList(consulServerList.getInitialListOfServers());
            lb.setServerListImpl(consulServerList);
            return LBClient.create(lb, config);
        }
    }
    

    and then use it in feign:

    public class Demo {
        public static void main(String[] args) {
            ConsulLBFactory consulLBFactory = new ConsulLBFactory(
                new ConsulClient(),
                new ConsulDiscoveryProperties(new InetUtils(new InetUtilsProperties()))
            );
    
            RibbonClient ribbonClient = RibbonClient.builder()
                .lbClientFactory(consulLBFactory)
                .build();
    
            GitHub github = Feign.builder()
                .client(ribbonClient)
                .decoder(new GsonDecoder())
                .target(GitHub.class, "https://api.github.com");
    
            List<Contributor> contributors = github.contributors("OpenFeign", "feign");
            for (Contributor contributor : contributors) {
                System.out.println(contributor.login + " (" + contributor.contributions + ")");
            }
        }
    
        interface GitHub {
            @RequestLine("GET /repos/{owner}/{repo}/contributors")
            List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo);
        }
    
        public static class Contributor {
            String login;
            int contributions;
        }
    }
    

    you can find this demo code here, add api.github.com to your local consul before running this demo.