Chapter 13. Discovering services

This chapter covers

  • Thinking in microservices
  • Creating a service registry
  • Registering and discovering services

Have you ever watched Finding Nemo? In that movie, Marlin (a clown fish) and Dory (a blue tang fish) are trying to get to Sydney, Australia to find Marlin’s missing son, Nemo. Along the way, they encounter a school of moonfish. For fun, the moonfish arrange themselves into several shapes—a swordfish, an octopus, and they even mock Marlin by arranging themselves to look like him. When Dory asks them if they know how to get to Sydney, they form the shape of the Sydney Opera House and then change into an arrow pointing toward the east Australian current.

Although the movie doesn’t delve into the lives of any particular moonfish, it can be assumed that each of them is an individual, independent of the other moonfish. Each has its own scales, fins, gills, eyes, internal organs, and (as far as we know) their own hopes and dreams. Even so, they still work together to form those fun shapes and help Marlin and Dory make their way to Australia.

This chapter is the first of a handful of chapters that discuss how to develop applications that are composed of moonfish. That is, you’ll see how to develop with microservices—small, independent applications that work together to provide the functionality of a complete application.

More specifically, you’re going to see how to use a few of the most useful components in the Spring Cloud portfolio, including configuration management, failure tolerance, and the subject of this chapter—service discovery. But before we get started, let’s take a quick, high-level look at what it means to develop with microservices and the benefits they offer.

13.1. Thinking in microservices

Up to this point, you’ve developed the Taco Cloud application as a single application that builds into a single, deployable JAR or WAR file. A single, deployable file seemed like a natural move. After all, that’s exactly how most applications have been built for decades. Even when the application is broken down into a multi-module build, you still end up with a single JAR or WAR file that you push into production.

This is certainly the most obvious way to build small, simple applications. But the funny thing about small applications is that they tend to grow. It’s easy enough to drop more code into the project when a new feature is needed. Before you know it, you have a complex, monolithic application that has a mind of its own. Like the Mogwai in the movie Gremlins, if you keep feeding it, it’ll eventually become a monster that turns against you.[1]

1

Yes, I realize that the timing of when you feed Mogwai—after midnight—was the real issue in the movie. No analogy is perfect.

Monolithic applications are deceptively simple, but they present a few challenges:

  • Monoliths are difficult to reason about The bigger the codebase gets, the harder it is to comprehend each component’s role in the whole application.
  • Monoliths are more difficult to test As the application grows, comprehensive integration and acceptance testing gets more complicated.
  • Monoliths are more prone to library conflicts One feature may require a dependency that’s incompatible with the dependency required by another.
  • Monoliths scale inefficiently If you need to deploy the application to more hardware for scaling purposes, you must deploy the entire application to more servers—even if it’s only a small fraction of the application that requires scaling.
  • Technology decisions for a monolith are made for the entire monolith When you choose a language, runtime platform, framework, or library for your application, you choose it for the entire application, even if the choice is made to only support a single use case.
  • Monoliths require a great deal of ceremony to get to production It would seem that when an application has only a single deployment unit, it would be easier to get into production. In reality, however, the size and complexity of monolithic applications generally require a more rigid development process and a more thorough testing cycle to ensure that what’s deployed is of high quality and doesn’t introduce bugs.

In the past few years, microservice architecture has risen to address these challenges. In simple terms, microservice architecture is a way of factoring an application into small-scale, miniature applications that are independently developed and deployed. These microservices coordinate with each other to provide the functionality of a greater application. In contrast to monolithic application architecture, microservice architecture has these traits:

  • Microservices can be easily understood Each microservice has a small, finite contract with other microservices in the greater application. As a result, microservices are more focused in purpose and, therefore, easier to understand as a unit.
  • Microservices are easier to test The smaller something is, the easier it is to test. This is certainly evident when you consider unit testing versus integration testing versus acceptance testing. That also applies when testing microservices versus monolithic applications.
  • Microservices are unlikely to suffer from library incompatibilities Because each microservice has its own set of build dependencies that isn’t shared with other microservices, it’s less likely that there’ll be library conflicts.
  • Microservices scale independently If any given microservice needs more horsepower, then the memory allotment and/or the number of instances can be scaled up without impacting the memory or instance count of other microservices in the greater application.
  • Technology choices can be made differently for each microservice Entirely different decisions can be made with regard to the language, platform, framework, and library choices for each microservice. In fact, it’s entirely reasonable for one microservice written in Java to coordinate with another microservice written in C#.[2]

    2

    We’ll focus on microservices written with Java and Spring. But if you’re interested in how to write microservices in .NET that work with Spring Cloud Services, have a look at Steeltoe (http://steeltoe.io/).

  • Microservices can be published to production more frequently Even though a microservice-architected application is made up of many microservices, each one can be deployed without requiring that any of the other microservices also be deployed. And because they’re smaller, more focused, and easier to test, there’s less ceremony in taking a microservice into production. The time between having an idea and seeing it in production can potentially be measured in minutes and hours, instead of weeks and months.

Microservices certainly seem to make things a lot easier. But to be fair, microservice architecture isn’t exactly a free lunch. Microservice architecture is a distributed architecture that brings its own challenges to the table, including network latency. As you make the move to microservice architecture, you should keep this in mind as many remote calls can add up and slow down an application.

You should also consider whether it even makes sense to architect your application in microservices. Not all applications require or benefit from such an architecture. If the application is relatively small or simple, perhaps it’s best to leave it as a monolith ... for now. As it grows, you can begin to consider breaking it into microservices.

There’s a lot of thought that goes into developing cloud-native, microservice-architected applications. This chapter and the next few chapters will focus primarily on the technology afforded by Spring Cloud to develop applications that are made up of microservices. But if you’re interested in digging deeper into the design and thought process around cloud-native applications, may I suggest that you read Cloud Native by Cornelia Davis (Manning, 2019, www.manning.com/books/cloud-native).

Another common challenge faced by microservice architecture is how each service even knows about the other services it coordinates with. That’s precisely the topic of this chapter. Without further delay, let’s see how to set up a service registry with Spring Cloud.

13.2. Setting up a service registry

Spring Cloud is a rather large umbrella project, made up of several separate subprojects that each enables microservice development in some way. One of those subprojects is Spring Cloud Netflix, which offers several components from the Netflix open source portfolio with a Spring twist. Among those components is Eureka, the Netflix service registry.

The naked truth concerning Eureka

The word Eureka is an exclamation of joy when one finds or discovers something. This makes it a fitting name for the service registries that will be used by microservices to discover each other.

Legend says that Eureka was first uttered by Greek physicist Archimedes when, on discovering the principal of buoyant force while sitting in a bath, he leapt out of the bath and ran home naked shouting “Eureka!”

There’s some debate whether or not Archimedes actually ran home in the buff shouting “Eureka!” But the story is amusing, nonetheless. Regardless, we’re able to work with the Eureka service registry fully-clothed.

Eureka acts as a central registry for all services in a microservice application. Eureka itself can be thought of as a microservice—one whose purpose in the greater application is to help the other services discover each other.

Because of its role in a microservice application, it’s probably best to set up a Eureka service registry before creating any of the services that register with it. To understand how Eureka works, consider the flow as described in figure 13.1.

Figure 13.1. Services register with the Eureka service registry so that other services can discover and consume them.

When a service instance starts, it’ll register itself by name with Eureka. In figure 13.1, the service name is some-service. There may be multiple equivalent instances of some-service, but all of them register with Eureka with the same name.

At some point, another service (named other-service in figure 13.1) needs to consume endpoints on some-service. Rather than hard coding other-service with specific host and port information for some-service, other-service only knows to look up some-service from Eureka by its name. Eureka replies with information for all instances of some-service that it knows about.

Now other-service needs to make a decision. Which instance of some-service will it use? If they’re all equivalent, then it doesn’t matter much. But to avoid any given instance being chosen every time, it’s best to apply some client-side, load-balancing algorithm to spread the requests around. That’s where another Netflix project—Ribbon—comes into play.

Although other-service could be solely responsible for both looking up and choosing an instance of some-service, it relies on Ribbon instead. Ribbon is a client-side load balancer that makes the choice on behalf of other-service. Once Ribbon has made its choice, all that’s left is for other-service to make requests to the instance that Ribbon chooses.

Why a client-side load balancer?

Often, load balancers are thought of as a single centralized service that handles all requests and distributes them across many instances of the intended target. In contrast, Ribbon is a client-side load balancer that’s local to each client making the requests.

As a client-side load balancer, Ribbon has several benefits over a centralized load balancer. Because there’s one load balancer local to each client, the load balancer naturally scales proportional to the number of clients. Furthermore, each load balancer can be configured to employ a load-balancing algorithm best suited for each client, rather than apply the same configuration for all services.

If this seems complex, don’t worry. As you’ll soon see, most of this is handled automatically and transparently. But before you can register and consume services, you need to enable a Eureka server.

To get started with Spring Cloud and Eureka, you’ll need to create a brand new project for the Eureka server itself. The easiest way to get started is with the Spring Initializr. Name the project whatever you want, although I’m inclined to name it service-registry. When it comes time to select starter dependencies, there’s only one dependency you’ll need: the one whose checkbox is labeled Eureka Server. After creating the new project, Initializr gives you a project whose pom.xml file contains the following dependency:

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

You’ll also see a property named spring-cloud.version and a <dependencyManagement> section in the pom.xml file that specifies the Spring Cloud release train version. When I created my service-registry, it referenced the first service release (SR1) of the Finchley release train:

<properties>
  ...
  <spring-cloud.version>Finchley.SR1</spring-cloud.version>
</properties>

...

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-dependencies</artifactId>
      <version>${spring-cloud.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

If you’d like to use a different version of Spring Cloud, you only need to change the spring-cloud.version property to the desired version.

With the Eureka starter dependency in the build, there’s only one other thing you must do to enable the Eureka server. Open the application’s main bootstrap class and annotate it with @EnableEurekaServer:

@SpringBootApplication
@EnableEurekaServer
public class ServiceRegistryApplication {

  public static void main(String[] args) {
    SpringApplication.run(ServiceRegistryApplication.class, args);
  }

}

And that’s it! If you start the application, you’ll have a Eureka service registry running and listening on port 8080. If you point your web browser to http://localhost:8080, you’ll see the web interface shown in figure 13.2.

Figure 13.2. The Eureka web dashboard

The Eureka dashboard is informative, telling you (among other things) what service instances are registered with Eureka. You’ll find yourself viewing this UI frequently as you register services to ensure that they’re registered as expected. At this point, no services are registered, thus the message No Instances Available.

Eureka also exposes a REST API, through which services register themselves and discover other services. You likely won’t use the REST API directly, but you might find the /eureka/apps endpoint interesting. It lists in detail all of the service instances in the registry. At this point, with no service instances registered, the response you’ll get follows. We’ll revisit this endpoint a little later in the chapter as you start registering services:

<applications>
  <versions__delta>1</versions__delta>
  <apps__hashcode></apps__hashcode>
</applications>

You may have noticed that Eureka logs several exceptions in its log every 30 s or so. Don’t worry! Eureka is running and will work as expected. But those exceptions are indicative of the fact that you’ve not fully configured the service registry yet. Let’s add a few configuration properties to make those exceptions disappear.

13.2.1. Configuring Eureka

Eureka doesn’t like working alone. Eureka believes in the concept of safety in numbers and wants to be part of a cluster of Eureka servers. If there’s more than one Eureka server, then there’s no single point of failure should one of those servers run into trouble. Therefore, Eureka’s default behavior is to attempt to make friends with other Eureka servers. It’ll try to fetch the service registry from other Eureka servers and even register itself as a service with other Eureka servers.

High availability of Eureka is desirable in a production setting. For development, however, it’s inconvenient and unnecessary to fire up more than one Eureka server. A single, lonely Eureka server is sufficient for development purposes. But unless you configure the Eureka server properly, it’ll whine about its loneliness incessantly, every 30 s, in the form of exceptions in the log file. That’s because every 30 s, Eureka is trying to get in touch with another Eureka server to register itself and to share registry information.

What you need to do is configure Eureka to accept a solitary existence. To do this, you’ll need to set a handful of configuration properties as shown in the following snippet from application.yml:

eureka:
  instance:
    hostname: localhost
  client:
    fetch-registry: false
    register-with-eureka: false
    service-url:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka

First, you set the eureka.instance.hostname property to localhost. This tells Eureka what host it’s running on. It’s optional, but if you don’t specify it, then Eureka attempts to determine its host from environment variables. Explicitly setting this property gives you more certainty of what its value will be.

The next two properties, eureka.client.fetch-registry and eureka.client.register-with-eureka, are properties that you might set on another microservice to tell them how they should interact with Eureka. But recall that Eureka is also a microservice, so these properties can be used with a Eureka server to tell it how it should interact with other Eureka servers.

The default value for both of these properties is true, indicating that Eureka should fetch the registry from other Eureka instances, and it should register itself as a service with the other Eureka servers. Because there aren’t other Eureka servers when in development mode, you’re setting them to false so that Eureka won’t try to reach out to other Eureka servers.

Finally, you set the eureka.client.service-url property. This property contains a map of zone names to one or more URLs of Eureka servers in that zone. The map key of defaultZone is a special zone name, which is the zone that should be used if the client (in this case, Eureka itself) hasn’t specified a desired zone. Because this is the only Eureka instance, the URL mapped to the default zone is for the Eureka server itself, using placeholder variables populated from other properties.

Specifying Eureka’s server port

Although it’s optional, you’ll probably want to override the default server port. Whereas Eureka is perfectly happy to listen on port 8080, you’ll run several applications (microservices) simultaneously on the local machine as you develop the code, and the applications can’t all listen on port 8080. Therefore, it’s generally a good idea to set the server.port property for local development purposes:

server:
  port: 8761

Here you’re setting it to port 8761, which is the default port that the Eureka client (which we’ll discuss in section 13.3) listens on.

Disabling self-perservation mode

One other property that you may consider setting is eureka.server.enable-self-preservation. If you ever start the Eureka server and let it sit idle for more than a minute or so, you may see a scary message in the Eureka UI that looks like figure 13.3.

Figure 13.3. When in self-preservation mode, Eureka displays this message in its dashboard.

In spite of the red lettering and all uppercase text, this message isn’t nearly as serious as it sounds. Eureka expects service instances to register themselves and to continue to send registration renewal requests every 30 s. Normally, if Eureka doesn’t receive a renewal from a service for three renewal periods (or 90 s), it deregisters that instance. In this case, Eureka assumes there’s a network problem, enters self-preservation mode, and won’t deregister service instances.

Self-preservation mode is actually a good thing in a production setting, preventing the deregistration of active services when some network hiccup has stopped the renewal request from making its way to Eureka. But it can be quite alarming when you’re first starting up and haven’t registered any services yet. You can disable self-preservation mode by setting the eureka.server.enable-self-preservation property to false:

eureka:
  ...
  server:
    enable-self-preservation: false

This property is useful in a development environment where it’s likely that for a number of reasons, Eureka may not receive renewal requests. In those cases where you may be bringing service instances up and down frequently, self-preservation mode results in registry entries for stopped services being retained, creating problems when another service tries to consume the service that’s long gone. Disabling self-preservation mode will prevent strange behavior like that. The tradeoff, however, is that you’ve traded one scary red-letter message for another (figure 13.4).

Figure 13.4. When self-preservation mode is disabled, you’ll get a different message reminding you that self-preservation mode is disabled.

Even if you decide to disable self-preservation mode for development, you should leave it enabled when you go into production.

13.2.2. Scaling Eureka

Even though a single Eureka instance is more convenient in development, you’ll probably want to have at least two Eureka instances for high-availability purposes when you take the application to production.

Production-ready Spring Cloud Services

There’s a lot to consider when deploying microservices into a production environment. High availability and security of the Eureka server are a few of the concerns that aren’t as important at development time, but are critical in production. If you’re a Pivotal Cloud Foundry or Pivotal Web Services customer, then you can let someone else worry about those things.

Spring Cloud Services offers a production-ready implementation of Eureka, as well as a configuration server and a circuit breaker dashboard. All you need to do is provision a p-service-registry service from the marketplace and then bind your microservices to that service. For the configuration server and the circuit breaker dashboard (which we’ll discuss in the next couple of chapters), the marketplace names are p-config-server and p-circuit-breaker-dashboard.

The easiest, most straightforward way to configure two (or more) Eureka instances is to use Spring profiles in the application.yml file and then start the Eureka server twice, once for each profile. For example, the configuration in the next listing shows how you might configure two Eureka servers that act as peers to each other.

Listing 13.1. Configuring Eureka for two peers using Spring profiles
eureka:
  client:
    service-url:
      defaultZone: http://${other.eureka.host}:${other.eureka.port}/eureka

---
spring:
  profiles: eureka-1
  application:
    name: eureka-1

server:
  port: 8761

eureka:
  instance:
    hostname: eureka1.tacocloud.com

other:
  eureka:
    host: eureka2.tacocloud.com
    port: 8761

---
spring:
  profiles: eureka-2
  application:
    name: eureka-2

server:
  port: 8762

eureka:
  instance:
    hostname: eureka2.tacocloud.com

other:
  eureka:
    host: eureka1.tacocloud.com
    port: 8762

In the default profile (at the top of listing 13.1), you set eureka.client.service-url.defaultZone to take advantage of placeholder variables that you set in each of the profile-specific configurations.

After the default profile, you configure two profiles, one named eureka-1 and the other named eureka-2. Each profile specifies its own port and eureka.instance.hostname for its own configuration needs. But then you set other.eureka.host and other.eureka.port, two contrived properties, in each profile to reference the other Eureka instance. There’s nothing about these properties that’s specific to the framework, but these properties are what get referenced by the placeholders in the default profile.

Notice that you’re not setting eureka.client.fetch-registry or eureka.client.register-with-eureka. The default value of true ensures that each Eureka server registers itself and fetches registry information from the other Eureka server.

Now you have a Eureka service registry up and running. But at this point, it’s essentially a phonebook with empty pages that nobody ever looks at. Until services start registering themselves in the service registry and other services look up those services and call them, it’s all for naught. Let’s see how to enable some microservices as Eureka clients.

13.3. Registering and discovering services

A Eureka service registry is useless unless services register themselves. If your services are going to be discovered and consumed by other services, then you need to enable them as clients of the service registry. To enable an application (any application, but presumably a microservice) as a service registry client, the least you must do is add the Eureka client dependency to the service application’s build:

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

As with the Eureka server starter dependency, you’ll also need the Spring Cloud version property set for Spring Cloud’s dependency management:

<properties>
  ...
  <spring-cloud.version>Finchley.SR1</spring-cloud.version>
</properties>

...

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-dependencies</artifactId>
      <version>${spring-cloud.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

You can add these entries to your service application’s pom.xml file manually, but the easier way to get them is to select the Eureka Discovery dependency from the Spring Initializr’s selection of checkboxes.

The Eureka client starter dependency adds all you need for discovering services via Eureka, including Eureka’s client-side library, as well as the Ribbon load balancer. By doing nothing more than adding this dependency, you’ll enable your application as a client of the Eureka service registry. When the application starts, it attempts to contact a Eureka server running locally and listening on port 8761 to register itself under the name UNKNOWN.

13.3.1. Configuring Eureka client properties

Although the default location of the Eureka server is fine for development purposes, you’ll most certainly want to override it once your services are deployed outside of localhost. What’s more, that default service name of UNKNOWN is a horrible choice ... but truthfully, any default choice would be equally bad, as all services would have the same name.

Changing the name under which the service is registered in Eureka is as easy as setting the spring.application.name property. For example, if you’re registering a service that handles all operations that involve dealing with taco ingredients, you might register it as ingredient-service. In application.yml that would look like this:

spring:
  application:
    name: ingredient-service

As a result of setting this property, the service can be looked up by the name ingredient-service. What’s more, if you were to fire up multiple instances of the ingredient service, they’d all appear under the same name, effectively scaling the service up to multiple, presumably equivalent, instances that a consuming service can choose from. When you view the Eureka dashboard, this service appears as shown in figure 13.5.

Figure 13.5. The ingredient service as it appears in Eureka’s dashboard

You’ll find as you continue to work with Spring Cloud that the spring.application.name property is one of the most important properties that you’ll ever set. It determines the registry name in Eureka. And in the next chapter, you’ll see that it identifies the application to the configuration service for managing application-specific configurations. Other Spring Cloud projects, such as Spring Cloud Task (ephemeral microservices) and Spring Cloud Sleuth (distributed tracing), also rely on the spring.application.name property to identify the service.

As you’ve learned from the first chapter, all Spring MVC and Spring WebFlux applications are listening on port 8080 by default. Because you’ll only be looking up services through Eureka, it doesn’t matter what port they’re listening on—Eureka knows what port they’re on. Therefore, to avoid potential port conflicts when running locally, you can set the port to 0:

server:
  port: 0
Note

Setting the port to 0 results in the application starting on a randomly chosen available port.

Now, regarding the location of the Eureka servers. By default, Eureka clients assume that Eureka is listening on localhost (port 8761). That’s great for development, but in production that most certainly isn’t the case. Therefore, you’ll need to specify the location of your Eureka server(s). That’s accomplished the same way you achieved it for the Eureka server itself, with the eureka.client.service-url property:

eureka:
 client:
   service-url:
     defaultZone: http://eureka1.tacocloud.com:8761/eureka/

This configures the client to register with the Eureka server listening on host eureka1.tacocloud.com (port 8761). That’s fine, so long as that Eureka server is in working order. But if that Eureka server is down for any reason, then the service fails to register. To avoid registration failure, it’s best to configure your service with two or more Eureka servers:

eureka:
 client:
   service-url:
     defaultZone: http://eureka1.tacocloud.com:8761/eureka/,
                  http://eureka2.tacocloud.com:8762/eureka/

When the service starts, it attempts to register with the first server in the zone. If that fails for any reason, then it attempts to register with the next one in the list. Eventually, when the failing Eureka comes back online, it replicates the registry from its peer, containing a registry entry for the service.

Registering a service in Eureka is only half of the story. Once services are registered in Eureka, other services can discover them and start consuming them. Let’s see how to consume services registered with Eureka.

13.3.2. Consuming services

It would be a mistake to hard code any service instance’s URL in the consumer’s code. This not only couples the consumer to a specific instance of the service, but also can cause the consumer to break if the service’s host and/or port were to change.

On the other hand, the consuming application has a lot of responsibility when it comes to looking up a service in Eureka. Eureka may reply to the lookup with many instances for the same service. If the consumer asks for ingredient-service and receives a half-dozen or so service instances in return, how does it choose the correct service?

The good news is that consuming applications don’t need to make that choice or even explicitly look up a service on their own. Spring Cloud’s Eureka client support, along with the Ribbon client-side load balancer, makes it simple work to look up, select, and consume a service instance. Two ways to consume a service looked up from Eureka include:

  • A load-balanced RestTemplate
  • Feign-generated client interfaces

Which you choose is largely a matter of personal taste. We’ll look at both options, starting with the load-balanced RestTemplate. Then you can choose which you like best.

Consuming services with RestTemplate

You got your first look at Spring’s RestTemplate client in chapter 7. As a quick reminder of how it works, once a RestTemplate has been created or injected, you can make an HTTP call and have the responses bound to domain types. For example, to perform an HTTP GET request to retrieve an ingredient by its ID, you could use the following RestTemplate code:

public Ingredient getIngredientById(String ingredientId) {
  return rest.getForObject("http://localhost:8080/ingredients/{id}",
                           Ingredient.class, ingredientId);
}

The only problem with this code is that the URL passed into getForObject() is hard-coded to a specific host and port. I suppose that you could extract that detail into a property, but if the request is destined for one of many instances of an ingredient service, then any URL you configure would target a specific instance; there’d be no load balancer in play to spread requests across the service instances.

Once you’ve enabled an application as being a Eureka client, however, you have the option of declaring a load-balanced RestTemplate bean. All you need to do is declare a regular RestTemplate bean, but annotate the @Bean method with @LoadBalanced:

@Bean
@LoadBalanced
public RestTemplate restTemplate() {
  return new RestTemplate();
}

The @LoadBalanced annotation has two purposes: first, and most importantly, it tells Spring Cloud that this RestTemplate should be instrumented with the ability to look up services through Ribbon. Secondly, it acts as an injection qualifier, so that if you have two or more RestTemplate beans, you can specify that you want the load-balanced RestTemplate at the injection point.

For example, suppose that you want to use the load-balanced RestTemplate to look up an ingredient as in the previous code. First, you’d inject the load-balanced RestTemplate into the bean that needs it:

@Component
public class IngredientServiceClient {

  private RestTemplate rest;

  public IngredientServiceClient(@LoadBalanced RestTemplate rest) {
    this.rest = rest;
  }

  ...

}

Then rewrite the getIngredientById() method slightly so that it uses the service’s registered name instead of an explicit host and port:

public Ingredient getIngredientById(String ingredientId) {
  return rest.getForObject(
              "http://ingredient-service/ingredients/{id}",
              Ingredient.class, ingredientId);
}

Did you notice the difference? The URL given to getForObject() doesn’t use any specific hostname or port. In place of the hostname and port, the service name ingredient-service is used. Internally, RestTemplate asks Ribbon to look up a service by that name and to select an instance. Ribbon, happy to oblige, rewrites the URL to include the host and port information for the chosen service instance and then lets RestTemplate proceed as usual.

As you can see, using a load-balanced RestTemplate isn’t that different from using a standard RestTemplate. The key difference is that the client code only needs to deal with service names instead of explicit hostnames and ports. But what if you’re using WebClient instead of RestTemplate? Can WebClient also be used along with Ribbon to consume services by name?

Consuming services with WebClient

In chapter 11, you saw how WebClient offers an HTTP client similar to RestTemplate, but that works with reactive types such as Flux and Mono. If you’ve been bit by the reactive programming bug, you might prefer to use WebClient instead of RestTemplate. The good news is that you can use WebClient as a load-balanced client in much the same way as you’ve seen RestTemplate used. The first thing to do is declare a WebClient.Builder bean method that’s annotated with @LoadBalanced:

@Bean
@LoadBalanced
public WebClient.Builder webClientBuilder() {
  return WebClient.builder();
}

With a WebClient.Builder bean declared, you can now inject the load-balanced WebClient.Builder into any bean that needs it. For example, you might inject it into the constructor of IngredientServiceClient:

@Component
public class IngredientServiceClient {

  private WebClient.Builder wcBuilder;

  public IngredientServiceClient(
        @LoadBalanced WebClient.Builder webclientBuilder wcBuilder) {
    this.wcBuilder = wcBuilder;
  }

  ...

}

Finally, when you’re ready to use it, you can use the WebClient.Builder to build a WebClient and then make requests using the service’s name as it’s registered in Eureka:

public Mono<Ingredient> getIngredientById(String ingredientId) {
  return wcBuilder.build()
    .get()
      .uri("http://ingredient-service/ingredients/{id}", ingredientId)
    .retrieve().bodyToMono(Ingredient.class);
}

As with the load-balanced RestTemplate, there’s no need to explicitly specify a host or port when making requests. The service name will be extracted from the given URL and used to look up a service from Eureka. Then Ribbon will select an instance of the service and the URL will be rewritten with the chosen instance’s host and port before making the request.

Although this programming model is easy to grasp, especially if you’re already familiar with RestTemplate or WebClient, Spring Cloud has another trick up its sleeve. Next, let’s take a look at how to use Feign to create interface-based service clients.

Defining Feign client interfaces

Feign is a REST client library that applies a unique, interface-driven approach to defining REST clients. Put simply, if you enjoy how Spring Data automatically implements repository interfaces, then you’re going to love Feign.

Feign was originally a Netflix project, but has since been turned loose as an independent open-source project called OpenFeign (https://github.com/OpenFeign). The word feign means “to pretend,” which you’ll soon see is an appropriate name for a project that pretends to be a REST client.

The first step to using Feign is to add the dependency to the project build. In pom.xml, the following <dependency> does the trick:

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

This same starter dependency can be added automatically by checking the Feign checkbox when using Spring Initializr. Unfortunately, there’s no autoconfiguration to enable Feign based on the existence of this dependency. Therefore, you’ll need to add the @EnableFeignClients annotation to one of the configuration classes:

@Configuration
@EnableFeignClients
public RestClientConfiguration {
}

Now comes the fun part. Let’s say that you want to write a client that fetches an Ingredient from the service that’s registered in Eureka as ingredient-service. The following interface is all you need:

package tacos.ingredientclient.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import tacos.ingredientclient.Ingredient;

@FeignClient("ingredient-service")
public interface IngredientClient {

  @GetMapping("/ingredients/{id}")
  Ingredient getIngredient(@PathVariable("id") String id);

}

It’s a simple interface, with no implementations. But at runtime, when Feign gets hold of it, none of that matters. Feign automatically creates an implementation and exposes it as a bean in the Spring application context.

Looking closer, you’ll see that there are a few annotations in play that make this come together. The @FeignClient annotation at the interface-level specifies that any methods you declare in this interface will make requests against the service whose name is ingredient-service. Internally, this service will be looked up via Ribbon, the same way as it worked for the load-balanced RestTemplate.

Then there’s the getIngredient() method, annotated with @RequestMapping. You’ll no doubt recognize @GetMapping from Spring MVC. Indeed, it’s the very same annotation! But this time, it’s on the client instead of the controller. It says that any calls to getIngredient() will result in a GET request to the /ingredients/{id} path at the host and port chosen by Ribbon. The @PathVariable annotation, also from Spring MVC, maps the method parameter to the placeholder in the given path.

All that’s left is to inject the Feign-implemented interface wherever it’s needed and start using it. For instance, to use it in a controller, you might do something like this:

@Controller
@RequestMapping("/ingredients")
public class IngredientController {

  private IngredientClient client;

  @Autowired
  public IngredientController(IngredientClient client) {
    this.client = client;
  }

  @GetMapping("/{id}")
  public String ingredientDetailPage(@PathVariable("id") String id,
                                     Model model) {
    model.addAttribute("ingredient", client.getIngredient(id));
    return "ingredientDetail";
  }
}

I don’t know about you, but I think that’s mighty slick! It’s hard to decide which I like best: the load-balanced RestTemplate, WebClient, or this magical Feign client interface. Whichever you choose, you can rest assured that your REST clients (no pun intended) will be able to consume services registered in Eureka by their name, without being hard-coded with any specific hostname or port.

For what it’s worth, Feign comes with its own set of annotations. @RequestLine and @Param are roughly analogous to Spring MVC’s @RequestMapping and @PathVariable, but their use is slightly different. It’s rather nice, though, to be able to use Spring MVC annotations on clients that are already familiar and, perhaps, identical to the ones you used when defining the service controllers.

Summary

  • Spring Cloud Netflix enables the simple creation of a Netflix Eureka service registry with autoconfiguration and the @EnableEurekaServer annotation.
  • Microservices register themselves by name with Eureka for discovery by other services.
  • On the client-side, Ribbon acts as a client-side load balancer, looking up services by name and selecting an instance.
  • Client code has the choice of either a RestTemplate that’s instrumented for Ribbon load balancing or defining its REST client code as interfaces that are implemented automatically at runtime by Feign.
  • In any event, client code is not hard-coded with the location of the services that it consumes.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset