Search code examples
javaspring-bootmicroservicesspring-cloudnetflix-eureka

How do I find a service registered with Eureka knowing its name – without tinkering with DiscoveryService?


How do I find a service registered with Eureka knowing its name? I was under the impression that once you include the @LoadBalanced annotation, the mapping is done behind the scenes for you, and all you need to do is to include the app's name.

Somewhere in a requesting microservice:

@Configuration
public class Config {
    @Bean
    @LoadBalanced // note this
    public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
        return restTemplateBuilder.build();
    }
}
@Service
@RequiredArgsConstructor
public class PlanetService {
    private final RestTemplate restTemplate;
    public Planet getPlanet(String name) {
        return restTemplate.getForObject("http://planet_ms/api/planets?name=" + name, Planet.class);
    }
}

Somewhere in a responding microservice:

@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class PlanetController {
    private final PlanetService planetService;
    @GetMapping("/planets")
    public Planet getPlanet(@RequestParam String name) {
        return planetService.getPlanet(name);
    }
}
server.port=8081
spring.application.name=planet_ms

Apparently, it's not the case (however, it seemed to me it's what happened in this video)

How do I ensure that a microservice name is successfully mapped to a host-port pair using @LoadBalanced? Do I really have to deal with a DiscoveryClient? If I decide to complicate my code and retrieve the host and the port directly, it works, but the idea of relying on that nice implicit mapping (and not doing anything manually) is so appealing. Surely, I can write a simple utility class and write the code just once (it's what I did indeed), but still, why didn't @LoadBalanced work?

// if I do something like this, it works

@Service
@RequiredArgsConstructor
public class PlanetService {
    private final RestTemplate restTemplate;
    private final DiscoveryClient discoveryClient;
    public Planet getPlanet(String name) {
        var planetMicroservice = discoveryClient.getInstances("planet_ms").get(0);
        String host = planetMicroservice.getHost();
        int port = planetMicroservice.getPort();
        return restTemplate.getForObject(format("http://%s:%d/api/planets?name=%s", host, port, name), Planet.class);
    }
}

UPD: I followed Ken Chan's advice and:

  1. replaced underscores with hyphens
# example
spring.application.name=planet-ms
  1. included a loadbalancer starter
<!-- pom -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
            <version>4.0.4</version>
        </dependency>
  1. included @LoadBalanced annotations at the @Bean method and before a respective constructor parameter
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
        return restTemplateBuilder.build();
    }
    public PlanetService(@LoadBalanced RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    public Planet getPlanet(String name) {
        return restTemplate.getForObject("http://planet-ms/api/planets?name=" + name,
                Planet.class);
    }

Result:

WARN 15492 --- [nio-8080-exec-1] o.s.c.l.core.RoundRobinLoadBalancer      : No servers available for service: host.docker.internal

I have no idea what Docker has to do with it. I didn't use it for this application. Microservices are running:

enter image description here


Solution

  • I accidentally discovered a fix to the problem. Let me group things into ones that don't matter and the ones that do

    Things that don't matter

    1. Putting a @LoadBalanced annotation on a field/parameter of type RestTemplate if you only have one bean of that type (like I did)
        public PlanetService(/* you don't need @LoadBalanced here, but you can put it if you want */ RestTemplate restTemplate) {
            this.restTemplate = restTemplate;
        }
    
    1. Having an explicit loadbalancer dependency (Eureka got it covered, look at the Compile Dependencies section here)
    <!-- you don't need it -->
    
    <!--        <dependency>-->
    <!--            <groupId>org.springframework.cloud</groupId>-->
    <!--            <artifactId>spring-cloud-starter-loadbalancer</artifactId>-->
    <!--            <version>4.0.4</version>-->
    <!--        </dependency>-->
    

    Things that do matter

    1. Avoiding low dashes. I was under the impression that URI standards don't apply to application names since they should be replaced with valid host-port pairs anyway, but no. planet-ms is fine, planet_ms is not
    {
      "error_message": "Request URI does not contain a valid hostname: http://planet_ms/api/planets?name=hoth"
    }
    
    1. (it's the fix I was referring to at the beginning!) Choosing a method of making requests to your microservices and sticking with it. See, I used the @LoadBalanced annotation to communicate with one microservice (planet-ms) and DiscoveryClient to communicate with another one (converter-ms). Like so:
    @Service
    @RequiredArgsConstructor
    public class WeightService {
        private final RestTemplate restTemplate;
        private final MicroserviceUtil mu;
        public Double getWeightInKilos(String weight) {
            String[] weightAndUnit = weight.split("(?<=\\d)(?=[a-z]+)");
            Double value = Double.valueOf(weightAndUnit[0]);
            String unit = weightAndUnit[1];
            if (unit.equals(WeightUnit.KILOS.toString())) {
                return value;
            } else if (unit.equals(WeightUnit.POUNDS.toString())) {
                return restTemplate.getForObject(String.format("http://%s/api/kilos?pounds=%s",
                        mu.getHostAndPort("converter-ms"), value), Double.class);
            }
            throw new IllegalArgumentException("Unsupported unit");
        }
    }
    
    @Component
    @RequiredArgsConstructor
    public class MicroserviceUtil {
        private final DiscoveryClient discoveryClient;
        public String getHostAndPort(String microserviceName) {
            var converterMicroservice = discoveryClient.getInstances(microserviceName).get(0); // no balancing needed since I have only one instance per microservice
            return converterMicroservice.getHost() + ":" + converterMicroservice.getPort();
        }
    }
    

    It led to this:

    WARN 2580 --- [nio-8080-exec-1] o.s.c.l.core.RoundRobinLoadBalancer      : No servers available for service: host.docker.internal
    

    If I replace this

                return restTemplate.getForObject(String.format("http://%s/api/kilos?pounds=%s",
                        mu.getHostAndPort("converter-ms"), value), Double.class);
    

    with this

                return restTemplate.getForObject("http://converter-ms/api/kilos?pounds=" + value,
                        Double.class);
    

    everything works fine