Search

Dark theme | Light theme

February 11, 2025

Helidon SE Helpings: Adding Information To Info Endpoint

It is possible to add an endpoint to Helidon SE that can show information about the application. You can add custom information to this endpoint. In order to enable the endpoint you need to add the dependency io.helidon.webserver.observe:helidon-webserver-observe-info to your pom.xml file. This will add the endpoint /observe/info to your application. You can add key-value pairs to your configuration or code that will be exposed in the endpoint.

To start using the endpoint you must add the following dependency to your pom.xml file:

<dependency>
    <groupId>io.helidon.webserver.observe</groupId>
    <artifactId>helidon-webserver-observe-info</artifactId>
</dependency>

You can define the information that should be exposed by the endpoint in your configuration file. Any property name and value prefixed with server.features.observe.observers.info.values. will be added to the endpoint:

# File: src/main/resources/application.properties
...
server.features.observe.observers.info.values.version=1.0.2
server.features.observe.observers.info.values.app-name=Helidon-Sample
...

Instead of using fixed values for the properties you can also use values from properties coming from the pom.xml file. You can refer to properties name using @{property}@ or ${property} syntax. You must enable resource filtering in your pom.xml:

...
    <build>
        ...
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <filtering>true</filtering>
                <includes>
                    <include>application.properties</include>
                </includes>
            </resource>
        </resources>  
        ...
    </build>
...

So for our previous example you can also use the POM project properties project.version and project.artifactId in the application.properties file. When the file is copied to the target directory by Maven the placeholders will be replaced with the actual values.

# File: src/main/resources/application.properties
...
server.features.observe.observers.info.values.version=@project.version@
server.features.observe.observers.info.values.app-name=@project.artifactId@
...

Next you need to read the configuration file and use it to configure a WebServer instance:

import io.helidon.config.Config;
import io.helidon.webserver.WebServer;
...
    Config config = Config.create();

    WebServer server =
        WebServer.builder()
            ...
            .config(config.get("server"))
            .build()
            .start();
...

Instead of defining the properties for the /observe/info endpoint in a properties file you can also programmatically define them in your code. You must create a InfoObserver object and add the properties to the instance using the addValues method. This InfoObserver object can be assigned as ObserverFeature to a WebServer instance. In the following example you can see how to define a version and app-name property with some hardcoded value in the application code.

import io.helidon.webserver.WebServer;
import io.helidon.webserver.observe.ObserveFeature;
import io.helidon.webserver.observe.info.InfoObserver;
import java.util.Map;
...
    // Create InfoObserver instance and add properties version and app-name.
    InfoObserver infoObserver =
        InfoObserver.builder()
            .addValues(Map.of("version", "1.0.0", "app-name", "Helidon-Sample"))
            .build();

    WebServer server =
        WebServer.builder()
            ...
            // Add observe feature with info observer.
            .addFeature(ObserveFeature.create(infoObserver))
            .build()
            .start();
...

If you start your Helidon SE application you can see the information in the /observe/info endpoint. In the following example we use a curl command to get the information:

$ curl -s -X GET --location "http://localhost:8080/observe/info" | jq .
{
  "app-name": "Helidon-Sample",
  "version": "1.0.0"
}
$

Written with Helidon SE 4.1.6.

February 8, 2025

Helidon SE Helpings: Return Response Based On Request Accept Header

Suppose you want to return a response based on the Accept header of the request. If the Accept header is application/json you want to return a JSON response and if the Accept header is application/xml you want to return an XML response. You can use the isAccepted(MediaType) of the ServerRequestHeaders class to check if the value of the request header Accept is equal to the specified media type. The method returns true if the media type is defined for the request header Accept and false if not.

In order to convert an object to XML you need to add the dependency com.fasterxml.jackson.dataformat:jackson-dataformat-xml to your pom.xml file:

<dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
</dependency>

In the following example HttpService implementation the request header Accept is used to determine if the response should be in XML or default JSON format:

package mrhaki;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import io.helidon.common.media.type.MediaTypes;
import io.helidon.http.HeaderNames;
import io.helidon.webserver.http.HttpRules;
import io.helidon.webserver.http.HttpService;
import io.helidon.webserver.http.ServerRequest;
import io.helidon.webserver.http.ServerResponse;

/** Example service to return data in either XML or JSON depending on the request header Accept. */
class SampleService implements HttpService {
  /** Use for converting an object to XML. */
  private final XmlMapper xmlMapper = new XmlMapper();

  @Override
  public void routing(HttpRules rules) {
    rules.get("/content", this::handle);
  }

  private void handle(ServerRequest req, ServerResponse resp) throws JsonProcessingException {
    // Define record for data structure to send in the response.
    record Message(String content) {}

    // Check if the Accept header is set with the value application/xml.
    if (req.headers().isAccepted(MediaTypes.APPLICATION_XML)) {
      // Return XML response.
      resp.header(HeaderNames.CONTENT_TYPE, MediaTypes.APPLICATION_XML.text())
          .send(xmlMapper.writeValueAsString(new Message("Sample")));
    } else {
      // Return default JSON response.
      resp.send(new Message("Sample"));
    }
  }
}

When you access the endpoint with different values for the request header Accept you get a response in the format you specified:

$ curl -s -X GET --location "http://localhost:8080/sample/content" \
    -H "Accept: application/xml" | xmllint --format -
<?xml version="1.0">
<Message>
  <content>Sample</content>
</Message>
$
$ curl -s -X GET --location "http://localhost:8080/sample/content" \
    -H "Accept: application/json" | jq .
{
  "content": "Sample"
}
$

Written with Helidon SE 4.1.6.

Spring Sweets: Use Logging in EnvironmentPostProcessor Implementation

You can write an implementation of the interface EnvironmentPostProcessor to customize the Environment of a Spring Boot application. For example you can read an external configuration file and add its properties to the Environment. If you want to add some logging statement to the class then you need to make sure you pass a DeferredLogFactory to the constructor of the class. From this factory you can use the getLogger method to get a Log instance. This is needed, because the implementation of the interface EnvironmentPostProcessor is created before the logging system is initialized. By using the Log instances created from DeferredLogFactory Spring Boot will make sure the log messages are written when the logging system is initialized.

In the following example you can see how to use the DeferredLogFactory:

package mrhaki.sample;

import org.apache.commons.logging.Log;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.boot.logging.DeferredLogFactory;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.ConfigurableEnvironment;

/**
 * Sample implementation for {@link EnvironmentPostProcessor}.
 */
@Order
public class SampleEnvironmentPostProcessor implements EnvironmentPostProcessor {

  /**
   * Log instance to log messages after the logging system is initialized.
   */
  private final Log log;

  /**
   * Create instance.
   *
   * @param logFactory Use logFactory to create logger that will output if the logging system is
   *     initialized.
   */
  public SampleEnvironmentPostProcessor(DeferredLogFactory logFactory) {
    log = logFactory.getLog(SampleEnvironmentPostProcessor.class);
  }

  @Override
  public void postProcessEnvironment(
      ConfigurableEnvironment environment, SpringApplication application) {
    if (log.isInfoEnabled()) {
      // This log message will only be written once the logging system is initialized
      // and the log level is INFO or higher.
      log.info("Sample environment post processor started");
    }
  }
}

The support for DeferredLogFactory is added in Spring Boot 2.4.

Written with Spring Boot 3.4.2.

February 7, 2025

Helidon SE Helpings: Configure Memory Health Check

You can configure a memory health check in Helidon SE. A memory health check will return a status of UP if the memory usage is below a certain threshold percentage and DOWN if the memory usage is above the threshold percentage. The default threshold percentage is 98%. To add the memory health check you need to add the dependency io.helidon.health:helidon-health-checks to your pom.xml file. This dependency contains three health checks: disk space usage, memory usage and dead lock detection.

To configure the memory health check you need to set the configuration property server.features.observe.observers.health.helidon.health.memory.thresholdPercent to the threshold percentage. Alternatively you can set the threshold percentage in your application code.

In the following example application you add a memory usage health check. First you add the dependency to the pom.xml file:

<dependency>
  <groupId>io.helidon.health</groupId>
  <artifactId>helidon-health-checks</artifactId>
</dependency>

To configure the memory health check you can add the property server.features.observe.observers.health.helidon.health.memory.thresholdPercent to the application.properties file.

...
# Set threshold percentage for memory usage.
# When the memory usage is above this percentage, the health check will fail.
# Default is 98.0
server.features.observe.observers.health.helidon.health.memory.thresholdPercent=90.0
...

In your application code you need to use the configuration to create the WebServer instance:

import io.helidon.webserver.WebServer;
import io.helidon.config.Config;
...
    Config config = Config.create();
    Config.global(config);

    WebServer server =
        WebServer.builder()
            ...
            // Read configuration properties.
            .config(config.get("server"))
            .build()
            .start();...

Instead of setting the threshold percentage in the configuration file you can also set it in your application code:

import io.helidon.health.checks.MemoryHealthCheck;
import io.helidon.webserver.WebServer;
import io.helidon.webserver.observe.ObserveFeature;
import io.helidon.webserver.observe.health.HealthObserver;
...
    // Configure memory usage health check.
    HeapMemoryHealthCheck heapMemoryHealthCheck =
        HeapMemoryHealthCheck.builder().thresholdPercent(90.0).build();

    // Create observe feature with health check.
    HealthObserver healthObserver =
        HealthObserver.builder()
            // Show details in health endpoint.
            .details(true)
            // Disable auto-discovery of health checks.
            .useSystemServices(false)
            // Add heap memory health check.
            .addCheck(heapMemoryHealthCheck)
            .build();

    WebServer server =
        WebServer.builder()
            .port(8080)
            // Add observe feature with health check.
            .addFeature(ObserveFeature.create(healthObserver))
            .build()
            .start();

...

Once you start the application you can access the health endpoint at http://localhost:8080/health:

$ curl --silent -X GET --location "http://localhost:8080/observe/health" | jq .
{
  "status": "UP",
  "checks": [
    {
      "name": "heapMemory",
      "status": "UP",
      "data": {
        "free": "39.25 MB",
        "freeBytes": 41160992,
        "max": "50.00 MB",
        "maxBytes": 52428800,
        "percentFree": "78.51%",
        "total": "50.00 MB",
        "totalBytes": 52428800
      }
    }
  ]
}

Written with Helidon SE 4.1.6.

January 29, 2025

Helidon SE Helpings: Configure Disk Space Health Check

In Helidon SE you can enable a health check for the disk space usage. If the disk space usage is above a certain threshold then the health check will fail. To enable the disk space health check you need to add the dependency io.helidon.health:helidon-health-checks to your pom.xml file. The dependency contains three health checks: disk space usage, memory usage and dead lock detection. To configure the disk space health check you need to set the configuration property server.features.observe.observers.health.helidon.health.diskSpace.thresholdPercent to the threshold percentage. Or programmatically set the value in your application code. The default value is 99.999, which means that in real life the health check will not fail. You need to set a lower percentage in order to see the health check fail. For example when you set the value to 95.0 then the health check will fail when the disk space usage is above 95% or less than 5% of the disk space is available. You can also configure the path to check for disk space usage. The default path is the current working directory, but it can be set to another path. You need to set the configuration property server.features.observe.observers.health.helidon.health.diskSpace.path to change the path.

In the following example application you add a disk space usage health check. First you add the dependency to the pom.xml file:

<dependency>
  <groupId>io.helidon.health</groupId>
  <artifactId>helidon-health-checks</artifactId>
</dependency>

Next you configure the disk space health check. You need to enable the details view of the health endpoint. Next you set the threshold percentage and the path to check for disk space usage. If your application uses application.properties then you add the following lines to the configuration file:

...
# Enable detailed information for the health endpoint.
server.features.observe.observers.health.details=true

# Set threshold percentage for disk space usage.
# When the disk space usage is above this percentage, the health check will fail.
# Default is 99.999
server.features.observe.observers.health.helidon.health.diskSpace.thresholdPercent=95.0

# Configure path to check for disk space usage.
# Default path is "."
server.features.observe.observers.health.helidon.health.diskSpace.path=/mnt/data
...

In your application code you need to use the configuration to create the WebServer instance:

import io.helidon.webserver.WebServer;
import io.helidon.config.Config;
...
    Config config = Config.create();
    Config.global(config);

    WebServer server =
        WebServer.builder()
            ...
            // Read configuration properties.
            .config(config.get("server"))
            .build()
            .start();
...

The configuration can also be done in application code. In the following example you configure the disk space health check in the application code:

import io.helidon.health.checks.DiskSpaceHealthCheck;
import io.helidon.webserver.WebServer;
import io.helidon.webserver.observe.ObserveFeature;
import io.helidon.webserver.observe.health.HealthObserver;
...
    // Configure disk space usage health check.
    DiskSpaceHealthCheck diskSpaceHealthCheck =
        DiskSpaceHealthCheck.builder()
            // Set threshold percentage for disk space usage.
            // When the disk space usage is above this percentage, the health check will fail.
            // Default is 99.999
            .thresholdPercent(95.0)
            // Configure path to check for disk space usage.
            // Default path is "."
            .path("/mnt/data")
            .build();

    // Create observe feature with health check.
    HealthObserver healthObserver =
        HealthObserver.builder()
            // Show details in health endpoint.
            .details(true)
            // Disable auto-discovery of health checks.
            .useSystemServices(false)
            // Add disk space usage health check.
            .addCheck(diskSpaceHealthCheck)
            .build();

    WebServer server =
        WebServer.builder()
            .port(8080)
            // Add observe feature with health check.
            .addFeature(ObserveFeature.create(healthObserver))
            .build()
            .start();
...

When you access the health endpoint you see the disk space usage health check:

$ curl --silent -X GET --location "http://localhost:8080/observe/health" | jq .
{
  "status": "DOWN",
  "checks": [
    {
      "name": "diskSpace",
      "status": "DOWN",
      "data": {
        "free": "18.94 GB",
        "freeBytes": 20333867008,
        "percentFree": "4.07%",
        "total": "465.63 GB",
        "totalBytes": 499963174912
      }
    }
  ]
}

Written with Helidon SE 4.1.6.

January 24, 2025

Helidon SE Helpings: Show Details For Health Endpoint

With Helidon SE you can add a health endpoint to your application by simply adding a dependency. In your pom.xml you have to add the dependency io.helidon.webserver.observe:helidon-webserver-observe-health. This adds a new endpoint /health to your application. When your application is up and running the /health endpoint will return the HTTP status code 204 with an empty body. If your application is not healthy then the HTTP status code 503 is returned. In case of an error an HTTP status code 500 is sent to the client.

If you want to see a response with details then you need to set the configuration property server.features.observe.observers.health.details to the value true. Instead of the HTTP status code 204 the status code 200 is returned when our application is healthy. The response contains a JSON object with a status field with the value UP for a healthy response. The response also contains the field checks with an array of detailed information from health checks that are available in our application.

In the following example application you add a health endpoint with details. First you add the dependency to the pom.xml file:

<dependency>
  <groupId>io.helidon.webserver.observe</groupId>
  <artifactId>helidon-webserver-observe-health</artifactId>
</dependency>

Next you can start the application to try out the health endpoint. Using a HTTP client, for example curl, you can send a GET request to the health endpoint:

$ curl -X GET -i "http://localhost:8080/observe/health"
HTTP/1.1 204 No Content
Date: Fri, 24 Jan 2025 15:55:06 +0100
Connection: keep-alive
Content-Length: 0

You see that the HTTP status code 204 is returned.

In order to see some more details you need to set the configuration property server.features.observe.observers.health.details to the value true. Depending on the configuration file format that is supported you add this property to a configuration file. If you use application.properties simply add the following line:

...
server.features.observe.observers.health.details=true
...

Alternatively you can also set the property value using Java system properties or environment variables.

You need to read in the configuration and use it configure the WebServer instance:

import io.helidon.webserver.WebServer;
import io.helidon.config.Config;
...
    Config config = Config.create();
    Config.global(config);

    WebServer server =
        WebServer.builder()
            .config(config.get("server"))
            .build()
            .start();
...

Now you can send a GET request to the health endpoint again:

$ curl -X GET -i "http://localhost:8080/observe/health"
HTTP/1.1 200 OK
Date: Fri, 24 Jan 2025 15:57:35 +0100
Connection: keep-alive
Content-Length: 27
Content-Type: application/json

{"status":"UP","checks":[]}

The application doesn’t have any extra health checks so the checks array is empty. You can also see the HTTP status code 200 is returned and the response contains a JSON object with the field status with the value UP.

You can also use Java code to add a detailed health check. In the following example we add a feature to our code that creates the WebServer instance:

import io.helidon.webserver.WebServer;
import io.helidon.config.Config;
...
    Config config = Config.create();
    Config.global(config);

    WebServer server =
        WebServer.builder()
            .addFeature(
                ObserveFeature.builder()
                    // Add health observer with details
                    .addObserver(HealthObserver.builder().details(true).build())
                    .build())
            // Allow override of configuration properties
            .config(config.get("server"))
            .build()
            .start();
...

Written with Helidon 4.1.6.

November 3, 2024

Mastering Maven: Disable Logging Of Progress Downloading Artifacts

When Maven needs to download artifacts from a remote repository, it logs the progress of the download. This can lead to a lot of noise in the output. Luckily, we can suppress the logging of the download progress. Since Maven 3.6.1. we can use the command-line option --no-transfer-progress to disable the logging of the download progress. There is also a short version of the option: -ntp.

First look at an example where we do not disable the logging of the download progress. In the output, we see (a lot of) messages showing which artifacts are downloaded and the progress.

$ mvn package
[INFO] Scanning for projects...
Downloading from central: https://repo.maven.apache.org/maven2/org/springframework/boot/spring-boot-starter-parent/3.3.5/spring-boot-starter-parent-3.3.5.pom
Downloaded from central: https://repo.maven.apache.org/maven2/org/springframework/boot/spring-boot-starter-parent/3.3.5/spring-boot-starter-parent-3.3.5.pom (13 kB at 29 kB/s)
Downloading from central: https://repo.maven.apache.org/maven2/org/springframework/boot/spring-boot-dependencies/3.3.5/spring-boot-dependencies-3.3.5.pom
Downloaded from central: https://repo.maven.apache.org/maven2/org/springframework/boot/spring-boot-dependencies/3.3.5/spring-boot-dependencies-3.3.5.pom (100 kB at 1.7 MB/s)
...
[INFO]
[INFO] --------------------------< com.example:demo >--------------------------
[INFO] Building demo 0.0.1-SNAPSHOT
[INFO]   from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
Downloading from central: https://repo.maven.apache.org/maven2/org/springframework/boot/spring-boot-maven-plugin/3.3.5/spring-boot-maven-plugin-3.3.5.pom
Downloaded from central: https://repo.maven.apache.org/maven2/org/springframework/boot/spring-boot-maven-plugin/3.3.5/spring-boot-maven-plugin-3.3.5.pom (4.0 kB at 308 kB/s)
Downloading from central: https://repo.maven.apache.org/maven2/org/springframework/boot/spring-boot-maven-plugin/3.3.5/spring-boot-maven-plugin-3.3.5.jar
Downloaded from central: https://repo.maven.apache.org/maven2/org/springframework/boot/spring-boot-maven-plugin/3.3.5/spring-boot-maven-plugin-3.3.5.jar (137 kB at 6.5 MB/s)
...

We remove the downloaded dependencies from our local repository and run the same Maven command again, but now we add the command-line option --no-transfer-progress (or the short version -ntp). We no longer have the downloading progress messages in our output:

$ mvn --no-transfer-progress package
[INFO] Scanning for projects...
[INFO]
[INFO] --------------------------< com.example:demo >--------------------------
[INFO] Building demo 0.0.1-SNAPSHOT
[INFO]   from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
...

Instead of adding this option each time we run Maven, we can add it to the file .mvn/maven.config in our project:

--no-transfer-progress

Alternatively, since Maven 3.9.0, we can add the option to the environment variable MAVEN_ARGS.

$ export MAVEN_ARGS=-ntp 
$ mvn package
[INFO] Scanning for projects...
[INFO]
...

Written with Maven 3.9.9.

October 24, 2024

Helidon SE Helpings: Default Configuration Sources During Testing

In a previous blog post we learned about the default input sources that are used by Helidon SE. The list of input sources is different based on which artifacts are on the classpath of our application. When we write tests for code in our application that uses the default configuration created by Config.create() we must take into account that different input sources are used. Also here it is based on the artifacts that are on the classpath. That means that different files with configuration data are loaded, eg. a file application-test.conf when we have the artifact helidon-config-hocon and a file application-test.yml if the artifact helidon-config-yaml is on the classpath.

If we use the artifact helidon-config then the following input sources are searched with the following order of preference:

  1. System environment variables
  2. Java system properties
  3. a file META-INF/microprofile-config-test.properties on the classpath,
  4. a file META-INF/microprofile-config.properties on the classpath.

Notice that there is no input source that looks for a file application.properties on the classpath, but when we run our application that is a valid input source. Also the order of the input sources system environment variables and Java system properties switched. So the input sources for a default configuration created with Config.create() is different during run-time and when we test our application.

In the following test we create a default configuration using Config.create() and get the configuration property app.message that is set using a system environment variable, Java system property and the file META-INF/microprofile-config-test.properties placed in src/test/resources (which will be on the classpath). In order to test with setting a system environment variable in our test code we use the library com.github.stefanbirkner:system-lambda by adding the following dependency:

<dependency>
    <groupId>com.github.stefanbirkner</groupId>
    <artifactId>system-lambda</artifactId>
    <version>1.2.1</version>
    <scope>test</scope>
</dependency>

Our properties file looks like this:

# File: src/test/resources/META-INF/microprofile-config-test.properties
app.message=Hello from classpath:META-INF/microprofile-config-test.properties

Our test class has three test methods to check the value of the configuration property app.message:

// File: src/test/java/mrhaki/helidon/DefaultConfigTest.java
package mrhaki.helidon;

import io.helidon.config.Config;
import org.junit.jupiter.api.Test;

import static com.github.stefanbirkner.systemlambda.SystemLambda.restoreSystemProperties;
import static com.github.stefanbirkner.systemlambda.SystemLambda.withEnvironmentVariable;
import static org.assertj.core.api.Assertions.assertThat;

public class DefaultConfigTest {

    @Test
    void defaultConfig() throws Exception {
        // expect
        withEnvironmentVariable("APP_MESSAGE", "Hello from environment variable")
                .execute(() -> {
                    final Config config = Config.create();

                    assertThat(config.get("app.message").asString().asOptional())
                            .hasValue("Hello from environment variable");
                });
    }

    @Test
    void withSystemProperties() throws Exception {
        restoreSystemProperties(() -> {
            System.setProperty("app.message", "Hello from Java system property");
            assertThat(Config.create().get("app.message").asString().asOptional())
                    .hasValue("Hello from Java system property");
        });
    }

    @Test
    void withConfigTestProperties() {
        // given
        final Config config = Config.create();

        // expect
        assertThat(config.get("app.message").asString().asOptional())
                .hasValue("Hello from classpath:META-INF/microprofile-config-test.properties");
    }
}

When we apply the artifact helidon-config-hocon in our pom.xml file then the following input sources are searched with the following order of preference:

  1. System environment variables,
  2. Java system properties,
  3. a file application-test.json,
  4. a file application-test.conf,
  5. classpath:application-test.json,
  6. a file application-test.conf on the classpath,
  7. a file application.json,
  8. a file application.conf,
  9. a file application.json on the classpath,
  10. a file application.conf on the classpath,
  11. a file META-INF/microprofile-config-test.properties on the classpath,
  12. a file META-INF/microprofile-config.properties.` on the classpath

If we would use the artifact helidon-config-yaml then the following input sources are searched with the following order of preference:

  1. System environment variables,
  2. Java system properties,
  3. a file application-test.yml,
  4. a file application-test.yaml,
  5. a file application-test.yml on the classpath,
  6. a file application-test.yaml on the classpath,
  7. a file application.yml,
  8. a file application.yaml,
  9. a file application.yml on the classpath,
  10. a file application.yaml on the classpath,
  11. a file META-INF/microprofile-config-test.properties on the classpath,
  12. a file META-INF/microprofile-config.properties on the classpath.

Written with Helidon SE 4.1.2.

October 17, 2024

Helidon SE Helpings: Default Configuration Sources

When we use Helidon SE we can use the Config class to pass configuration properties to our application. The static method create() creates a default configuration. The Config class is then configured to support different input sources. This configuration reads configuration properties from the following sources in order:

  1. Java system properties,
  2. system environment variables,
  3. a file on the classpath that has the name application.properties (based on default config parser that is part of the artifact helidon-config).

The last input source behaves differently based on which classes that can parse a configuration file are on the classpath of our application. If we use the helidon-config artifact on the classpath then the configuration file read is application.properties. To read a JSON formatted configuration file we must add the helidon-config-hocon artifact to the classpath. The file that is read is application.json. With the same artifact we can read a HOCON formatted configuration file that is named application.conf. Finally if we add the helidon-config-yaml artifact to the classpath we can read a YAML formatted configuration file that is named application.yaml or application.yml. Helidon SE will only read one configuration file from the classpath with the following order of preference:

  1. application.yaml or application.yml,
  2. application.conf,
  3. application.json,
  4. application.properties.

In the following example class we create a default configuration using Config.create() and we show the contents of the configuration property app.message:

// File: src/main/java/mrhaki/helidon/Application.java
package mrhaki.helidon;

import io.helidon.config.Config;

public class Application {

    public static void main(String[] args) {
        // Create the default configuration.
        // Configuration properties are read from in order:
        // - from Java system properties
        // - from system environment variables
        // - from a file on the classpath that has the name
        //   'application.properties' (based on default config
        //   parser that is part of the artifact helidon-config).
        Config config = Config.create();

        // Get the configuration property app.message.
        // If the property is not set, the fallback value
        // is defined as "Hello from application code".
        String message = config.get("app.message")
                               .asString()
                               .orElse("Hello from application code");

        // Print the value of the configuration property to System.out.
        System.out.printf("app.message = %s%n", message);
    }
}

In our pom.xml we first only have the dependency for the artifact helidon-config:

...
<dependencies>
    <dependency>
        <groupId>io.helidon.config</groupId>
        <artifactId>helidon-config</artifactId>
    </dependency>
</dependencies>
...

Let’s build our application and run it without any configuration properties and rely on the default value that we defined in our code:

$ helidon build
[INFO] Scanning for projects...
[INFO] ------------------------------------------------------------------------
[INFO] Detecting the operating system and CPU architecture
[INFO] ------------------------------------------------------------------------
[INFO] os.detected.name: osx
[INFO] os.detected.arch: x86_64
[INFO] os.detected.version: 15.0
[INFO] os.detected.version.major: 15
[INFO] os.detected.version.minor: 0
[INFO] os.detected.classifier: osx-x86_64
[INFO]
[INFO] -----------------------< mrhaki.helidon:config >------------------------
[INFO] Building config 0.0.0-SNAPSHOT
[INFO]   from pom.xml
...

$ java -jar target/config.jar
app.message = Hello from application code

Next we add the file application.properties to the directory src/main/resources. This will put the file in the JAR file we build and make it available on the classpath:

# File: src/main/resources/application.properties
app.message=Hello from 'application.properties'

When we build and run our application again we see that the value of the configuration property app.message is read from the file application.properties on the classpath:

$ helidon build
...

$ java -jar target/config.jar
app.message = Hello from 'application.properties'

Our code also support setting the configuration property using environment variables. The value set by the environment variable APP_MESSAGE will overrule the value found in application.properties:

$ APP_MESSAGE="Hello from environment variable" java -jar target/config.jar
app.message = Hello from environment variable

We can overrule the value of the environment variable by setting the configuration property using the Java system properties:

$ APP_MESSAGE="Hello from environment variable" java -Dapp.message="Hello from Java system property" -jar target/config.jar
app.message = Hello from Java system property

If we replace the artifact helidon-config with helidon-config-hocon we can read a file named application.json from the classpath. First we change our dependency in the pom.xml:

...
<dependencies>
    <dependency>
        <groupId>io.helidon.config</groupId>
        <artifactId>helidon-config-hocon</artifactId>
    </dependency>
</dependencies>
...

Next we add the file application.json in src/main/resources:

{
  "app": {
    "message": "Hello from 'aplication.json'"
  }
}

We can rebuild our application and run it to see the following output:

$ helidon build
...

$ java -jar target/config.jar
app.message = Hello from 'aplication.json'

Instead of a JSON file we can also use file with the extension .conf written in HOCON format. The following example file application.conf in src/main/resources sets the configuration property app.message:

// File: src/main/resources/application.conf
app {
    message = Hello from 'application.conf'
}

When we build and run our application we see the following output:

$ helidon build
...

$ java -jar target/config.jar
app.message = Hello from 'application.conf'

To support a configuration file with the name application.yaml or application.yml in YAML format we must add the artifact helidon-config-yaml as dependency:

...
<dependencies>
    <dependency>
        <groupId>io.helidon.config</groupId>
        <artifactId>helidon-config-yaml</artifactId>
    </dependency>
</dependencies>
...

Our example application.yaml will look like this:

# File: src/amin/resources/application.yaml
app:
  message: Hello from 'application.yaml'

For the final time we build and run the application to show the output:

$ helidon build
...

$ java -jar target/config.jar
app.message = Hello from 'application.yaml'

Written with Helidon SE 4.1.2.

October 12, 2024

Helidon SE Helpings: Starting Web Server On A Random Port

Helidon SE provides a web server using Java virtual threads. When we configure the web server we can specify a specific port number the server will listen on for incoming request. If we want to use a random port number we must specify the value 0. Helidon will then start the web server on a random port number that is available on our machine.

The following example shows how to start a web server on a random port number. We use Helidon SE to write our code:

package mrhaki.helidon;

import io.helidon.logging.common.LogConfig;
import io.helidon.webserver.WebServer;

public class Application {
    public static void main(String[] args) {
        // Load logging configuration.
        LogConfig.configureRuntime();

        // Configure web server on a random port number.
        WebServer server = WebServer.builder()
            .port(0)  // Use random port number
            .build()
            .start();

        // Print port number the server is listening on.
        System.out.println("WEB server is up at http://localhost:" + server.port());
    }
}

When we start our application we see the following output:

2024.10.11 17:27:36.005 Logging at runtime configured using classpath: /logging.properties
2024.10.11 17:27:36.606 Helidon SE 4.1.2 features: [Config, Encoding, Media, WebServer]
2024.10.11 17:27:36.621 [0x2326f965] http://0.0.0.0:61685 bound for socket '@default'
2024.10.11 17:27:36.639 Started all channels in 28 milliseconds. 863 milliseconds since JVM startup. Java 21.0.4+7-LTS
WEB server is up at http://localhost:61685

The next time we start our application we see a different port number:

2024.10.11 17:28:11.283 Logging at runtime configured using classpath: /logging.properties
2024.10.11 17:28:11.852 Helidon SE 4.1.2 features: [Config, Encoding, Media, WebServer]
2024.10.11 17:28:11.873 [0x28b386dd] http://0.0.0.0:61698 bound for socket '@default'
2024.10.11 17:28:11.892 Started all channels in 41 milliseconds. 835 milliseconds since JVM startup. Java 21.0.4+7-LTS
WEB server is up at http://localhost:61698

We can also use the Helidon Configuration API to configure the web server to use a random port number. We can for example set the port number to 0 in the application.yaml file. In the following example we initialize standard configuration and use it configure the webserver:

package mrhaki.helidon;

import io.helidon.config.Config;
import io.helidon.logging.common.LogConfig;
import io.helidon.webserver.WebServer;

public class Application {
    public static void main(String[] args) {
        // Load logging configuration.
        LogConfig.configureRuntime();

        // Initialize the configuration.
        Config config = Config.create();
        Config.global(config);

        // Configure web server on a random port number.
        WebServer server = WebServer.builder()
            .config(config.get("server"))  // Get port number from configuration.
            .build()
            .start();

        // Print port number the server is listening on.
        System.out.println("WEB server is up at http://localhost:" + server.port());
    }
}

With our new configuration we can use an environment variable SERVER_PORT to set the port number to 0 for our web server. The configuration could also be defined in an application.yaml file:

server:
  port: 0

Written with Helidon SE 4.1.2.