Programming · Quarkus

Quarkus Guide: Configuration

This post is a follow-up on Quarkus Guide: Contexts and Dependency Injection, if you’re not yet familiar with injection in Quarkus, go check it out first.

In this one, we’ll go over how to add configuration to our application to provide extra flexibility.

From getting credentials out of a properties file to choosing between different services based on a configuration, allowing for a feature/bean to be changed at build or run-time, with code examples along the way.

  1. Simple demo
  2. Configuration sources
  3. Combining multiple properties
  4. Multiple properties / Config class
  5. Build Profiles
  6. Injecting a bean based on configuration
    1. Why can’t a build property be changed after build?
    2. Using runtime properties
  7. Extra: Using YAML

Simple demo

Like the previous post, we’ll select just one dependency on https://code.quarkus.io.

Don’t enable “Starter Code”, that’s cheating!

Note: Java will be used, Kotlin is currently still in preview, if you want to use it, add “quarkus-kotlin”.

We’ll begin by creating these 2 classes:

// QuoteController.java

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;

@Path("/quotes")
public class QuoteController {

    final CrawlerService service;

    QuoteController(CrawlerService service) {
        this.service = service;
    }

    @GET
    @Produces("text/plain")
    public String get() {
        return service.getRandomQuote();
    }
}

// CrawlerService.java

import javax.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class CrawlerService {

    String getRandomQuote() {
        return "This is truly random, trust me";
    }
}

Now visiting http://localhost:8080/quotes, you should get that truly random quote.

Let’s start by injecting a config property, add a field with @ConfigProperty to CrawlerService:

@ConfigProperty(name = "quote")
String quote;

String getRandomQuote() {
	return quote;
}

Now, before running it, create an application.properties file in main/resources, and add it there:

quote=Live as if you were to die tomorrow. Learn as if you were to live forever.

Visiting the endpoint again should yield this text.

You might want a default value so that, in case one is not provided, the app doesn’t crash.

To do so, add a defaultValue parameter to the annotation:

@ConfigProperty(name = "quote", defaultValue = "Nothing to see here.")

Now you can remove the line you added on application.properties and it will show “Nothing to see here.”.

Configuration sources

There are multiple places to define config properties:

  • Runtime
    • System properties with the -D flag: mvn -Dquote=hi-there quarkus:dev
    • Environment variables
    • .env file containing environment variables
    • config/application.properties file
  • Build-time
    • main/resources/application.properties file
    • main/resources/META-INF/microprofile-config.properties file

Note: this is the precedence order of Quarkus.

If you define a property in application properties and build the service, you can override it when running, with an environment variable for instance.

Combining multiple properties

It can be helpful to combine multiple properties into a single one. Think like a database connection string, you want it to get the credentials, the host, and the port from other properties.

database.host=pg-machine
database.port=5432
database.connection-string=postgresql://${database.host}:${database.port}

That “database.connection-string” would be expanded to postgresql://pg-machine:5432.

But this port is pretty common for Postgres, so we can even omit that line, by having a default value on the port:

database.host=pg-machine
# No port defined here
database.connection-string=postgresql://${database.host}:${database.port:5432}

In this case, instead of defining a property for the port, we simply inline its default value.

It would expand to the same: postgresql://user1:secure@pg-machine:5432.

If you want a different host for each environment (dev, int, prod, …), you can override it in any of the Runtime Configuration Sources.

For instance, as an environment variable when running on production: DATABASE_HOST=prod-pg-machine.

Multiple properties / Config class

You can have an object that holds all configs inside a prefix:

import io.smallrye.config.ConfigMapping;

@ConfigMapping(prefix = "service-configs")
public interface ServiceConfig {

    String author();
    String quote();
}

It is required to be an interface, which you then inject as it is a bean:

ServiceConfig config;

CrawlerService(ServiceConfig config) {
	this.config = config;
}

@Produces
String getRandomQuote() {
	return config.author() + ": " + config.quote();
}

Now in the application.properties, you’d add the lines:

service-configs.author=Mahatma Gandhi
service-configs.quote=Live as if you were to die tomorrow. Learn as if you were to live forever.

Note: If you’re configuring via environment variables, you instead define as:

SERVICE_CONFIGS_AUTHOR=Mahatma Gandhi
SERVICE_CONFIGS_QUOTE=Live as if you were to die tomorrow. Learn as if you were to live forever.

Here, any character that is not alphanumeric gets instead interpreted as an underscore.

Build Profiles

In Quarkus, by default there are 3 profiles:

  • dev
    • when running as quarkusDev/quarkus:dev
  • test
    • when running unit or int tests
  • prod
    • when you’re neither running a test nor in dev (in some sense, it’s the default)

These profiles have nothing to do with the Maven ones, they’re of Quarkus.

To define a property for different profiles, you prefix it with %profile.your.config, in our example, it would be:

service-configs.author=Mahatma Gandhi
service-configs.quote=Live as if you were to die tomorrow. Learn as if you were to live forever.
%prod.service-configs.author=Mark Twain
%prod.service-configs.quote=If you tell the truth, you don't have to remember anything.

Note: we still need to define the config without %prod, to have it in the dev and test profiles, the %prod one is simply overriding that default.

Now mvn quarkus:dev will yield the same response (dev profile), while mvn quarkus:build then java -jar target/quarkus-app/quarkus-run.jar will respond with the Mark Twain quote instead. (prod profile)

You could also have a application-prod.properties file for defining prod properties without having to prefix them inside the file. (or any other profile for that matter)

Injecting a bean based on configuration

Let’s say we want to deploy a version of our API that goes with the quote crawler feature disabled.

For this, we can use the @IfBuildProperty and @UnlessBuildProperty annotations.

We create what’s called a null object (from this pattern) of our CrawlerService:

@ApplicationScoped
@IfBuildProperty(name = "crawler.enabled", stringValue = "false")
public class NullCrawlerService implements CrawlerService {

    @Override
    public String getRandomQuote() {
        return "";
    }
}

You can see I’ve added that annotation, so Quarkus will inject this service in the controller if the crawler.enabled is false.

But we still have to add the @UnlessBuildProperty to the actual implementation, otherwise, if crawler.enabled is not false, then Quarkus will fail to inject due to those being candidates of CrawlerService.

@ApplicationScoped
@UnlessBuildProperty(name = "crawler.enabled", stringValue = "false", enableIfMissing = true)
public class CrawlerServiceImpl implements CrawlerService {
...
}

Note: the enableIfMissing is required, because otherwise, if the property isn’t defined, none of these beans satisfy for injecting a CrawlerService

Now if we add this to the application.properties file:

crawler.enabled=false

The endpoint will return nothing.

This kind of pattern is useful here because we must have a bean to inject.

Here, I’m showing how to “disable” a feature, but you could also have another CrawlerService implementation with a different behavior than the default.

There is also @IfBuildProfile, which works the same way but for profiles. (@IfBuildProfile("prod"))

Why can’t a build property be changed after build?

The example showed choosing beans using a build property, it won’t work after being built because, at build-time, Quarkus discards beans that don’t get injected.

In the previous example, if we disable the CrawlerService, then the implementation bean won’t be included in the built jar/executable.

In a project of mine, which does crawl quotes, I have two implementations for 2 different website sources, since they required different parsing.

But in that project, the approach was different, because I wanted to change the implementation at runtime, as we’ll see in the next chapter.

Using runtime properties

By runtime, I mean being able to re-launch our application with different beans, by providing either a build-time runtime property of any source mentioned in the beginning. (in this case, via an environment variable)

We change the previous “@..BuildProperty” annotations to these (“@Lookup..Property”):

@ApplicationScoped
@LookupIfProperty(name = "crawler.enabled", stringValue = "false")
public class NullCrawlerService implements CrawlerService {
...
}

@ApplicationScoped
@LookupUnlessProperty(name = "crawler.enabled", stringValue = "false", lookupIfMissing = true)
public class CrawlerServiceImpl implements CrawlerService {
...
}

And now if we try to run it:

javax.enterprise.inject.AmbiguousResolutionException: Ambiguous dependencies for type me.davidgomes.blog.quarkus.CrawlerService and qualifiers [@Default]

It doesn’t work. That’s because Quarkus has both beans present in the built jar, it doesn’t know which one we actually want.

We need to programmatically get the dependency, for that, we make use of the Instance<T> class, allowing us to query the candidates of the bean we’re trying to inject.

final CrawlerService service;

QuoteController(Instance<CrawlerService> service) {
    this.service = service.get();
}

Here, we’re asking for an instance of CrawlerService manually, and this enables us to inject it.

Now running again, we can call/visit the endpoint, and it works as expected, we have it disabled by the property we’ve defined in application.properties.

You can now set an environment variable, like so:

# remember the format when specifying environment variables
export CRAWLER_ENABLED=true

Or in a .env file.

And the service will now respond with the quote.

Extra: Using YAML

YAML is an alternative to the rather legacy Properties format.

In our previous example, we had the following:

service-configs.author=Mahatma Gandhi
service-configs.quote=Live as if you were to die tomorrow. Learn as if you were to live forever.

In YAML it would look like this:

service-configs:
  author: Mahatma Gandhi
  quote: Live as if you were to die tomorrow. Learn as if you were to live forever.

Not only is this more readable, but it also avoids the need for repeating the property’s prefix.

That example isn’t as drastic, but consider this:

database:
  name: quotes
  schema: quote_service
  host: localhost
  port: 5432
  credentials:
    username: quote-user
    password: secure-password!
  connection-string: postgresql://${database.host}:${database.port}/${database.name}?currentSchema=${database.schema}

Instead of that:

database.name=quotes
database.schema=quote_service
database.host=localhost
database.port=5432
database.credentials.username=quote-user
database.credentials.password=secure-password!
database.connection-string=postgresql://${database.host}:${database.port}/${database.name}?currentSchema=${database.schema}

To enable it, add the following dependency to your pom.xml:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-config-yaml</artifactId>
</dependency>

Summary

In this post, we’ve covered the configuration part of Quarkus, beginning with a simple property, mentioning the available ways to provide it, and finishing off by having an application capable of swapping a feature.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s