Skip to content

Commit 2ecc1f5

Browse files
committed
Type-safe customization of an existing application
The goal of this commit is to allow the configuration of an existing application with type-safe configuration properties instead of using error prone environment variables like now. One of the design challenge is to be able to use configuration properties in the DSL directly which is tricky because currently they are beans and the context is not refreshed yet. The proposed solution is to change `inline fun <reified T : Any> configurationProperties(prefix: String = "")` to `inline fun <reified T : Any> configurationProperties(properties: T? = null, prefix: String = ""): T` and to use `fun` instead of `val` for the application and the configuration. Likely to be refined, but this is a major Kofu principle. Closes spring-atticgh-228
1 parent d0584c2 commit 2ecc1f5

File tree

17 files changed

+108
-47
lines changed

17 files changed

+108
-47
lines changed

autoconfigure-adapter/src/main/java/org/springframework/boot/context/properties/FunctionalConfigurationPropertiesBinder.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,10 @@ public class FunctionalConfigurationPropertiesBinder {
4141

4242
public FunctionalConfigurationPropertiesBinder(ConfigurableApplicationContext context) {
4343
this.context = context;
44-
this.propertySources = new PropertySourcesDeducer(context).getPropertySources();
44+
this.propertySources = new FunctionalPropertySourcesDeducer(context).getPropertySources();
4545
this.binder = new Binder(ConfigurationPropertySources.from(propertySources),
4646
new PropertySourcesPlaceholdersResolver(this.propertySources),
47-
new ConversionServiceDeducer(context).getConversionService(),
47+
null,
4848
(registry) -> context.getBeanFactory().copyRegisteredEditorsTo(registry));
4949
}
5050

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package org.springframework.boot.context.properties;
2+
3+
import org.apache.commons.logging.Log;
4+
import org.apache.commons.logging.LogFactory;
5+
6+
import org.springframework.context.ApplicationContext;
7+
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
8+
import org.springframework.core.env.ConfigurableEnvironment;
9+
import org.springframework.core.env.Environment;
10+
import org.springframework.core.env.MutablePropertySources;
11+
import org.springframework.core.env.PropertySources;
12+
13+
/**
14+
* Similar to {@code PropertySourcesDeducer} without {@link PropertySourcesPlaceholderConfigurer}
15+
* bean retrieval since application context is not refreshed yet.
16+
* TODO Support property placeholder configuration
17+
*/
18+
public class FunctionalPropertySourcesDeducer {
19+
20+
private final ApplicationContext applicationContext;
21+
22+
FunctionalPropertySourcesDeducer(ApplicationContext applicationContext) {
23+
this.applicationContext = applicationContext;
24+
}
25+
26+
public PropertySources getPropertySources() {
27+
MutablePropertySources sources = extractEnvironmentPropertySources();
28+
if (sources != null) {
29+
return sources;
30+
}
31+
throw new IllegalStateException("Unable to obtain PropertySources from "
32+
+ "PropertySourcesPlaceholderConfigurer or Environment");
33+
}
34+
35+
private MutablePropertySources extractEnvironmentPropertySources() {
36+
Environment environment = this.applicationContext.getEnvironment();
37+
if (environment instanceof ConfigurableEnvironment) {
38+
return ((ConfigurableEnvironment) environment).getPropertySources();
39+
}
40+
return null;
41+
}
42+
}

kofu/src/main/kotlin/org/springframework/fu/kofu/ConfigurationDsl.kt

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,17 @@ open class ConfigurationDsl(private val dsl: ConfigurationDsl.() -> Unit): Abstr
4141
}
4242

4343
/**
44-
* Specify the class and the optional prefix of configuration properties, which is the same mechanism than regular
45-
* [Spring Boot configuration properties mechanism](https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html#boot-features-external-config-typesafe-configuration-properties),
46-
* without `@ConfigurationProperties` annotation.
44+
* Bind and return the specified configuration properties class. If you need a bean,
45+
* register it via the [beans] DSL.
4746
*
4847
* @sample org.springframework.fu.kofu.samples.configurationProperties
4948
*/
50-
inline fun <reified T : Any> configurationProperties(prefix: String = "") {
51-
context.registerBean("${T::class.java.simpleName.toLowerCase()}ConfigurationProperties") {
52-
FunctionalConfigurationPropertiesBinder(context).bind(prefix, Bindable.of(T::class.java)).get()
49+
inline fun <reified T : Any> configurationProperties(properties: T? = null, prefix: String = ""): T {
50+
val properties = properties ?: FunctionalConfigurationPropertiesBinder(context).bind(prefix, Bindable.of(T::class.java)).get()
51+
context.registerBean<T>("${T::class.java.simpleName.toLowerCase()}ConfigurationProperties") {
52+
properties
5353
}
54+
return properties
5455
}
5556

5657
/**

kofu/src/test/kotlin/org/springframework/fu/kofu/ApplicationDslTests.kt

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,10 @@ class ApplicationDslTests {
7777
@Test
7878
fun `Application properties`() {
7979
val app = application(WebApplicationType.NONE) {
80-
configurationProperties<City>("city")
81-
}
82-
with(app.run()) {
83-
assertEquals(getBean<City>().name, "San Francisco")
84-
close()
80+
val properties = configurationProperties<City>(prefix = "city")
81+
assertEquals(properties.name, "San Francisco")
8582
}
83+
app.run().close()
8684
}
8785

8886
@Test

kofu/src/test/kotlin/org/springframework/fu/kofu/samples/application.kt

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,31 +31,37 @@ private fun applicationDsl() {
3131
beans {
3232
bean<Foo>()
3333
}
34-
configurationProperties<City>("city")
34+
3535
}
3636

3737
fun main(args: Array<String>) = app.run()
3838
}
3939

4040
private fun applicationDslWithConfiguration() {
41-
val conf = configuration {
41+
fun conf(cityName: String) = configuration {
4242
beans {
4343
bean<Foo>()
44+
bean {
45+
Bar(cityName)
46+
}
4447
}
4548
}
46-
val app = application(WebApplicationType.NONE) {
47-
logging {
48-
level = LogLevel.WARN
49+
fun app(properties: CityProperties? = null) = application(WebApplicationType.NONE) {
50+
with(configurationProperties(properties, prefix = "city")) {
51+
logging {
52+
level = LogLevel.WARN
53+
}
54+
enable(conf(name))
4955
}
50-
configurationProperties<City>("city")
51-
enable(conf)
5256
}
5357

54-
fun main(args: Array<String>) = app.run()
58+
fun main(args: Array<String>) = app().run()
5559
}
5660
class Foo
5761

58-
class City(val name: String, val country: String)
62+
class Bar(value: String)
63+
64+
class CityProperties(val name: String, val country: String)
5965

6066
interface UserRepository {
6167
fun init()

kofu/src/test/kotlin/org/springframework/fu/kofu/samples/properties.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ import org.springframework.fu.kofu.application
66
fun configurationProperties() {
77
application(WebApplicationType.NONE) {
88

9-
/** Will bind `sample.message` property typically defined in an `application.configurationProperties`
9+
/** Bind `sample.message` property typically defined in an `application.properties`
1010
* or `application.yml` file to [SampleProperties.message]. Typically used by retrieving
1111
* a [SampleProperties] bean.
1212
*/
13-
configurationProperties<SampleProperties>(prefix = "sample")
13+
val properties = configurationProperties<SampleProperties>(prefix = "sample")
14+
1415
}
1516
}
1617

kofu/src/test/kotlin/org/springframework/fu/kofu/samples/webflux.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ private fun webFluxApplicationDsl() {
175175
level = LogLevel.INFO
176176
level("org.springframework", LogLevel.DEBUG)
177177
}
178-
configurationProperties<City>(prefix = "city")
178+
val city = configurationProperties<CityProperties>(prefix = "city")
179179
enable(dataConfiguration)
180180
enable(webConfiguration)
181181
}

samples/kofu-coroutines-mongodb/src/main/kotlin/org/springframework/fu/sample/coroutines/Application.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ package org.springframework.fu.sample.coroutines
33
import org.springframework.boot.WebApplicationType
44
import org.springframework.fu.kofu.application
55

6-
val app = application(WebApplicationType.REACTIVE) {
6+
val app = application(WebApplicationType.REACTIVE) {
7+
configurationProperties<SampleProperties>(prefix = "sample")
78
enable(dataConfig)
89
enable(webConfig)
9-
configurationProperties<SampleProperties>("sample")
1010
}
1111

1212
fun main() {

samples/kofu-coroutines-r2dbc/src/main/kotlin/com/sample/Application.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import org.springframework.fu.kofu.application
2222

2323
@FlowPreview
2424
val app = application(WebApplicationType.REACTIVE) {
25-
configurationProperties<SampleProperties>("sample")
25+
configurationProperties<SampleProperties>(prefix = "sample")
2626
enable(dataConfig)
2727
enable(webConfig)
2828
}

samples/kofu-reactive-cassandra/src/main/kotlin/com/sample/Application.kt

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,19 @@ import org.springframework.boot.WebApplicationType
2020
import org.springframework.fu.kofu.application
2121
import org.testcontainers.containers.CassandraContainer
2222

23-
val app = application(WebApplicationType.REACTIVE) {
24-
configurationProperties<SampleProperties>("sample")
25-
enable(dataConfig)
26-
enable(webConfig)
23+
fun app(properties: ApplicationProperties) = application(WebApplicationType.REACTIVE) {
24+
with(configurationProperties(properties, prefix = "sample")) {
25+
enable(dataConfig(cassandraHost, cassandraPort))
26+
enable(webConfig(serverPort))
27+
}
2728
}
2829

2930
fun main() {
3031
class KCassandraContainer : CassandraContainer<KCassandraContainer>() // https://github.com/testcontainers/testcontainers-java/issues/318
3132
val cassandraContainer = KCassandraContainer().withInitScript("schema.cql")
3233
cassandraContainer.start()
33-
app.run(args = arrayOf("--cassandra.port=${cassandraContainer.firstMappedPort}", "--cassandra.host=${cassandraContainer.containerIpAddress}"))
34+
val properties = ApplicationProperties(
35+
cassandraHost = cassandraContainer.containerIpAddress,
36+
cassandraPort = cassandraContainer.firstMappedPort)
37+
app(properties).run()
3438
}

0 commit comments

Comments
 (0)