Configuring microservices

duration 20 minutes

Learn how to provide external configuration to microservices using MicroProfile Config.

What you’ll learn

You will learn how to externalize and inject both static and dynamic configuration properties for microservices using MicroProfile Config.

You will learn to aggregate multiple configuration sources, assign prioritization values to these sources, merge configuration values, and create custom configuration sources.

The application that you will be working with is an inventory service which stores the information about various JVMs running on different hosts. Whenever a request is made to the inventory service to retrieve the JVM system properties of a particular host, the inventory service will communicate with the system service on that host to get these system properties. You will add configuration properties to simulate if a service is down for maintenance.

Getting started

The fastest way to work through this guide is to clone the Git repository and use the projects that are provided inside:

git clone https://github.com/openliberty/guide-microprofile-config.git
cd guide-microprofile-config

The start directory contains the starting project that you will build upon.

The finish directory contains the finished project, which is what you will build.

Try what you’ll build

The finish directory in the root of this guide contains the finished inventory application. Feel free to give it a try before you proceed.

To try out the application, first navigate to the finish directory and then run the following Maven goals to build the application and run it inside Open Liberty:

mvn install liberty:start-server

After starting the application, you can access the following two microservices to test their availability:

In addition, you can access a third microservice, which retrieves and aggregates all of the configuration properties and sources that have been added throughout this guide. This is available at:

Once you are done checking out the application, stop the Open Liberty server:

mvn liberty:stop-server

Ordering multiple configuration sources

Now, navigate to the start directory to begin.

MicroProfile Config combines configuration properties from multiple sources, each known as a ConfigSource. Each ConfigSource has a specified priority, defined by its config_ordinal value.

A higher ordinal value means that the values taken from this ConfigSource will override values from ConfigSources with a lower ordinal value.

There are three default configuration sources as following:

  • System properties has a default ordinal of 400. (e.g. bootstrap.properties file)

  • Environment variables has a default ordinal of 300. (e.g. server.env file)

  • The META-INF/microprofile-config.properties configuration property file on the classpath has a default ordinal of 100.

Access the src/main/resources/META-INF/microprofile-config.properties local configuration file. This configuration file is the default configuration source for an application that uses MicroProfile Config.

config_ordinal=100
io_openliberty_guides_port_number=9080
io_openliberty_guides_inventory_inMaintenance=false
io_openliberty_guides_system_inMaintenance=false
io_openliberty_guides_testConfigOverwrite=DefaultSource

Injecting static configuration

The MicroProfile Config API was added as a dependency to your pom.xml file. Look for the dependency with the microprofile-config-api artifact ID. This feature allows you to use the MicroProfile Config API to externalize configurations for your microservices. The mpConfig-1.2 feature is also enabled in the src/main/liberty/config/server.xml file.

Now navigate to the src/main/resources/META-INF/microprofile-config.properties local configuration file to check some static configuration. This configuration file is the default configuration source for an application that uses MicroProfile Config.

The io_openliberty_guides_port_number property that has already been defined in this file, determines the port number of the REST service.

To use this configuration property, create a src/main/java/io/openliberty/guides/inventory/InventoryConfig.java file:

package io.openliberty.guides.inventory;

import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.inject.Provider;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import io.openliberty.guides.config.Email;

@RequestScoped
public class InventoryConfig {

  @Inject
  @ConfigProperty(name = "io_openliberty_guides_port_number")
  private int portNumber;

  public int getPortNumber() {
    return portNumber;
  }

}

Inject the io_openliberty_guides_port_number property, and add the getPortNumber() class method to the InventoryConfig.java file.

The @Inject annotation injects the port number directly, the injection value is static and fixed on application starting.

The getPortNumber() method directly returns the value of portNumber because it has been injected.

Injecting dynamic configuration

Note that three default config sources mentioned above are static and fixed on application starting, so the properties within them cannot be modified while the server is running. However, you can externalize configuration data out of the application package, through the creation of custom configuration sources, so that the service updates configuration changes dynamically.

Creating custom configuration sources

Custom configuration sources can be created by implementing the org.eclipse.microprofile.config.spi.ConfigSource interface and using the java.util.ServiceLoader mechanism.

A CustomConfigSource.json JSON file has already been created in the resources directory. This JSON file simulates a remote configuration resource in real life. This file contains 4 custom config properties and has an ordinal of 150. To use these properties in the application, the data object needs to be transformed from this JSON file to the configuration for your application.

To link this JSON file to your application, create a CustomConfigSource class in the src/main/java/io/openliberty/guides/config/CustomConfigSource.java file. Add the following content to implement the ConfigSource interface:

package io.openliberty.guides.config;

import javax.json.stream.JsonParser;
import javax.json.stream.JsonParser.Event;
import javax.json.Json;
import java.math.BigDecimal;
import java.util.*;
import java.io.StringReader;

import java.io.BufferedReader;
import java.io.FileReader;
import org.eclipse.microprofile.config.spi.ConfigSource;

public class CustomConfigSource implements ConfigSource {

  String fileLocation = System.getProperty("user.dir").split("target")[0]
      + "resources/CustomConfigSource.json";

  @Override
  public int getOrdinal() {
    return Integer.parseInt(getProperties().get("config_ordinal"));
  }

  @Override
  public Set<String> getPropertyNames() {
    return getProperties().keySet();
  }

  @Override
  public String getValue(String key) {
    return getProperties().get(key);
  }

  @Override
  public String getName() {
    return "Custom Config Source: file:" + this.fileLocation;
  }

  public Map<String, String> getProperties() {
    Map<String, String> m = new HashMap<String, String>();
    String jsonData = this.readFile(this.fileLocation);
    JsonParser parser = Json.createParser(new StringReader(jsonData));
    String key = null;
    while (parser.hasNext()) {
      final Event event = parser.next();
      switch (event) {
      case KEY_NAME:
        key = parser.getString();
        break;
      case VALUE_STRING:
        String string = parser.getString();
        m.put(key, string);
        break;
      case VALUE_NUMBER:
        BigDecimal number = parser.getBigDecimal();
        m.put(key, number.toString());
        break;
      case VALUE_TRUE:
        m.put(key, "true");
        break;
      case VALUE_FALSE:
        m.put(key, "false");
        break;
      default:
        break;
      }
    }
    parser.close();
    return m;
  }

  public String readFile(String fileName) {
    String result = "";
    try {
      BufferedReader br = new BufferedReader(new FileReader(fileName));
      StringBuilder sb = new StringBuilder();
      String line = br.readLine();
      while (line != null) {
        sb.append(line);
        line = br.readLine();
      }
      result = sb.toString();
      br.close();
    } catch (Exception e) {
      e.printStackTrace();
    }
    return result;
  }
}

The getProperties() method reads the key value pairs from the resources/CustomConfigSource.json JSON file and writes the information into a map.

Last but important, register the custom configuration source. Create a src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSource file. Add the following fully qualified class name of the configuration source into it:

io.openliberty.guides.config.CustomConfigSource

Enabling dynamic configuration injection

Now that the custom configuration source has successfully been set up, you can enable dynamic configuration injection of the properties being set in this ConfigSource. To enable this dynamic injection, first access the partially implemented Java class in the src/main/java/io/openliberty/guides/inventory/InventoryConfig.java file, inject the io_openliberty_guides_inventory_inMaintenance property, and add the isInMaintenance() class method. Simply copy all the following code and replace the contents of the file:

package io.openliberty.guides.inventory;

import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.inject.Provider;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import io.openliberty.guides.config.Email;

@RequestScoped
public class InventoryConfig {

  @Inject
  @ConfigProperty(name = "io_openliberty_guides_port_number")
  private int portNumber;

  @Inject
  @ConfigProperty(name = "io_openliberty_guides_inventory_inMaintenance")
  private Provider<Boolean> inMaintenance;

  public int getPortNumber() {
    return portNumber;
  }

  public boolean isInMaintenance() {
    return inMaintenance.get();
  }

}

The @Inject and @ConfigProperty annotations inject the io_openliberty_guides_inventory_inMaintenance configuration property from the CustomConfigSource.json file. The Provider<> interface used, forces the service to retrieve the inMaintenance value just in time. This retrieval of the value just in time makes the config injection dynamic and able to change without having to restart the application.

Every time that you invoke the inMaintenance.get() method, the Provider<> interface picks up the latest value of the io_openliberty_guides_inventory_inMaintenance property from configuration sources.

Creating custom converters

Configuration values are purely Strings. MicroProfile Config API has built-in converters that automatically converts configured Strings into target types such as int, Integer, boolean, Boolean, float, Float, double and Double. Therefore, in the previous section, it is type-safe to directly set the variable type to Provider<Boolean>.

To convert configured Strings to an arbitrary class type, such as the Email class type, create a src/main/java/io/openliberty/guides/config/Email.java file:

package io.openliberty.guides.config;

public class Email {
  private String name;
  private String domain;

  public Email(String value) {
    String[] components = value.split("@");
    if (components.length == 2) {
      name = components[0];
      domain = components[1];
    }
  }

  public String getEmailName() {
    return name;
  }

  public String getEmailDomain() {
    return domain;
  }

  public String toString() {
    return name + "@" + domain;
  }
}

To use this Email class type, add a custom converter by implementing the generic interface org.eclipse.microprofile.config.spi.Converter<T>. The Type parameter of the interface is the target type the String is converted to.

Create a src/main/java/io/openliberty/guides/config/CustomEmailConverter.java file, and add the following content to implement the Converter<T> interface:

package io.openliberty.guides.config;

import org.eclipse.microprofile.config.spi.Converter;
import io.openliberty.guides.config.Email;

public class CustomEmailConverter implements Converter<Email> {

  @Override
  public Email convert(String value) {
    return new Email(value);
  }

}

To register your implementation, create a src/main/resources/META-INF/services/org.eclipse.microprofile.config.spi.Converter file. Add the following fully qualified class name of the custom converter into it:

io.openliberty.guides.config.CustomEmailConverter

To use the custom Email converter, open the src/main/java/io/openliberty/guides/inventory/InventoryConfig.java file, inject the io_openliberty_guides_email property, and add the getEmail() method. Simply copy all the following code and replace the contents of the file:

package io.openliberty.guides.inventory;

import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.inject.Provider;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import io.openliberty.guides.config.Email;

@RequestScoped
public class InventoryConfig {

  @Inject
  @ConfigProperty(name = "io_openliberty_guides_port_number")
  private int portNumber;

  @Inject
  @ConfigProperty(name = "io_openliberty_guides_inventory_inMaintenance")
  private Provider<Boolean> inMaintenance;

  @Inject
  @ConfigProperty(name = "io_openliberty_guides_email")
  private Provider<Email> email;

  public int getPortNumber() {
    return portNumber;
  }

  public boolean isInMaintenance() {
    return inMaintenance.get();
  }

  public Email getEmail() {
    return email.get();
  }
}

Adding configuration to the microservice

To use externalized configuration in the inventory service, open the src/main/java/io/openliberty/guides/inventory/InventoryResource.java file. Simply copy all the following code and replace the contents of the file:

package io.openliberty.guides.inventory;

import java.util.Properties;

// CDI
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

import io.openliberty.guides.inventory.InventoryConfig;

@RequestScoped
@Path("systems")
public class InventoryResource {

  @Inject
  InventoryManager manager;

  @Inject
  InventoryConfig inventoryConfig;

  @GET
  @Path("{hostname}")
  @Produces(MediaType.APPLICATION_JSON)
  public Response getPropertiesForHost(@PathParam("hostname") String hostname) {

    if (!inventoryConfig.isInMaintenance()) {
      Properties props = manager.get(hostname, inventoryConfig.getPortNumber());
      if (props == null) {
        return Response.status(Response.Status.NOT_FOUND)
                       .entity(
                           "ERROR: Unknown hostname or the resource may not be running on the host machine")
                       .build();
      }
      return Response.ok(props).build();
    } else {
      return Response.status(Response.Status.SERVICE_UNAVAILABLE)
                     .entity("ERROR: Service is currently in maintenance. Contact: "
                         + inventoryConfig.getEmail().toString())
                     .build();
    }
  }

  @GET
  @Produces(MediaType.APPLICATION_JSON)
  public Response listContents() {
    if (!inventoryConfig.isInMaintenance()) {
      return Response.ok(manager.list()).build();
    } else {
      return Response.status(Response.Status.SERVICE_UNAVAILABLE)
                     .entity("ERROR: Service is currently in maintenance. Contact: "
                         + inventoryConfig.getEmail().toString())
                     .build();
    }
  }

}

To add configuration to the inventory service, the InventoryConfig object is injected to the existing class.

The port number from the configuration is retrieved by the inventoryConfig.getPortNumber() method and passed to the manager.get() method as a parameter.

To determine whether the inventory service is in maintenance or not (according to the configuration value), inventoryConfig.isInMaintenance() class method is used. If you set the io_openliberty_guides_inventory_inMaintenance property to true in the configuration, the inventory service returns the message, ERROR: Service is currently in maintenance, along with the contact email. The email configuration value can be get by calling inventoryConfig.getEmail() method.

Building and running the application

To build the application, run the Maven install phase from the command line in the start directory:

mvn install

This command builds the application and creates a .war file in the target directory. It also configures and installs Open Liberty into the target/liberty/wlp directory.

Next, run the Maven liberty:start-server goal:

mvn liberty:start-server

This goal starts an Open Liberty server instance. Your Maven pom.xml is already configured to start the application in this server instance.

Once the server is running, the following two microservices should be available to access:

You can find the service that retrieves configuration information that is specific to this guide at the following location:

The config_ordinal value of the custom configuration source is set to 150. It overrides configuration values of the default microprofile-config.properties source, which has a config_ordinal value of 100.

Play with this application by changing configuration values for each property in the resources/CustomConfigSource.json file. Your changes are added dynamically, and you do not need to restart the server. Refresh http://localhost:9080/config to see the dynamic changes.

For example, change io_openliberty_guides_inventory_inMaintenance from false to true, then try to access http://localhost:9080/inventory/systems again. The following message displays: ERROR: Service is currently in maintenance.

If you make changes to the code, use the Maven compile goal to rebuild the application and have the running Open Liberty server pick them up automatically. Make sure your application is using loose config for this to work:

mvn compile

To stop the Open Liberty server, run the Maven liberty:stop-server goal:

mvn liberty:stop-server

Testing the application

Create a src/test/java/it/io/openliberty/guides/config/ConfigurationTest.java file and add the following code:

package it.io.openliberty.guides.config;

import static org.junit.Assert.*;

import javax.json.JsonObject;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;

import org.apache.cxf.jaxrs.provider.jsrjsonp.JsrJsonpProvider;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

public class ConfigurationTest {

  private String port;
  private String baseUrl;
  private Client client;

  private final String INVENTORY_HOSTS = "inventory/systems";
  private final String USER_DIR = System.getProperty("user.dir");
  private final String DEFAULT_CONFIG_FILE = USER_DIR
      + "/src/main/resources/META-INF/microprofile-config.properties";
  private final String CUSTOM_CONFIG_FILE = USER_DIR.split("target")[0]
      + "/resources/CustomConfigSource.json";
  private final String INV_MAINTENANCE_PROP = "io_openliberty_guides_inventory_inMaintenance";

  @Before
  public void setup() {
    port = System.getProperty("liberty.test.port");
    baseUrl = "http://localhost:" + port + "/";
    ConfigTestUtil.setDefaultJsonFile(CUSTOM_CONFIG_FILE);

    client = ClientBuilder.newClient();
    client.register(JsrJsonpProvider.class);
  }

  @After
  public void teardown() {
    ConfigTestUtil.setDefaultJsonFile(CUSTOM_CONFIG_FILE);
    client.close();
  }

  @Test
  public void testSuite() {
    this.testInitialServiceStatus();
    this.testPutServiceInMaintenance();
    this.testChangeEmail();
  }

  public void testInitialServiceStatus() {
    boolean status = Boolean.valueOf(ConfigTestUtil.readPropertyValueInFile(
        INV_MAINTENANCE_PROP, DEFAULT_CONFIG_FILE));
    if (!status) {
      assertEquals("The Inventory Service should be available", 0,
          ConfigTestUtil.getJsonObjectFromURL(client, baseUrl + INVENTORY_HOSTS, 1,
              null).getInt("total"));
    } else {
      assertEquals("The Inventory Service should be in maintenance",
          "ERROR: Service is currently in maintenance. Contact: admin@guides.openliberty.io",
          ConfigTestUtil.getStringFromURL(client, baseUrl + INVENTORY_HOSTS));
    }
  }

  public void testPutServiceInMaintenance() {
    JsonObject obj = ConfigTestUtil.getJsonObjectFromURL(client,
        baseUrl + INVENTORY_HOSTS, 1, null);

    assertEquals("The inventory service should be up in the beginning", 0,
        obj.getInt("total"));

    ConfigTestUtil.switchInventoryMaintenance(CUSTOM_CONFIG_FILE, true);

    String error = ConfigTestUtil.getStringFromURL(client,
        baseUrl + INVENTORY_HOSTS);

    assertEquals("The inventory service should be down in the end",
        "ERROR: Service is currently in maintenance. Contact: admin@guides.openliberty.io",
        error);
  }

  public void testChangeEmail() {
    ConfigTestUtil.switchInventoryMaintenance(CUSTOM_CONFIG_FILE, true);

    String error = ConfigTestUtil.getStringFromURL(client,
        baseUrl + INVENTORY_HOSTS);

    assertEquals("The email should be admin@guides.openliberty.io in the beginning",
        "ERROR: Service is currently in maintenance. Contact: admin@guides.openliberty.io",
        error);

    ConfigTestUtil.changeEmail(CUSTOM_CONFIG_FILE, "service@guides.openliberty.io");

    error = ConfigTestUtil.getStringFromURL(client,
        baseUrl + INVENTORY_HOSTS);

    assertEquals("The email should be service@guides.openliberty.io in the beginning",
        "ERROR: Service is currently in maintenance. Contact: service@guides.openliberty.io",
        error);
  }

}

The testInitialServiceStatus() test case reads the value of the io_openliberty_guides_inventory_inMaintenance configuration property in the file META-INF/microprofile-config.properties and checks the HTTP response of the inventory service. If the configuration value is false, the service returns a valid response. Otherwise, the service returns the following message: ERROR: Service is currently in maintenance.

Because the io_openliberty_guides_inventory_inMaintenance configuration property is set to false by default, the testPutServiceInMaintenance() test case first checks that the inventory service is not in maintenance in the beginning. Next, this test switches the value of the io_openliberty_guides_inventory_inMaintenance configuration property to true. In the end, the inventory service returns the following message: ERROR: Service is currently in maintenance.

The testChangeEmail() test case first puts the inventory service in maintenance, then it changes the email address in the configuration file. In the end, the inventory service should display the error message with the latest email address.

In addition, a few endpoint tests have been provided for you to test the basic functionality of the inventory and system services. If a test failure occurs, then you must have introduced a bug into the code. Reminder that you must register the custom configuration source and custom converter in the src/main/resources/META-INF/services/ directory, if you miss to complete these steps, the tests will fail.

Running the tests

If the server is still running from the previous steps, stop it using the Maven liberty:stop-server goal from command line in the start directory:

mvn liberty:stop-server

Then, verify that the tests pass using the Maven verify goal:

mvn verify

It may take some time before build is complete. If the tests pass, you will see a similar output to the following:

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running it.io.openliberty.guides.config.ConfigurationTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 4.729 sec - in it.io.openliberty.guides.config.ConfigurationTest
Running it.io.openliberty.guides.inventory.InventoryEndpointTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.477 sec - in it.io.openliberty.guides.inventory.InventoryEndpointTest
Running it.io.openliberty.guides.system.SystemEndpointTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.013 sec - in it.io.openliberty.guides.system.SystemEndpointTest

Results :

Tests run: 3, Failures: 0, Errors: 0, Skipped: 0

To see whether the tests detect a failure, remove the configuration resetting line in the setup() method of the ConfigurationTest.java file. Then manually change some configuration values in the resources/CustomConfigSource.json file. Re-run the Maven build. You will see a test failure occur.

Great work! You’re done!

You just built and tested a MicroProfile application with MicroProfile Config and Open Liberty.

Feel free to try one of the related guides. They demonstrate new technologies that you can learn and expand on top what you built in this guide.

Contribute to this guide

Is something missing or broken? Raise an issue, or send us a pull request.