7

The following is a minimal example, showing my problem.

Main.kt:

package com.mycompany.configurationpropertiestest

import org.slf4j.LoggerFactory
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.ConstructorBinding
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.runApplication
import org.springframework.scheduling.annotation.EnableScheduling
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Service


@SpringBootApplication
@EnableScheduling
@EnableConfigurationProperties(FooServiceConfig::class)
class Application

fun main(args: Array<String>) {
    runApplication<Application>(*args)
}


@ConstructorBinding
@ConfigurationProperties("configurationpropertiestest.foo")
data class FooServiceConfig(
    val interval: Int = 1000,
    val message: String = "hi"
)


@Service
class FooService(
    private val myConfig: FooServiceConfig
) {
    private val log = LoggerFactory.getLogger(this.javaClass)
    //@Scheduled(fixedDelayString = "#{@FooServiceConfig.interval}")
    //@Scheduled(fixedDelayString = "#{@myConfig.interval}")
    @Scheduled(fixedDelayString = "\${configurationpropertiestest.foo.interval}")
    fun talk() {
        log.info(myConfig.message)
    }
}

(@ConstructorBinding is used to allow having the members of FooServiceConfig immutable.)

application.yml:

configurationpropertiestest:
  foo:
    interval: 500
    message: "hi"

Test.kt:

package com.mycompany.configurationpropertiestest

import org.junit.Test
import org.junit.runner.RunWith
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.junit4.SpringRunner


@RunWith(SpringRunner::class)
@SpringBootTest
class Test {
    @Test
    fun `sit and wait`() {
        Thread.sleep(3000)
    }
}

It works, but it only does, because I reference interval in the @Scheduled annotation like so:

@Scheduled(fixedDelayString = "\${configurationpropertiestest.foo.interval}")

This somewhat breaks the nicely isolated configuration of my service. It suddenly has to know about external things, which it should now need to know about.

Ideally, it would only access its configuration either by the type of the bean:

@Scheduled(fixedDelayString = "#{@FooServiceConfig.interval}")

or by the injected instance:

@Scheduled(fixedDelayString = "#{@myConfig.interval}")

But these attempts result in No bean named 'FooServiceConfig' available and No bean named 'myConfig' available respectively.

Any idea of how I can achieve to access only the config bean and not the global config value?

Tobias Hermann
  • 9,936
  • 6
  • 61
  • 134
  • The bean name gets a lowercase first char; try `@fooServiceConfig`. – Gary Russell Jan 16 '20 at 15:15
  • @GaryRussell Thanks, but this also does not work. `@Scheduled(fixedDelayString = "#{@fooServiceConfig.interval}")` results in `No bean named 'fooServiceConfig' available`. – Tobias Hermann Jan 16 '20 at 15:17
  • Try adding `@Component` to `FooServiceConfig` - perhaps `ConfigurationProperties` alone doesn't make it a bean. – Gary Russell Jan 16 '20 at 15:19
  • @GaryRussell Adding `@Component` to `data class FooServiceConfig` results in `org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'fooServiceConfig': @EnableConfigurationProperties or @ConfigurationPropertiesScan must be used to add @ConstructorBinding type com.mycompany.configurationpropertiestest.FooServiceConfig` despite `class Application` already having `@EnableConfigurationProperties(FooServiceConfig::class)`. – Tobias Hermann Jan 16 '20 at 15:24
  • I am not really familiar with the `@ConstructorBinding` and `@ConfigurationProperties` internals. I was just answering from the SpEL perspective that to reference a `@Bean` whose name is created from a class name (such as with `@Component`), it's name has a lower case first char. The rest is out of my domain; sorry. – Gary Russell Jan 16 '20 at 15:29

2 Answers2

12

If you don't mind making FooService.myConfig public, this should work:

@Service
class FooService(val myConfig: FooServiceConfig) {

    val log = LoggerFactory.getLogger(this.javaClass)

    @Scheduled(fixedDelayString = "#{@fooService.myConfig.interval}")
    fun talk() {
        log.info(myConfig.message)
    }
}

UPDATE:

Apparently Spring changes the names of the beans annotated with the @ConstructorBinding annotation to [configuration-properties-value]-[fully-qualified-bean-name]. FooServiceConfig ends up as configurationpropertiestest.foo-com.mycompany.configurationpropertiestest.FooServiceConfig

So, despite being quite ugly, this should work as well:

@Service
class FooService(private val myConfig: FooServiceConfig) {

    val log = LoggerFactory.getLogger(this.javaClass)

    @Scheduled(fixedDelayString = "#{@'configurationpropertiestest.foo-com.mycompany.configurationpropertiestest.FooServiceConfig'.interval}")
    fun talk() {
        log.info(myConfig.message)
    }
}

Finally, the last option, answering the title question: How to reference a bean by type in a SpEL? You can do it by calling beanFactory.getBean:

@Service
class FooService(private val myConfig: FooServiceConfig) {

    val log = LoggerFactory.getLogger(this.javaClass)

    @Scheduled(fixedDelayString = "#{beanFactory.getBean(T(com.mycompany.configurationpropertiestest.FooServiceConfig)).interval}")
    fun talk() {
        log.info(myConfig.message)
    }
}
qwazer
  • 7,174
  • 7
  • 44
  • 69
Mafor
  • 9,668
  • 2
  • 21
  • 36
  • Thanks a lot for the good investigation. Yeah, using that long name really is ugly. Even the IDE complains (https://i.imgur.com/rO8kBE8.png). But I have to admit that it works. :D Do you know if this derived name is something that can be relied upon, i.e., it's part of the official specification or is ist just some implementation details, that could change with future versions of Spring Boot? Also, I guess there is no way to access `myConfig` in the annotation, right? – Tobias Hermann Jan 19 '20 at 06:45
  • @TobiasHermann No, I couldn't find anything about the bean name in the documentation, it just mentions that `@ConstructorBinding` cannot be used in conjunction with `@Component` etc.: 'You cannot use constructor binding with beans that are created by the regular Spring mechanisms'. See [constructor-binding](https://docs.spring.io/spring-boot/docs/2.2.0.RELEASE/reference/html/spring-boot-features.html#boot-features-external-config-constructor-binding). Indded, it looks to me like a hack. Personally, I would stick to the first option. – Mafor Jan 19 '20 at 12:58
  • @TobiasHermann Sorry, I was wrong, they mention this: ''When the ConfigurationProperties bean is registered using configuration property scanning or via EnableConfigurationProperties, the bean has a conventional name: -, where is the environment key prefix specified in the ConfigurationProperties annotation and is the fully qualified name of the bean. If the annotation does not provide any prefix, only the fully qualified name of the bean is used.' – Mafor Jan 19 '20 at 13:02
  • That's good news. At least the ugly solution is stable. Thanks a lot! :) – Tobias Hermann Jan 20 '20 at 06:21
  • I tried the first solution in Java (not in Kotlin) and I always get circular reference problem in that class... Could you please help me, how to fix it? I tried adding @Lazy annotation but it didn't work out. The second and third solutions work though – Eduard Grigoryev Feb 01 '23 at 15:08
1

I changed a little your code and for me it working. Main change was inject FooServiceConfig with @Autowired. Then in scheduler I could write: "#{@fooServiceConfig.interval}"

@SpringBootApplication
@EnableScheduling
class Application

fun main(args: Array<String>) {
    SpringApplication.run(Application::class.java, *args)
}

@Configuration
@EnableConfigurationProperties
@ConfigurationProperties("configurationpropertiestest.foo")
data class FooServiceConfig(
        var interval: Int = 1000,
        var message: String = "hi"
)

@Service
class FooService {
    private val log = LoggerFactory.getLogger(this.javaClass)

    @Autowired
    lateinit var fooServiceConfig:FooServiceConfig

    @Scheduled(fixedDelayString = "#{@fooServiceConfig.interval}")
    fun talk() {
        log.info(fooServiceConfig.message)
    }
}

UPDATE

If you need @ConstructorBinding you can access to its values in other way. Introduce other config class that for example extract interval value and expose it as a new bean. After that you can refer to this bean later in @Scheduled

@Configuration
class DelayConfig{

    @Bean(name = ["talkInterval"])
    fun talkInterval(fooServiceConfig: FooServiceConfig): Int {
        return fooServiceConfig.interval
    }
}

@Service
class FooService(
        private val myConfig: FooServiceConfig
) {
    private val log = LoggerFactory.getLogger(this.javaClass)

    @Scheduled(fixedDelayString = "#{@talkInterval}")
    fun talk() {
        log.info(myConfig.message)
    }
}
lczapski
  • 4,026
  • 3
  • 16
  • 32
  • Thanks a lot. I can verify your solution is working. Injecting the config with `@Autowired` is not even needed (https://gist.github.com/Dobiasd/c57ee9aa6f8c2ab5930c523c9342dc74). The important change is not using `@ConstructorBinding` (but `@Configuration`) instead. The issue is, that I'd like to avoid the values in the configuration being mutable (`var` instead of `val`). That's why I was using `@ConstructorBinding`, so I'd like to find a solution allowing for that. I just added a remark to my question regarding `ConstructorBinding`. Nevertheless, your answer reveals something interesting. :) – Tobias Hermann Jan 17 '20 at 14:32
  • @TobiasHermann i came up to other solution: it use `ConstructorBinding` and introduce clear naming. – lczapski Jan 22 '20 at 11:57
  • Despite the boilerplate code needed for every value, that's quite nice. Thanks! :) – Tobias Hermann Jan 22 '20 at 16:15
  • @TobiasHermann It doesn't have to be for every value. `FooServiceConfig` can be pass as a whole bean as well. For example: `"#{@conf.Interval}")` – lczapski Jan 22 '20 at 17:39