Skip to content

Instantly share code, notes, and snippets.

@michalmela
Last active September 15, 2023 14:27
Show Gist options
  • Save michalmela/03ab6612425ca1cd1bbe39fd1f643233 to your computer and use it in GitHub Desktop.
Save michalmela/03ab6612425ca1cd1bbe39fd1f643233 to your computer and use it in GitHub Desktop.
[hocon-spring] A set of examples of using HOCON / TypesafeConfig (the latter being the more important part, with the former being just one of the optional formats, but HOCON sounds so much better than the mouthful TypesafeConfig) outside of their typical use cases

Known limitations

If the @PropertySource annotation is given a value of a list longer than a single file location, it's not possible to create a single PropertySource with a single, cascading Config object. Spring is quite opinionated in creatingseparate property source for each given value here. If required to have a list of cascaded config files it would be best to just repurpose the factory class to the specific purposes and use it instead.

Spring is also quite opinionated in what it allows custom PropertySource implementations to do - namely, it allows it to resolve string values and always does the type mapping itself. Which is quite a different set of responsibilities than what the default, .properties-based property sources can do (which don't really follow the property source extension API introduced in Spring 4.3). Config can do so much more with mapping the config values to a respectable amount of target types but it's not allowed to by Spring ☹️.

What about Spring Boot?

I.e., using HOCON .conf files instead of the application.yml/application.properties? Done already, see: https://github.com/zeldigas/spring-hocon-property-source

Unfortunately, making an elegant example in a gist is not easily possible.

Spring Boot doesn't really use the property sources idea and APIs, instead having its own PropertyLoaders. By default, there are .properties and .yaml PropertyLoaders only. To extend the list, one must add a descriptor to the application's META-INF (is it me or does it seem like disregarding the fact that the Spring is an IoC container itself, instead relying on a custom mechanism that resembles ServiceLoaders? 😃), which is still not a big deal in a real-world application, but is no longer as gistable as a non-boot application example.

Useful links

# companion to the `minimalisticSpringApp`
# BTW, this comment will get preserved when rendering the config in logs
minimalisticApp {
valueAnnotationService {
moneyToMakeInASingleBatch = 100
currencies = ["USD", "PLN"]
}
envApiService {
moneyToMakeInASingleBatch = 200
currencies = ${minimalisticApp.valueAnnotationService.currencies}
currencies += "GBP"
}
configApiService {
moneyToMakeInASingleBatch = 300
currencies = ${minimalisticApp.valueAnnotationService.currencies} ["EUR"]
}
# not really used in the example, just to show some syntax options
someController {
colons.are.fine.too : true
uri.base = /some
uri {
moneyEndpoint = ${minimalisticApp.someController.uri.base}/money
othreEndpoint = ${minimalisticApp.someController.uri.base}/other
}
}
}
someLibrary {
database {
uri = "jdbc:sth:whatever"
password = "test123"
}
}
// place next to the `app.conf` file
// then run with `groovy minimalisticSpringApp.groovy`
//
@Grab(group = 'com.typesafe', module = 'config', version = '1.3.3')
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
@Grab(group='ch.qos.logback', module='logback-classic', version='1.2.3')
import org.slf4j.*
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.beans.factory.annotation.Value
@Grab(group = 'org.springframework', module = 'spring-context', version = '5.1.3.RELEASE')
import org.springframework.context.annotation.*
import org.springframework.core.env.Environment
import org.springframework.core.io.support.EncodedResource
import org.springframework.core.io.support.PropertySourceFactory
// ⚠️#1⚠️ Extremely minimalistic groovy/spring/typesafe config app
// -- requires only an app.conf to be present at the execution path
def ctx = new AnnotationConfigApplicationContext(AppConfig.class)
println("💰1")
ctx.getBean('valueAnnotationService', ImportantBusinessService).makeMoney()
println("💰2")
ctx.getBean('envApiService', ImportantBusinessService).makeMoney()
println("💰3")
ctx.getBean('configApiService', ImportantBusinessService).makeMoney()
// ⚠️#2⚠️ very basic configuration example – @PropertySource has the most interesting parts
@Configuration
@PropertySource(
value = [ 'file:./app.conf'],
factory = TypesafeConfigPropertySourceFactory.class,
name = 'tsConfig',
ignoreResourceNotFound = false
)
class AppConfig {
// ⚠️#3⚠️ some optional plumbing code for the sake of example
// - Environment is a spring API we can use (among others) to retrieve *properties* programmatically
// - by declaring the typeSafeConfig as a bean we can... access it as a bean – the Config API blows the Spring
// property sources of the water and nicer to use unless the beans need to be property-source-type-agnostic
@Autowired Environment e
@Bean Config typeSafeConfig() {
e.getPropertySources().get('tsConfig').getSource() as Config
}
// ⚠️#5⚠️ basic example – so the Typesafe Config may serve as a drop-in replacement for the traditional (🤮)
// .properties files and the receiver is none the wiser
@Bean ImportantBusinessService valueAnnotationService(
@Value('${minimalisticApp.valueAnnotationService.moneyToMakeInASingleBatch}') int property,
// not really sure why the SpEL is needed...
@Value('#{\'${minimalisticApp.valueAnnotationService.currencies}\'.split(\',\')}') List<String> currencies
) {
new ImportantBusinessService(property, currencies)
}
// ⚠️#6⚠️ another basic, drop-in-replacement example - this time without annotations
@Bean ImportantBusinessService envApiService() {
new ImportantBusinessService(
e.getRequiredProperty('minimalisticApp.valueAnnotationService.moneyToMakeInASingleBatch', Integer),
e.getRequiredProperty('minimalisticApp.valueAnnotationService.currencies', List),
)
}
// ⚠️#7⚠️ Config API usage example – although not a very exhaustive one; Config API allows, e.g.:
// - to separate out a logical "branch" of the configuration tree into another, limited Config object
// - convert a logical branch into a HashMap or Properties objects
// - iterate over all the configuration keys under a specific parent (!!!)
@Bean ImportantBusinessService configApiService() {
// ⚠️#8⚠️ alternative example
//def specificServiceConfig = typeSafeConfig().getConfig('minimalisticApp.configApiService')
//new ImportantBusinessService(
// specificServiceConfig.getInt('moneyToMakeInASingleBatch'),
// specificServiceConfig.getStringList('currencies'),
//)`
new ImportantBusinessService(
typeSafeConfig().getInt('minimalisticApp.configApiService.moneyToMakeInASingleBatch'),
typeSafeConfig().getStringList('minimalisticApp.configApiService.currencies'),
)
}
}
// ⚠️#4⚠️ – a class with a single configurable property we will use as an example
class ImportantBusinessService {
private Integer moneyToMakeInASingleBatch
private List<String> currencies
ImportantBusinessService(moneyToMakeInASingleBatch, currencies) {
this.moneyToMakeInASingleBatch = moneyToMakeInASingleBatch
this.currencies = currencies
}
def makeMoney() {
currencies.forEach {
println "just made $moneyToMakeInASingleBatch $it, ka CHING"
}
}
}
/*
*
*
*
here be
^\ ^
/ \\ / \
/. \\/ \ |\___/|
*----* / / | \\ \ __/ O O\
| / / / | \\ \_\/ \ \
/ /\/ / / | \\ _\/ '@___@
/ / / / | \\ _\/ |U
| | / / | \\\/ |
\ | /_ / | \\ ) \ _|_
\ \ ~-./_ _ | .- ; ( \_ _ _,\'
~ ~. .-~-.|.-* _ {-,
\ ~-. _ .-~ \ /\'
\ } { .*
~. '-/ /.-~----.
~- _ / >..----.\\\
~ - - - - ^}_ _ _ _ _ _ _.-\\\
i.e. classes below are included just to make the script as self-contained as possible – you'd normally just
have them (or some like them) on classpath and forget about them
*/
class TypesafeConfigPropertySourceFactory implements PropertySourceFactory {
private static final Logger LOG = LoggerFactory.getLogger(TypesafeConfigPropertySourceFactory.class)
@Override
org.springframework.core.env.PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
Config config = ConfigFactory.parseFile(resource.getResource().getFile())
.withFallback(ConfigFactory.load())
.resolve()
LOG.info("Creating property source named {} of {}:\n{}",
name,
config.origin(),
config.root().render()
)
new TypesafeConfigPropertySource(
Optional.ofNullable(name).orElse("default"),
config
)
}
private static class TypesafeConfigPropertySource extends org.springframework.core.env.PropertySource<Config> {
TypesafeConfigPropertySource(String name, Config source) { super(name, source) }
@Override
Object getProperty(String path) {
String pathWithoutColon = path.contains(":") ? path.substring(0, path.indexOf(':')) : path
if (this.@source.hasPath(pathWithoutColon)) {
return Optional.ofNullable(this.@source.getAnyRef(pathWithoutColon))
.filter { property -> !(property instanceof Map) }
.orElse(null)
} else return null
}
}
}
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.core.io.support.PropertySourceFactory;
import java.io.IOException;
import java.util.Map;
import java.util.Optional;
/**
* A {@link PropertySourceFactory} (a Spring component allowing the usage of a custom {@link PropertySource}
* implementation (e.g. different than a ".properties" file based) for the
* {@link org.springframework.context.annotation.PropertySource @PropertySource} annotation) allowing the usage of
* {@link Config TypeSafe Config} files (in, e.g., the famous HOCON format) as property sources.
*
* The default values for the application should be declared in a {@code reference.conf} file at the top of the
* classpath (i.e. {@code src/main/resources/reference.conf}, typically), while the
* {@link org.springframework.context.annotation.PropertySource#value()} shall point to a <strong>single</strong>
* file containing the overrides.
*
* Example usage:
*
* <pre>
* import com.typesafe.config.Config;
* import org.springframework.beans.factory.annotation.Autowired;
* import org.springframework.context.annotation.Bean;
* import org.springframework.context.annotation.Configuration;
* import org.springframework.context.annotation.PropertySource;
* import org.springframework.core.env.ConfigurableEnvironment;
*
* &#064;PropertySource(
* // the specific value given below emulates the default Akka (and TC config in general) setup – the override file
* // will be pointed to by the "config.file" system property
* value = {"file:${config.file}"},
* factory = TypesafeConfigPropertySourceFactory.class,
* name = PROPERTY_SOURCE_NAME, // required only if using the bean declaration below
* ignoreResourceNotFound = true)
* &#064;Configuration
* public class AppConfig {
*
* &#064;Autowired
* private ConfigurableEnvironment e;
*
* static final String PROPERTY_SOURCE_NAME = "Typesafe Config Property Source";
*
* /&#42;&#42; OPTIONAL - allows for autowiring the Config bean as an alternative to autowiring the Environment object
* &#42; for resolving placeholders, since the Config API may be considered preferable by the Config-aware client code
* &#42;&#42;/
* &#064;Bean
* public Config typeSafeConfig() {
* return ((org.springframework.core.env.PropertySource&lt;Config&gt;) e.getPropertySources()
* .get(PROPERTY_SOURCE_NAME))
* .getSource();
* }
* }
* </pre>
*
* @see <a href="https://github.com/typesafehub/config">https://github.com/typesafehub/config</a>
* @author jmelon.github.io
* @since Spring 4.3
*/
public class TypesafeConfigPropertySourceFactory implements PropertySourceFactory {
private static final Logger LOG = LoggerFactory.getLogger(TypesafeConfigPropertySourceFactory.class);
@Override
public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
Config config = ConfigFactory.parseFile(resource.getResource().getFile())
.withFallback(ConfigFactory.load())
.resolve();
LOG.info("Creating property source named {} of {}:\n{}",
name,
config.origin(),
config.root().render()
);
return new TypesafeConfigPropertySource(
Optional.ofNullable(name).orElse("default"),
config
);
}
private static class TypesafeConfigPropertySource extends PropertySource<Config> {
TypesafeConfigPropertySource(String name, Config source) {
super(name, source);
}
@Override
public Object getProperty(String path) {
String pathWithoutColon = path.contains(":") ? path.substring(0, path.indexOf(':')) : path;
if (source.hasPath(pathWithoutColon)) {
return Optional.ofNullable(source.getAnyRef(pathWithoutColon))
.filter(property -> !(property instanceof Map))
.orElse(null);
} else return null;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment