Skip to content

Instantly share code, notes, and snippets.

@lmolkova
Last active March 5, 2022 04:33
Show Gist options
  • Select an option

  • Save lmolkova/c4c84013287f7199a157c1c4392f08bd to your computer and use it in GitHub Desktop.

Select an option

Save lmolkova/c4c84013287f7199a157c1c4392f08bd to your computer and use it in GitHub Desktop.
Configuration beyond Spring

Quarkus configuration

Quarkus configuration is very similar to Spring:

  • supports application.properties and application.yaml
  • conventions on property names are the same: '.' as separator, kebab-case.
  • supports property sources, iterating over them and getting value by name programmatically or with annotation

What's different:

  • Quarkus uses smallrye-config which is implementation of microprofile-config. Annotation and feature-wise, it's similar to spring, but more explicit (reflection is discouraged)
  • Property metadata can be extended with microprofile language-server extension (example)
  • Doesn't support auto-binding 3rd party classes to config: requires explicit annotations and static binding.

What's (seems to be) the right approach for rich integration:

Micronaut configuration

Micronaut configuration is similar to Spring too:

  • supports application.properties and application.yaml
  • conventions on property names are the same: '.' as separator, kebab-case
  • supports property sources, iterating over them and getting value by name programmatically or with annotation
  • IntelliJ supports spring-configuration-metadata.json

What's different:

  • Doesn't support auto-binding 3rd party classes to config: requires explicit annotations and static binding.

What's (seems to be) the right approach for rich integration:

  • not clear
  • AWS uses a @ConfigurationBuilder that happens to work with their typed client configuration. It depends on typed client-specific options and some duck typing (e.g. presence of build() method on annotated with @ConfigurationBuilder options)

Samples with Azure SDK

Quarkus

  1. application.properties or application.yml

    az.client.application-id=123
    az.http.client.application-id=${az.client.application-id}321
    az.appconfig.endpoint=https://lmolkova-appconfig.azconfig.io
    az.appconfig-secondary.endpoint=https://lmolkova-appconfig.azconfig.io
  2. Implement configuration source

    class SdkConfigurationSource implements ConfigurationSource {
    
        Map<String, String> properties;
        public SdkConfigurationSource() {
            properties = new HashMap<>();
            ConfigProvider.getConfig().getConfigSources()
                    .forEach(s -> s.getProperties()
                            .forEach((k, v) -> properties.put(k, v)));
        }
    
        @Override
        public Set<String> getChildKeys(String path) {
            if (path == null) {
                return properties.keySet();
            }
    
            return properties.keySet().stream()
                    .filter(key -> key.startsWith(path) && key.length() > path.length() && key.charAt(path.length()) == '.')
                    .collect(Collectors.toSet());
        }
    
        @Override
        public String getValue(String propertyName) {
            return ConfigProvider.getConfig().getValue(propertyName, String.class);
        }
    }
  3. register builder

    public class SdkConfiguration {
      @Produces
      @ApplicationScoped
      ConfigurationBuilder sdkConfigurationBuilder() {
          return new ConfigurationBuilder(new SdkConfigurationSource()).root("az");
      }
    
      @Produces
      @ApplicationScoped
      TokenCredential defaultTokenCredential(ConfigurationBuilder sdkConfigBuilder) {
          return new DefaultAzureCredentialBuilder().configuration(sdkConfigBuilder.build()).build();
      }
    }
  4. register SDK client builder

    @Produces
    @ApplicationScoped
    @DefaultBean // in case there is another one
    ConfigurationClientBuilder configurationClientBuilder(ConfigurationBuilder sdkConfigBuilder, TokenCredential tokenCredential) {
        return new ConfigurationClientBuilder()
                .credential(tokenCredential)
                .configuration(sdkConfigBuilder.buildSection("appconfig"));
    }
  5. named client

    @Qualifier
    @Retention(RUNTIME)
    @Target({METHOD, FIELD})
    public @interface Secondary {
    }
    
    @Produces
    @ApplicationScoped
    @Secondary
    ConfigurationClientBuilder secondaryConfigurationClientBuilder(ConfigurationBuilder sdkConfigBuilder, TokenCredential tokenCredential) {
        return new ConfigurationClientBuilder()
                .credential(tokenCredential)
                .configuration(sdkConfigBuilder.buildSection("appconfig-secondary"));
    }

Resolving SDK builder/client

@Path("/hello")
public class GreetingResource {

    @Inject
    ConfigurationClient appconfig;


    @Inject @Secondary
    ConfigurationClient appconfigSecondary;

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return "Got configuration: " + appconfig.getConfigurationSetting("for", "bar").getValue() + ", secondary: " + appconfigSecondary.getConfigurationSetting("for", "bar").getValue();
    }
}

Micronaut

  1. application.yml

    az:
      http:
        client:
          application-id: test${appid}
        logging:
          log-level: body
      appconfig:
        endpoint: https://lmolkova-appconfig.azconfig.io
      appconfig-secondary:
        endpoint: https://lmolkova-appconfig.azconfig.io
  2. implement configuration source

    class SdkConfigurationSource implements ConfigurationSource {
    
        Set<String> properties;
        ApplicationContext appContext;
        public SdkConfigurationSource(ApplicationContext appContext) {
            this.properties = new HashSet<>();
            this.appContext = appContext;
            this.appContext.getEnvironment().getPropertySources().stream()
                    .forEach(source -> source.forEach(prop -> this.properties.add(prop)));
        }
    
        @Override
        public Set<String> getChildKeys(String path) {
            if (path == null) {
                return properties;
            }
    
            return properties.stream()
                    .filter(key -> key.startsWith(path) && key.length() > path.length() && key.charAt(path.length()) == '.')
                    .collect(Collectors.toSet());
        }
    
        @Override
        public String getValue(String propertyName) {
            return appContext.getProperty(propertyName, String.class).orElse(null);
        }
    }
  3. register configuration builder

    @Factory
    public class SdkConfiguration {
        @Inject
        ApplicationContext applicationContext;
    
        @Singleton
        ConfigurationBuilder configurationBuilder() {
            return new ConfigurationBuilder(new SdkConfigurationSource(applicationContext)).root("az");
        }
    }
  4. register SDK client builder

    @Factory
    public class AppConfiguration {
        @Inject
        ConfigurationBuilder sdkConfigurationBuilder;
    
        @Singleton
        @Named("appconfig-primary") // only needed if there is more than one client
        ConfigurationClient configurationClientBuilder(TokenCredential tokenCredential) {
            return new ConfigurationClientBuilder()
                    .credential(tokenCredential)
                    .configuration(sdkConfigurationBuilder.buildSection("appconfig"))
                    .buildClient();
        }
    
        @Singleton
        TokenCredential defaultTokenCredential() {
            return new DefaultAzureCredentialBuilder().configuration(sdkConfigurationBuilder.build()).build();
        }
    }
  5. named client

    @Singleton
    @Named("appconfig-secondary")
    ConfigurationClient configurationSecondaryClientBuilder(TokenCredential tokenCredential) {
        return new ConfigurationClientBuilder()
                .credential(tokenCredential)
                .configuration(sdkConfigurationBuilder.buildSection("appconfig-secondary"))
                .buildClient();
    }

Resolving SDK builder/client

@Controller("/hello")
public class HelloController {

    @Inject
    @Named("appconfig-primary")
    ConfigurationClient appconfig;

    @Inject
    @Named("appconfig-secondary")
    ConfigurationClient appconfigSecondary;

    @Get
    @Produces(MediaType.TEXT_PLAIN)
    public String index() {
        return "Got configuration: " + appconfig.getConfigurationSetting("foo", "bar").getValue();
    }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment