Spring Cloud Series

Subscribe to my newsletter to receive updates when content like this is published.

  1. Developing Microservices using Spring Boot, Jersey, Swagger and Docker
  2. Integration Testing using Spring Boot, Postgres and Docker
  3. Services registration and discovery using Spring Cloud Netflix Eureka Server and client-side load-balancing using Ribbon and Feign (you are here)
  4. Centralized and versioned configuration using Spring Cloud Config Server and Git
  5. Routing requests and dynamically refreshing routes using Spring Cloud Zuul Server
  6. Microservices Sidecar pattern implementation using Postgres, Spring Cloud Netflix and Docker
  7. Implementing Circuit Breaker using Hystrix, Dashboard using Spring Cloud Turbine Server (work in progress)

1. OVERVIEW

This blog post reviews an implementation pattern used in microservice architectural style: Service Registration and Discovery. The Discovery service and Configuration service, the latter discussed in this post, are most-likely the first infrastructure microservices needed when implementing such architecture.

Why is the Discovery service needed in a microservice architecture? Because if a running instance of service X needs to send requests to a running instance of service Y, it first needs to know the host and port service Y runs and listens on. One might think that service X may know this information via some static configuration file but this approach doesn’t sound very practical in an elastic-like distributed setup where services are dynamically (de)provisioned, triggered either manually or by some condition such as an auto-scaling policy or failure for instance.

Continuing with the Spring Cloud Series covered in this blog, this post discusses Spring Cloud Netflix’s Service Discovery component, a Spring-friendly and embedded Eureka server where services instances can register with and clients can discover such instances.

2. WORKFLOW WHEN USING SPRING CLOUD NETFLIX DISCOVERY SERVICE

  • One or more instances of the Discovery service need to be running.
  • Client services know the IP and port where the Eureka registry listen on (or find it via DNS) so that clients can advertise theirs via the Discovery service instances. This is actually named Discovery First Bootstrap, in this post, when I discuss the Configuration service, I’ll review another approach called Configuration First Bootstrap, where the client services figure out the Discovery service instances metadata by first querying a Configuration service instance.
  • Client services can also explicitly unregister from the Discovery service or the Discovery service can expire client service metadata if there hasn’t been a lease renew request for a period of time.
  • Client services don’t need to know the location about each other, if Service X needs to send a request to Service Y, Service X retrieves this information from the Discovery service.

This could summarized in the following figure: Service registration and discovery

3. REQUIREMENTS

  • Java 7+.
  • Maven 3.2+.
  • Familiarity with Spring Framework.
  • Docker host.

4. CREATE THE DISCOVERY SERVER

curl "https://start.spring.io/starter.tgz" -d bootVersion=1.4.1.RELEASE -d dependencies=cloud-eureka-server,web -d language=java -d type=maven-project -d baseDir=discoveryserver -d groupId=com.asimio.cloud -d artifactId=discovery-server -d version=0-SNAPSHOT | tar -xzvf -

This command will create a Maven project in a folder named discoveryserver with most of the dependencies used in the accompanying source code post for this post.

Alternatively, it can also be generated using Spring Initializr tool then selecting Eureka Server and Web dependencies as shown below:

Spring Initializr - Generate Eureka Server Spring Initializr - Generate Eureka Server

As usually done in this blog, the accompanying source code has been refactored to match the packages as described in this blog post.

The most relevant sections of Discovery Server’s pom.xml are shown below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
...
<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>1.4.1.RELEASE</version>
  <relativePath /> <!-- lookup parent from repository -->
</parent>
...
<properties>
...
  <start-class>com.asimio.cloud.eureka.EurekaServerApplication</start-class>
  <spring-cloud.version>Camden.SR2</spring-cloud.version>
<!--  <undertow.version>1.3.25.Final</undertow.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>
...
<dependencies>
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka-server</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
...
</dependencies>
...
<build>
...
  <plugins>
    <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
      <configuration>
        <requiresUnpack>
          <dependency>
            <groupId>com.netflix.eureka</groupId>
            <artifactId>eureka-core</artifactId>
          </dependency>
          <dependency>
            <groupId>com.netflix.eureka</groupId>
            <artifactId>eureka-client</artifactId>
          </dependency>
        </requiresUnpack>
      </configuration>
    </plugin>
...

This pom file inherits from Spring Boot and defines Spring Cloud dependencies management as a BOM. EurekaServerApplication class, described next, is the entry point to the Discovery service web application.
spring-cloud-starter-eureka-server and spring-boot-starter-web which also brings embedded Tomcat via spring-boot-starter-tomcat are the main dependencies included.

spring-boot-maven-plugin allows to package the application as a jar or war archive and also allows to run it from command line using Maven (as shown here) or Gradle. Visit the spring-boot-maven-plugin page for more information.
The plugin configuration for the Discover service artifact excludes a couple of Netflix dependencies that have caused issues in the past, they might have gotten figured out with the Camden.SR2 version though.

EurekaServerApplication.java, the entry point of this web application looks very simple:

package com.asimio.cloud.eureka;
...
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication extends SpringBootServletInitializer {

  @Override
  protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
    return builder.sources(EurekaServerApplication.class);
  }

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

@SpringBootApplication and SpringBootServletInitializer were explained in Developing Microservices using Spring Boot, Jersey, Swagger and Docker. The first is basically a composition of Spring annotations to scan packages, register beans and to automatically configure the application based on dependencies found in the classpath, thanks to Spring Boot favoring convention over configuration while the parent class allows to optionally build the application as a war file.

@EnableEurekaServer allows this application to run as a Eureka server, aided by Spring Boot to instantiate Eureka-related beans based on configuration properties.

The last remaining relevant piece of the Eureka registry web application would be its configuration properties, mainly included in bootstrap.yml and application.yml.

The following snippet comes from Discovery Service’s application.yml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
...
eureka:
  environment: ${environment}
  datacenter: ${dataCenter}
  enableSelfPreservation: false
  client:
    registerWithEureka: false
    fetchRegistry: false
    serviceUrl:
      defaultZone: ${peerUrls}
  instance:
    hostname: ${hostName}
    statusPageUrlPath: ${management.contextPath}/info
    healthCheckUrlPath: ${management.contextPath}/health
    preferIpAddress: true
    metadataMap:
      instanceId: ${spring.application.name}:${server.port}

---
spring:
  profiles: standalone
hostName: localhost
environment: local
dataCenter: ${hostName}
peerUrls: http://localhost:${server.port}/eureka/

---
spring:
  profiles: peerAware
# -DappPort=<port> -DhostName=<hostname> -DdataCenter=<data center> -e environment=<dev|test|staging|prod> -DpeerUrls=http://host1:port1/eureka/,http://host2:port2/eureka/,...

It includes two profiles in which this Spring web application could be run as: standalone or peerAware. The standalone profile would be useful for development purposes while the peerAware profile would be preferred for other environments. The latter is also missing some properties that would need to be passed via VM arguments when running the Discovery service.

These are some of the common Eureka configuration properties used (most-likely more config properties would be required in an AWS environment):

eureka.environment The environment name in which this Eureka cluster is running (dev, staging, prod).
eureka.datacenter The datacenter in which this instance is running.
eureka.enableSelfPreservation Specifies if this instance should enter Self Preservation Mode in case it cannot contact Eureka servers to refresh its local registry. If set to false, the registry would expire and attempts to locate other services will fail.
eureka.client.registerWithEureka Specifies if this instance should register its metadata with Eureka servers to be discovered by other clients.
eureka.client.fetchRegistry Specifies if this instance should request the Eureka registry from a Eureka server.
eureka.client.serviceUrl.defaultZone Specifies a single or comma-separated URL of alternative Eureka server locations.
eureka.instance.hostname The hostname where this instance is running or found out via the OS.
eureka.instance.statusPageUrlPath Specifies the relative status page URL for this instance.
eureka.instance.healthCheckUrlPath Specifies the relative server path to invoke for health checking.
eureka.instance.preferIpAddress Specifies using the IP address instead of the hostname during registration.
eureka.instance.metadataMap.instanceId Specifies the unique Id of the running instance (within the scope of this service) for registering with Eureka servers.

A more comprehensive list of properties could be found here.

5. RUNNING THE DISCOVERY SERVER

The Discovery server webapp could be run from command line using Maven, as in a development environment:

mvn spring-boot:run -Dspring.profiles.active=standalone

or as a self-contained, fat jar Java application:

mvn clean package
java -Dspring.profiles.active=standalone -jar target/discovery-server.jar

or using the peerAware Spring profile to start a Eureka cluster:

java -Dspring.profiles.active=peerAware -DappPort=8001 -DhostName=localhost -DdataCenter=local-dev -Denvironment=dev -DpeerUrls=http://localhost:8001/eureka/,http://localhost:8002/eureka/ -jar target/discovery-server.jar &
java -Dspring.profiles.active=peerAware -DappPort=8002 -DhostName=localhost -DdataCenter=local-dev -Denvironment=dev -DpeerUrls=http://localhost:8001/eureka/,http://localhost:8002/eureka/ -jar target/discovery-server.jar &

Notice that when peerAware is used, a few VM arguments are required that weren’t while using the standalone Spring profile, that’s because those where defined in the standalone profile as properties in application.yml.

Running the Eureka registry along with both Demo services using Docker will be covered later.

6. CREATE THE DEMO SERVICE 1

The first of the two microservice examples used in this demo is going to be very basic, it implements a couple of endpoints to retrieve a list of Actor and find and Actor by id using Jersey 1. It will also register with Eureka and will be discovered by Demo Service 2.

curl "https://start.spring.io/starter.tgz" -d bootVersion=1.4.1.RELEASE -d dependencies=actuator,web,cloud-eureka -d language=java -d type=maven-project -d baseDir=demo-registration-api-1 -d groupId=com.asimio.demo.api -d artifactId=demo-registration-api-1 -d version=0-SNAPSHOT | tar -xzvf -

This command will create a Maven project in a folder named demo-registration-api-1 with the dependencies required to implements the endpoints using Jersey and register the microservice with the Discovery service.

Alternatively, the Demo Service 1 can also be generated using Spring Initializr tool then selecting Eureka Discovery and Web dependencies as shown below:

Spring Initializr - Generate Demo Service 1 Spring Initializr - Generate Demo Service 1

Due to the previous limitation, Demo Service 1 will be built as a multi-module Maven project. One module for the parent pom, another module for the package with the classes that will need to be scanned by Jersey and the last module for the Spring Boot application and Docker image.

The relevant parts of Demo Service 1’s parent pom.xml are shown next:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
...
<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>1.4.1.RELEASE</version>
  <relativePath /> <!-- lookup parent from repository -->
</parent>
...
<properties>
...
  <start-class>com.asimio.api.demo1.main.Application</start-class>
  <spring-cloud.version>Camden.SR2</spring-cloud.version>
  <jersey1.version>1.19.1</jersey1.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>
...
<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
    <exclusions>
      <exclusion>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-logging</artifactId>
      </exclusion>
    </exclusions>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-log4j2</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-eureka</artifactId>
  </dependency>

  <dependency>
    <groupId>com.sun.jersey</groupId>
    <artifactId>jersey-servlet</artifactId>
<!--    <version>1.19.1</version> -->
  </dependency>
  <dependency>
    <groupId>com.sun.jersey</groupId>
    <artifactId>jersey-json</artifactId>
    <version>${jersey1.version}</version>
  </dependency>
  <dependency>
    <groupId>com.sun.jersey.contribs</groupId>
    <artifactId>jersey-spring</artifactId>
    <version>${jersey1.version}</version>
    <exclusions>
      <exclusion>
        <groupId>org.springframework</groupId>
        <artifactId>spring</artifactId>
      </exclusion>
      <exclusion>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
      </exclusion>
      <exclusion>
        <groupId>org.springframework</groupId>
        <artifactId>spring-web</artifactId>
      </exclusion>
      <exclusion>
        <groupId>org.springframework</groupId>
        <artifactId>spring-beans</artifactId>
      </exclusion>
      <exclusion>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
      </exclusion>
    </exclusions>
  </dependency>
...
<dependencies>
...
<modules>
  <module>../demo-registration-api-1-endpoints</module>
  <module>../demo-registration-api-1</module>
</modules>
...

Similarly to Discovery Server’s pom.xml, Demo Service 1 parent pom inherits from Spring Boot and defines Spring Cloud dependencies management as a BOM.

spring-boot-starter-actuator provides functionality allowing the application to be monitored and spring-boot-starter-web brings in Spring MVC dependencies used to expose actuator features via endpoints such us: /health, /info, /metrics, etc. More information about these endpoints could be found here.

spring-boot-starter-jersey won’t be used, read this note to learn why. Instead Jersey 1 dependencies will be included to implement the endpoints via JAX-RS standard.

spring-cloud-starter-eureka includes Eureka client dependencies to register this service with the Discovery server. These dependencies will be shared with the two Maven modules discussed next.

Demo Service 1’s endpoints are configured and implemented in a separate Maven module to deal with Jersey’s limitation when scanning nested jars. Here is an extract of its pom.xml:

...
<parent>
  <groupId>com.asimio.demo.api</groupId>
  <artifactId>demo-registration-api-1-ParentPOM</artifactId>
  <version>0-SNAPSHOT</version>
  <relativePath>../parentPOM/pom.xml</relativePath>
</parent>
...

It inherits from previous parent pom and and its declared dependencies. An endpoint implementation and resources configuration follows:

ActorResource.java:

package com.asimio.api.demo1.rest;
...
@Component
@Path("/actors")
@Produces(MediaType.APPLICATION_JSON)
public class ActorResource {

  @GET
  public List<Actor> findActors() {
...
  }

  @GET
  @Path("{id}")
  public Actor getActor(@PathParam("id") String id) {
...
  }
...
}

DemoResourcesConfig.java:

package com.asimio.api.demo1.config;
...
@ApplicationPath("/")
public class DemoResourcesConfig extends PackagesResourceConfig {

  private static final Map<String, Object> properties() {
    Map<String, Object> result = new HashMap<>();
    result.put(PackagesResourceConfig.PROPERTY_PACKAGES, "com.sun.jersey;com.asimio.api.demo1.rest");
    // To forward non-Jersey paths to servlet container for Spring Boot actuator endpoints to work.
    result.put("com.sun.jersey.config.feature.FilterForwardOn404", "true");
    result.put(JSONConfiguration.FEATURE_POJO_MAPPING, "true");
    return result;
  }

  public DemoResourcesConfig() {
    super(properties());
  }
...
}

ActorResource class implements the /actors and /actors/{id} resources. DemoResourcesConfig class defines the root path for the Jersey-based endpoints, configures the packages where the endpoints implementation will be scanned for, adds JSON support to serialize to and from POJOs and forwards requests not implemented using Jersey to servlet container allowing Spring’s dispatcher servlet to enable actuator endpoints.

Demo Service 1’s third and last Maven module, demo-registration-api-1, is used to package the application as a standalone Spring Boot application where Demo Service 1’s demo-registration-api-1-endpoints and Jersey dependencies are unpackaged at build time using spring-boot-maven-plugin, again, due to this Jersey limitation. Here is an extract of demo-registration-api-1’s pom.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
...
<parent>
  <groupId>com.asimio.demo.api</groupId>
  <artifactId>demo-registration-api-1-ParentPOM</artifactId>
  <version>0-SNAPSHOT</version>
  <relativePath>../parentPOM/pom.xml</relativePath>
</parent>
...
<dependencies>
  <dependency>
    <groupId>com.asimio.demo.api</groupId>
    <artifactId>demo-registration-api-1-endpoints</artifactId>
    <version>${project.version}</version>
  </dependency>
</dependencies>
...
<build>
...
  <plugins>
    <plugin>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-maven-plugin</artifactId>
      <configuration>
        <requiresUnpack>
          <dependency>
            <groupId>com.sun.jersey</groupId>
            <artifactId>jersey-client</artifactId>
          </dependency>
          <dependency>
            <groupId>com.sun.jersey</groupId>
            <artifactId>jersey-core</artifactId>
          </dependency>
          <dependency>
            <groupId>com.sun.jersey</groupId>
            <artifactId>jersey-json</artifactId>
          </dependency>
          <dependency>
            <groupId>com.sun.jersey</groupId>
            <artifactId>jersey-server</artifactId>
          </dependency>
          <dependency>
            <groupId>com.sun.jersey</groupId>
            <artifactId>jersey-servlet</artifactId>
          </dependency>
          <dependency>
            <groupId>com.sun.jersey.contribs</groupId>
            <artifactId>jersey-apache-client4</artifactId>
          </dependency>
          <dependency>
            <groupId>com.sun.jersey.contribs</groupId>
            <artifactId>jersey-spring</artifactId>
          </dependency>
          <dependency>
            <groupId>com.asimio.demo.api</groupId>
            <artifactId>demo-registration-api-1-endpoints</artifactId>
            <version>${project.version}</version>
          </dependency>
        </requiresUnpack>
      </configuration>
      <executions>
        <execution>
          <goals>
            <goal>repackage</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
...
  <plugins>
...
</build>
...

Demo Service 1’s Application.java, the entry point of this microservice looks like:

package com.asimio.api.demo1.main;
...
@SpringBootApplication
@EnableDiscoveryClient
public class Application extends SpringBootServletInitializer {

  @Override
  protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
    return builder.sources(Application.class);
  }

  @Bean
  public ServletContextInitializer servletInitializer() {
    return new ServletContextInitializer() {

      @Override
      public void onStartup(ServletContext servletContext) throws ServletException {
        final ServletRegistration.Dynamic appServlet = servletContext.addServlet("jersey-servlet", new SpringServlet());
        Map<String, String> filterParameters = new HashMap<>();
        // Set filter parameters
        filterParameters.put("javax.ws.rs.Application", "com.asimio.api.demo1.config.DemoResourcesConfig");
        appServlet.setInitParameters(filterParameters);
        appServlet.setLoadOnStartup(2);
        appServlet.addMapping("/*");
      }
    };
  }

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

@EnableDiscoveryClient causes this service to register with the Eureka registry using configuration properties defined in application.yml.
servletInitializer() creates a ServletContextInitializer bean configuring the Jersey servlet properties and mapping and declaring DemoResourcesConfig class (included in demo-registration-api-1-endpoints module) as the JAX-RS Application for this service.

The other important item of the Demo Service 1 application would be its configuration properties, primarily found in bootstrap.yml and application.yml:

This is an extract from Demo Service 1’s application.yml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
...
# For Spring MVC to enable Endpoints access (/admin/info, /admin/health, ...) along with Jersey
server.servlet-path: /admin

# http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#production-ready-endpoints
# http://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto-use-actuator-with-jersey
endpoints:
  enabled: false
  info:
    enabled: true
  health:
    enabled: true
  metrics:
    enabled: true
...
eureka:
  client:
    registerWithEureka: true
    fetchRegistry: true
    serviceUrl:
      defaultZone: http://localhost:8000/eureka/
  instance:
    hostname: ${hostName}
    statusPageUrlPath: ${server.servlet-path}/info
    healthCheckUrlPath: ${server.servlet-path}/health
    preferIpAddress: true
    metadataMap:
      instanceId: ${spring.application.name}:${server.port}
...

Lines 2 through 14 enables some actuator endpoints via Spring MVC without clashing with Jersey-backed APIs implementation. Lines 16 and on corresponds with service registration-related properties, a subset of what has been previously discussed in this table.

7. CREATE THE DEMO SERVICE 2

The second demo microservice example used in this blog post is also going to be very simple, its endpoints implementations uses Spring Cloud’s Feign and Ribbon to send requests to and aggregate results from Demo Service 1 APIs. It will also register with Netflix Eureka.

Demo Service 2’s pom.xml is similar to a combination of Demo Service 1 parent pom’s and , it replaces baseDir (while generating it using command line or Spring Initializr) and artifactId from demo-registration-api-1 to demo-registration-api-2. It won’t include Jersey though, endpoints implementation will be accomplished using Spring REST. A couple of new dependencies are also included:

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

Demo Service 2’s start class:

package com.asimio.api.demo2.main;
...
@SpringBootApplication(scanBasePackages = { "com.asimio.api.demo2.config", "com.asimio.api.demo2.rest" })
@EnableDiscoveryClient
@EnableFeignClients(basePackages = { "com.asimio.api.demo2.rest" })
public class Application extends SpringBootServletInitializer {

  @Bean(name = "restTemplate")
  RestTemplate restTemplate() {
    return new RestTemplate();
  }

  @Bean(name = "loadBalancedRestTemplate")
  @LoadBalanced
  RestTemplate loadBalancedRestTemplate() {
    return new RestTemplate();
  }
...
}

It adds support for this application to use Feign clients via @EnableFeignClients annotation and instantiates two RestTemplate beans used with Ribbon to load balance requests to Demo Service 1.

7.1. LOAD BALANCING REQUESTS USING SPRING CLOUD FEIGN

Spring Cloud Feign is a declarative Java http client that combines Ribbon and Eureka to load balance requests to registered services.

package com.asimio.api.demo2.rest;
...
@FeignClient(name = ActorsClient.ACTORS_SERVIDE_ID)
public interface ActorsClient {

  String ACTORS_SERVIDE_ID = "the-demo-registration-api-1";
  String ACTORS_ENDPOINT = "/actors";
  String ACTOR_BY_ID_ENDPOINT = "/actors/{id}";

  @RequestMapping(method = RequestMethod.GET, value = ACTORS_ENDPOINT)
  List<Actor> findActors();

  @RequestMapping(method = RequestMethod.GET, value = ACTOR_BY_ID_ENDPOINT)
  Actor getActor(@PathVariable("id") String id);
}

All is needed is an interface annotated with @FeignClient and methods with Spring MVC or JAX-RS annotations.
@FeignClient’s name field set to the-demo-registration-api-1, this is the same value used in Demo Service 2’s application.yml to configure the ActorsClient Feign client with a Ribbon and a Eureka client which refers to demo-registration-api-1 registered service.
This interface must be included in a package scanned by the application start class@EnableFeignClients annotation.

package com.asimio.api.demo2.rest;
...
@RestController
@RequestMapping(value = "/aggregation", produces = "application/json")
public class AggregationResource {
...
  // Uses Feign
  private ActorsClient actorsClient;
...
  @RequestMapping(value = "/actors", method = RequestMethod.GET)
  public List<Actor> findActors() {
    return this.actorsClient.findActors();
  }
...
  @Autowired
  public void setActorsClient(ActorsClient actorsClient) {
    this.actorsClient = actorsClient;
  }
...
}

This is the Spring REST resource using the Feign client to delegate requests to Demo Service 1’s /actors endpoint. Notice there is no implementation for ActorsClient interface, Spring Cloud Feign takes care of the Ribbon load balancer and Eureka client instantiation.

7.2. LOAD BALANCING REQUESTS USING RestTemplate AND @LoadBalanced

This section shows how to use a RestTemplate to load balance requests to an Eureka-registered service using Ribbon.

package com.asimio.api.demo2.rest;
...
@RestController
@RequestMapping(value = "/aggregation", produces = "application/json")
public class AggregationResource {
...
  // Uses Ribbon to load balance requests
  private RestTemplate loadBalancedRestTemplate;
...
  @RequestMapping(value = "/actors2/{id}", method = RequestMethod.GET)
  public Actor findActor2(@PathVariable(value = "id") String id) {
    String url = String.format("http://%s%s/{id}", ActorsClient.ACTORS_SERVIDE_ID, ActorsClient.ACTORS_ENDPOINT);
    return this.loadBalancedRestTemplate.getForObject(url, Actor.class, id);
  }
...
  @Autowired
  public void setLoadBalancedRestTemplate(RestTemplate loadBalancedRestTemplate) {
    this.loadBalancedRestTemplate = loadBalancedRestTemplate;
  }
...
}

loadBalancedRestTemplate bean was instantiated in Demo Service 2’s Application class, it’s configured with @LoadBalanced annotation, meaning it will get support from the load balancing features from Ribbon.
/actors2/{id} will send requests to http://the-demo-registration-api-1/actors/{id} where the-demo-registration-api-1 is the Ribbon client name configured in application.yml, that was mentioned in previous section, it refers to demo-registration-api-1 service registered with Eureka.

7.3. LOAD BALANCING REQUESTS USING RestTemplate AND LoadBalancerClient

The third and last section describing how to load balance requests to Eureka-registered services will use RestTemplate and Ribbon API directly.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.asimio.api.demo2.rest;
...
@RestController
@RequestMapping(value = "/aggregation", produces = "application/json")
public class AggregationResource {
...
  // Uses Ribbon to load balance requests
  private LoadBalancerClient loadBalancer;
  private RestTemplate restTemplate;
...
  @RequestMapping(value = "/actors1/{id}", method = RequestMethod.GET)
  public Actor findActor1(@PathVariable(value = "id") String id) {
    ServiceInstance instance = loadBalancer.choose(ActorsClient.ACTORS_SERVIDE_ID);
    URI uri = instance.getUri();
    String url = String.format("%s%s/{id}", uri, ActorsClient.ACTORS_ENDPOINT);
    return this.restTemplate.getForObject(url, Actor.class, id);
  }
...
  @Autowired
  public void setLoadBalancer(LoadBalancerClient loadBalancer) {
    this.loadBalancer = loadBalancer;
  }

  @Autowired
  public void setRestTemplate(RestTemplate restTemplate) {
    this.restTemplate = restTemplate;
  }
...
}

restTemplate beans was also instantiated in Demo Service 2’s Application class and LoadBalancerClient instance was autoconfigured by Spring Boot because spring-cloud-started-ribbon was found in the classpath.

/actors2/{id} will send requests to http://<demo-svc1-host>/actors/{id}. demo-svc1-host is calculated using Ribbon API as shown in lines 13 through 15 using the injected load balancer client instance. It translates the-demo-registration-api-1 configured client name found in application.yml to demo-registration-api-1 service name, then finds demo-registration-api-1 Eureka metadata for such registered service, which includes the host and port to send the request to.

A snippet of the Demo Service 2’s application.yml configuration file looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
...
# Spring dispatcher servlet (APIs) and actuator endpoint path (/admin/info, /admin/health, ...)
server.servlet-path: /
management.context-path: /admin
...
eureka:
  client:
    registerWithEureka: true
    fetchRegistry: true
    serviceUrl:
      defaultZone: http://localhost:8000/eureka/
  instance:
    hostname: ${hostName}
    statusPageUrlPath: ${management.context-path}/info
    healthCheckUrlPath: ${management.context-path}/health
    preferIpAddress: true
    metadataMap:
      instanceId: ${spring.application.name}:${server.port}
...
the-demo-registration-api-1:
  ribbon:
    # Eureka vipAddress of the target service
    DeploymentContextBasedVipAddresses: demo-registration-api-1

    #listOfServers: localhost:${SERVER.PORT}
    NIWSServerListClassName: com.netflix.niws.loadbalancer.DiscoveryEnabledNIWSServerList

    # Interval to refresh the server list from the source (ms)
    ServerListRefreshInterval: 30000
...

The Spring dispatcher servlet is mapped to / and resources are base off this value. The actuator endpoints’ base path is set to /admin.

This file assists configuring this service to register with Eureka and also fetches the Eureka registry to send requests to registered services. Refer to this table for more configuration options.

It also configures the Ribbon client used by Feign and Ribbon API as described in the previous three sections.

In lines 20 through 23 the-demo-registration-api-1 Ribbon client name maps to demo-registration-api-1 service name.

Line 26 configures metadata of demo-registration-api-1 instances will be found via service discovery but it could also be set to specific values as commented out in line 25. For more Ribbon client configuration options visit this page.

8. PACKAGE THE DISCOVERY SERVER AND DEMO SERVICES INTO DOCKER IMAGES

Packaging Discovery and both Demo services as Docker images is done via Spotify’s docker-maven-plugin.

Here is an extract of such configuration found in Discovery service’s pom.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<plugin>
  <groupId>com.spotify</groupId>
  <artifactId>docker-maven-plugin</artifactId>
  <version>0.4.10</version>
  <configuration>
    <!-- First add 'docker-hub' server entry in settings.xml -->
    <serverId>docker-hub</serverId>
    <imageName>asimio/${project.artifactId}:${project.version}</imageName>
    <!-- Pushes to Docker Hub -->
    <pushImage>true</pushImage>
    <forceTags>true</forceTags>
    <imageTags>
      <imageTag>${project.version}</imageTag>
      <imageTag>latest</imageTag>
    </imageTags>
    <!-- Dockerfile, scripts, .... location -->
    <dockerDirectory>src/main/docker</dockerDirectory>
    <resources>
      <resource>
        <targetPath>/</targetPath>
        <directory>${project.build.directory}</directory>
        <include>${project.build.finalName}.jar</include>
      </resource>
    </resources>
  </configuration>
</plugin>

And its Dockerfile:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
FROM azul/zulu-openjdk:8
MAINTAINER Orlando L Otero <ootero@asimio.net>, https://bitbucket.org/asimio/discoveryserver.git

VOLUME /tmp

ENV USER_NAME discoverysvc
ENV APP_HOME /opt/asimio-cloud/$USER_NAME
RUN \
  useradd -ms /bin/bash $USER_NAME && \
  mkdir -p $APP_HOME

ADD discovery-server.jar ${APP_HOME}/discovery-server.jar
RUN \
  chown $USER_NAME $APP_HOME/discovery-server.jar && \
  bash -c 'touch ${APP_HOME}/discovery-server.jar'

ENV JAVA_TOOL_OPTIONS "-Xms256M -Xmx256M -Djava.awt.headless=true -Djava.security.egd=file:/dev/./urandom"

USER $USER_NAME
WORKDIR $APP_HOME
ENTRYPOINT ["java", "-jar", "discovery-server.jar"]

# No need to EXPOSE port since it's using host's networking

# Run in 'peerAware' profile :
# docker run -idt -p 8001:8001 --net=host -e spring.profiles.active=peerAware -e appPort=8001 -e hostName=$HOSTNAME -e dataCenter=asimio-cloud-dal -e environment=staging -e peerUrls="http://$HOSTNAME:8002/eureka/" asimio/discovery-server:latest
# docker run -idt -p 8002:8002 --net=host -e spring.profiles.active=peerAware -e appPort=8002 -e hostName=$HOSTNAME -e dataCenter=asimio-cloud-dal -e environment=staging -e peerUrls="http://$HOSTNAME:8001/eureka/" asimio/discovery-server:latest

More details were already explained in the Docker-related section of Developing Microservices using Spring Boot, Jersey, Swagger and Docker.

Packaging both Demo services as Docker images is similar, the plugin configuration and Dockerfile could be found in their respective Bitbucket repositories.

9. RUNNING THE DISCOVERY SERVER AND DEMO SERVICES IN DOCKER CONTAINERS

Lets start two instances of the Discovery service, two instances of Demo Service 1 and two instances of Demo Service 2:

Discovery Server using peerAware Spring profile

docker pull asimio/discovery-server:1.0.71
docker run -idt -p 8001:8001 --net=host -e spring.profiles.active=peerAware -e appPort=8001 -e hostName=$HOSTNAME -e dataCenter=asimio-cloud-dal -e environment=staging -e peerUrls="http://$HOSTNAME:8002/eureka/" asimio/discovery-server:1.0.73
docker run -idt -p 8002:8002 --net=host -e spring.profiles.active=peerAware -e appPort=8002 -e hostName=$HOSTNAME -e dataCenter=asimio-cloud-dal -e environment=staging -e peerUrls="http://$HOSTNAME:8001/eureka/" asimio/discovery-server:1.0.73

Demo Service 1

docker pull asimio/demo-registration-api-1:1.0.25
docker run -idt -p 8501:8501 --net=host -e appPort=8501 -e hostName=$HOSTNAME -e eureka.client.serviceUrl.defaultZone="http://$HOSTNAME:8001/eureka/,http://$HOSTNAME:8002/eureka/" asimio/demo-registration-api-1:1.0.27
docker run -idt -p 8502:8502 --net=host -e appPort=8502 -e hostName=$HOSTNAME -e eureka.client.serviceUrl.defaultZone="http://$HOSTNAME:8001/eureka/,http://$HOSTNAME:8002/eureka/" asimio/demo-registration-api-1:1.0.27

Demo Service 2

docker pull asimio/demo-registration-api-2:1.0.20
docker run -idt -p 8601:8601 --net=host -e appPort=8601 -e hostName=$HOSTNAME -e eureka.client.serviceUrl.defaultZone="http://$HOSTNAME:8001/eureka/,http://$HOSTNAME:8002/eureka/" asimio/demo-registration-api-2:1.0.21
docker run -idt -p 8602:8602 --net=host -e appPort=8602 -e hostName=$HOSTNAME -e eureka.client.serviceUrl.defaultZone="http://$HOSTNAME:8001/eureka/,http://$HOSTNAME:8002/eureka/" asimio/demo-registration-api-2:1.0.21

Accessing http://<docker host>:8001/ or http://<docker host>:8002/ should display an image similar to:

Discovery Server - Demo Service 1 and Demo Service 2 Registration Discovery Server - Demo Service 1 and Demo Service 2 Registration

Sending requests to demo Feign client:

curl -v http://localhost:8601/aggregation/actors
*   Trying ::1...
* Connected to localhost (::1) port 8601 (#0)
> GET /aggregation/actors HTTP/1.1
> Host: localhost:8601
> User-Agent: curl/7.43.0
> Accept: */*
>
< HTTP/1.1 200
< X-Application-Context: demo-registration-api-2:8601
< Content-Type: application/json
< Content-Length: 149
< Date: Tue, 04 Oct 2016 04:13:52 GMT
<
* Connection #0 to host localhost left intact
[{"actorId":"1","firstName":"First1","lastName":"Last1","lastUpdate":null},{"actorId":"2","firstName":"First2","lastName":"Last2","lastUpdate":null}]

Sending requests to demo RestTemplate and RibbonLoadBalancerClient:

curl -v http://localhost:8601/aggregation/actors1/1
*   Trying ::1...
* Connected to localhost (::1) port 8601 (#0)
> GET /aggregation/actors1/1 HTTP/1.1
> Host: localhost:8601
> User-Agent: curl/7.43.0
> Accept: */*
>
< HTTP/1.1 200
< X-Application-Context: demo-registration-api-2:8601
< Content-Type: application/json
< Content-Length: 73
< Date: Tue, 04 Oct 2016 04:18:27 GMT
<
* Connection #0 to host localhost left intact
{"actorId":"1","firstName":"First1","lastName":"Last1","lastUpdate":null}

Sending requests to demo RestTemplate annotated with @LoadBalanced:

curl -v http://localhost:8601/aggregation/actors2/1
*   Trying ::1...
* Connected to localhost (::1) port 8601 (#0)
> GET /aggregation/actors2/1 HTTP/1.1
> Host: localhost:8601
> User-Agent: curl/7.43.0
> Accept: */*
>
< HTTP/1.1 200
< X-Application-Context: demo-registration-api-2:8601
< Content-Type: application/json
< Content-Length: 73
< Date: Tue, 04 Oct 2016 04:20:26 GMT
<
* Connection #0 to host localhost left intact
{"actorId":"1","firstName":"First1","lastName":"Last1","lastUpdate":null}

And that’s about it.

Thanks for reading and as always, feedback is very much appreciated. If you found this post helpful and would like to receive updates when content like this gets published, sign up to the newsletter.

10. SOURCE CODE

Accompanying source code for this blog post can be found at:

11. REFERENCES