Overview

Ravel integrates with CDI via Vauban Build-Compatible Extension (ravel-cdi-vauban), allowing you to inject configuration values directly into CDI beans with the @ConfigProperty annotation.

Setup

Add the dependency:

<dependency>
    <groupId>io.vidocq.ravel</groupId>
    <artifactId>ravel-cdi-vauban</artifactId>
    <version>0.4.0-SNAPSHOT</version>
</dependency>

The extension is automatically discovered by CDI via ServiceLoader and Vauban’s Build-Compatible Extension mechanism.

Basic injection

import jakarta.inject.Inject;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class DatabaseConfig {

    @Inject
    @ConfigProperty(name = "db.host")
    String host;

    @Inject
    @ConfigProperty(name = "db.port")
    int port;

    @Inject
    @ConfigProperty(name = "db.name")
    String database;
}

The values are injected at bean creation time, converted to the target type.

With defaults

Use the defaultValue attribute to provide a fallback:

@Inject
@ConfigProperty(name = "app.timeout", defaultValue = "5000")
long timeoutMs;

@Inject
@ConfigProperty(name = "app.cache.enabled", defaultValue = "true")
boolean cacheEnabled;

If the configuration key is not found and no defaultValue is provided, deployment fails with DeploymentException.

Optional values

For optional configuration, use Optional<T>:

@Inject
@ConfigProperty(name = "feature.flag.experimental")
Optional<Boolean> experimentalFeature;

// Usage
if (experimentalFeature.isPresent()) {
    boolean enabled = experimentalFeature.get();
    // ...
}

Any injectable type, including plain String, can be wrapped in Optional:

@Inject
@ConfigProperty(name = "app.version")
Optional<String> version;

Supported types

All types supported by type converters can be injected:

  • Primitives: int, long, boolean, float, double, etc.

  • Boxes: Integer, Long, Boolean, Float, Double, etc.

  • Collections: List<T>, Set<T>, Collection<T>

  • Temporal: Duration, LocalDate, LocalDateTime, Instant, etc.

  • Custom types with implicit or registered converters

Field vs. constructor injection

Field injection is the canonical pattern for @ConfigProperty:

@ApplicationScoped
public class Config {
    @Inject
    @ConfigProperty(name = "key")
    String value;
}

Constructor injection also works, but is less commonly used with config:

@ApplicationScoped
public class Config {
    private final String value;

    @Inject
    public Config(@ConfigProperty(name = "key") String value) {
        this.value = value;
    }
}

Profile-aware injection

@ConfigProperty automatically respects configuration profiles:

app.db.host=prod-db.example.com
%dev.app.db.host=localhost
@Inject
@ConfigProperty(name = "app.db.host")
String dbHost;

With java -Dmp.config.profile=dev: - dbHost = localhost

Validation at deployment time

Configuration values are validated when the container starts. If a required key is missing, DeploymentException is thrown:

@Inject
@ConfigProperty(name = "app.secret.key") // Required, no default
String secretKey;
$ java MyApplication
Exception in thread "main" jakarta.enterprise.inject.spi.DeploymentException:
  Configuration key 'app.secret.key' not found

Use defaultValue or Optional<T> to make injection optional.

When the extension runs in the compiler NEW

Vauban can also run the extension while the application compiles: put ravel-cdi-vauban on the annotation processor path (on Vidocq, the vidocq-runtime-ravel-config-extension-codegen bundle does it). The extension then registers the beans that satisfy each @ConfigProperty and @ConfigProperties point, so Vauban’s build-time validation accepts them, and an unsupported injection type fails the build.

It does not check the values there. They come from where the application runs — its configuration files, the environment, the system properties of its JVM — which the compiler does not see. A missing key still fails the container start, as above. The extension asks Vauban which of the two it is running in (io.vidocq.vauban.api.ExtensionPhase).

Grouped configuration

A common pattern is grouping related config in a @Dependent bean:

@Dependent
public class DatabaseConfig {
    @Inject
    @ConfigProperty(name = "database.host")
    String host;

    @Inject
    @ConfigProperty(name = "database.port")
    int port;

    @Inject
    @ConfigProperty(name = "database.name")
    String name;
}

@ApplicationScoped
public class MyApplication {
    @Inject
    DatabaseConfig dbConfig;

    void start() {
        // Use dbConfig.host, dbConfig.port, dbConfig.name
    }
}

Example: Feature flags

@ApplicationScoped
public class Features {

    @Inject
    @ConfigProperty(name = "feature.cache", defaultValue = "true")
    boolean cacheEnabled;

    @Inject
    @ConfigProperty(name = "feature.metrics", defaultValue = "false")
    boolean metricsEnabled;

    @Inject
    @ConfigProperty(name = "feature.tracing")
    Optional<Boolean> tracingEnabled;

    public boolean isCacheEnabled() {
        return cacheEnabled;
    }

    public boolean isMetricsEnabled() {
        return metricsEnabled;
    }

    public boolean isTracingEnabled() {
        return tracingEnabled.orElse(false);
    }
}

Example: Multi-environment API client

# microprofile-config.properties
api.base.url=https://api.example.com
api.timeout=5000
api.retry.attempts=3

%dev.api.base.url=http://localhost:8080
%dev.api.timeout=10000
%dev.api.retry.attempts=1
@ApplicationScoped
public class ApiClient {

    @Inject
    @ConfigProperty(name = "api.base.url")
    String baseUrl;

    @Inject
    @ConfigProperty(name = "api.timeout")
    long timeoutMs;

    @Inject
    @ConfigProperty(name = "api.retry.attempts", defaultValue = "3")
    int retryAttempts;

    public void call() {
        // baseUrl, timeoutMs, retryAttempts are injected
        // and vary by profile
    }
}

Other CDI containers NEW

Ravel does not need Vauban. The jars run unchanged under another CDI container, and two integration-test modules, grouped under ravel-it-other-containers, prove it on every build (ravel#25), next to the official TCK, which already runs on Weld (TCK):

Module What it runs

ravel-it-weld

Weld SE 6.0 (CDI 4.1), class path, no Vauban: @ConfigProperty with and without a default, @ConfigProperties, an injected Config, and ConfigProvider.getConfig() answering with Ravel.

ravel-it-openliberty

A WAR on Open Liberty 26.0.0.10, MicroProfile 7 distribution (CDI 4.0, Jakarta REST 3.1), with Liberty’s mpConfig feature off: the same checks, over HTTP.

Neither module is published. ravel-cdi-vauban ships no beans.xml, on purpose: every bean it adds is synthetic, made by its build compatible extension, except RavelConfigProducer. A container that does not scan implicit archives, such as Weld SE by default, does not discover that producer, and the extension then synthesises the @Default Config bean itself.

Deploying on an application server
  • Keep the server’s own MicroProfile Config off — mpConfig on Open Liberty. Ravel registers its ConfigProviderResolver through META-INF/services; with the server’s implementation enabled, the two compete for ConfigProvider and for the @ConfigProperty injection points.

  • Nothing to exclude — the WAR carries ravel-core, ravel-api and the MicroProfile Config API (ravel-mp-config-api), and the Jakarta APIs are provided. ravel-it-openliberty builds its WAR with exactly these dependencies.

Next

  • Usage — programmatic configuration and custom converters

  • Reference — full API documentation