Helidon Builder++ and Configuration in 4.0

Jeff Trent
Helidon
Published in
5 min readApr 3, 2023

--

This article is applicable for Helidon version 4.0.0–Alpha2 or less. There are significant changes in newer versions. Expect new articles covering this material to be published.

I introduced Helidon Builder in my previous article, a new tool to help you develop robust fluent builders with just a few easy-to-use annotations. In that article I mentioned how Helidon Builder is extensible, allowing developers to extend the capabilities that help you “do your own thing” for the source code that is generated to support those fluent builders.

This article will focus on that aspect — it will demonstrate how this extensibility is being leveraged to help developers integrate fluent builders with the Helidon Configuration subsystem. This extension comes via a new annotation @ConfigBean, which can be thought of as @Builder++. Before this, though, let’s take a look at its motivation.

The Motivation

The Helidon Configuration module/subsystem is quite powerful. Instead of going into details here (see the Helidon Docs for that), I’ll just quote a recent Reddit post:

“I have been sitting on our in house config framework that is like 10 years (while being modernized overtime) and seriously was considering open sourcing this year but saw Helidon’s implementation and noped that idea.” — agentoutlier

While Helidon Config is indeed powerful, up until now it required lower-level coding to access it. Let’s look more closely at an example.

Starting with the schema — let's create a ServerConfig, ClientConfig, and represent the commonality of the two using CommonConfig as a supertype for both client and server configurations. We will start off with the @Builder annotation from the basic constructs that I shared in my last article.

@Builder
public interface TestServerConfig extends TestCommonConfig {
@Override
String name();

Optional<String> description();
}


@Builder
public interface TestClientConfig extends TestCommonConfig {
@ConfiguredOption("default")
@Override
String name();

int serverPort();

Map<String, String> headers();
}


@Builder(allowNulls = true)
public interface TestCommonConfig {
String name();

@ConfiguredOption(required = true)
int port();

List<String> cipherSuites();

char[] pswd();
}

We will then apply this yaml.

test-server:
name: "server"
bind-address: "127.0.0.1"
# pswd: "no-pswd"
port: 8086
# description: "an optional description"

test-client:
port: 8087
server-port: 8086
# pswd: "no-pswd"
cipher-suites:
- "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"
- "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256"
headers:
- header-key1: "header-val1"
- header-key2: "header-val2"

When we apply the Helidon Builder annotation processor to the sources above, it will generate code that will then let you create the instances as follows — this is what I am referring to as that “lower level of coding” effort that would be required to access config.

// load the configuratrion from the yaml file
Config cfg = Config.builder()
.sources(ConfigSources.classpath("io/helidon/builder/config/test/basic-config-bean-test.yaml"))
.addParser(YamlConfigParser.create())
.disableEnvironmentVariablesSource()
.disableSystemPropertiesSource()
.build();

// handle the server configuration
Config serverCfg = cfg.get("test-server");
// construct the builder and wire in the port attribute - a required attribute
DefaultTestServerConfig.Builder serverConfigBeanManualBuilder = DefaultTestServerConfig.builder()
.port(serverCfg.get("port").asInt().get());
// manually wire into the builder all of the optional attributes
serverCfg.get("name").asString().ifPresent(serverConfigBeanManualBuilder::name);
serverCfg.get("pswd").asString().ifPresent(serverConfigBeanManualBuilder::pswd);
serverCfg.get("description").asString().ifPresent(serverConfigBeanManualBuilder::description);
TestServerConfig serverConfigBeanManual = serverConfigBeanManualBuilder.build();

// do the same for the client configuration
Config clientCfg = cfg.get("test-client");
DefaultTestClientConfig.Builder clientConfigBeanManualBuilder = DefaultTestClientConfig.builder()
.port(clientCfg.get("port").asInt().get())
.serverPort(clientCfg.get("server-port").asInt().get())
.cipherSuites(clientCfg.get("cipher-suites").asList(String.class).get())
.headers(clientCfg.get("headers").asMap().get());
clientCfg.get("name").asString().ifPresent(clientConfigBeanManualBuilder::name);
clientCfg.get("pswd").asString().ifPresent(serverConfigBeanManualBuilder::pswd);
TestClientConfig clientConfigBeanManual = clientConfigBeanManualBuilder.build();

You may notice how each attribute needs to be handled properly, how to translate the types, dealing with optionality, etc. It is not horrible, but it is still boilerplate code that ideally you shouldn’t be hassled with having to write. It would get much more complicated with other schema types, nesting of configuration objects, etc.

What people may not know is that Helidon is a collection of modules that are mostly independent — if you need a great Configuration solution for your project then consider shimming in just that module into your project.

TLDR; The @ConfigBean

In the above example, simply replace @Builder or add @ConfigBean annotation to each interface type.

@ConfigBean
public interface TestServerConfig extends TestCommonConfig {
...
}

@ConfigBean
public interface TestClientConfig extends TestCommonConfig {
...
}

@ConfigBean
@Builder(allowNulls = true)
public interface TestCommonConfig {
...
}

Since @ConfigBean is handled as an extension to the Helidon Builder, you will also need to modify your pom.xml slightly to include it.

...

<dependency>
<groupId>io.helidon.builder</groupId>
<artifactId>helidon-builder-config</artifactId>
<version>${helidon.version}
</dependency>
<dependency>
<groupId>io.helidon.config</groupId>
<artifactId>helidon-config</artifactId>
<version>${helidon.version}
</dependency>
<dependency>
<groupId>io.helidon.config</groupId>
<artifactId>helidon-config-metadata</artifactId>
<scope>provided</scope>
<version>${helidon.version}
</dependency>

...

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>io.helidon.builder</groupId>
<artifactId>helidon-builder-config-processor</artifactId>
<version>${helidon.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>

...

Now, when you build your project you will find that there is a new toBuilder(Config cfg) method available on the generated fluent builder classes (along with other less interesting supporting methods).

And if we juxtapose the previous low-level coding approach to the new approach we can see how simple integration to the Helidon Configuration can be.

TestServerConfig serverConfigBean = DefaultTestServerConfig.toBuilder(serverCfg).build();
TestClientConfig clientConfigBean = DefaultTestClientConfig.toBuilder(clientCfg).build();

I guess it is a little anti-climactic. But that is kind of the point — all of the boilerplate code has been cut out! This approach is simpler, less error-prone, more reliable, and definitely quicker to code and maintain.

I should also mention that the “How?” in how this all works in terms of extensibility is a more advanced topic left to the eager reader — find it in the git repo directly. You can also find other tests and documentation there that can serve as examples for other usage patterns. One last note — you will need to use Java 20 instead of Java 11+ as of Helidon 4.0 M1 (coming soon).

Conclusion

The Helidon Builder is extensible. In this article I demonstrated how this extensibility works in conjunction with the Helidon Configuration subsystem using the ConfigBean annotation.

In future articles I will show how the ConfigBean Builder is even further leveraged by the Helidon Team to enable some very interesting features that have yet to be debuted in the Helidon 4.0 release.

The Helidon Builder is a great tool for you to consider for your toolbox. You can use it standalone in your build to generate source code (not bytecode) directly into target/generated-sources. And if you want to customize it you can with these extensibility features I have shown. Of course, if you are simply looking to use a fluent builder then you can consume it basically as-is (see the previous article for details).

Come and give Helidon a try today!

--

--