Spring #Value injection conversion not working in #SpringJUnitConfig test - spring

I couldn't find a way to make the automatic value conversion when injecting #Value work when using #SpringJUnitConfig test.
For example, for the given code:
#SpringBootApplication
class DemoApplication
fun main(args: Array<String>) {
runApplication<DemoApplication>(*args)
}
#Component
class ValueConvertInjection(#Value("2ms") val duration : java.time.Duration) {
#PostConstruct
fun init() = println("Duration converted: $duration")
}
The given test will fail:
#SpringJUnitConfig(classes = [ValueConvertInjection::class])
class JUnitValueConvertInjectionTest {
#Autowired
lateinit var vi :ValueConvertInjection
#Test
fun test() = assert(vi.duration == Duration.ofMillis(2))
}
with the following exception:
...
Caused by: org.springframework.beans.ConversionNotSupportedException: Failed to convert value of type 'java.lang.String' to required type 'java.time.Duration'; nested exception is java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String' to required type 'java.time.Duration': no matching editors or conversion strategy found
at org.springframework.beans.TypeConverterSupport.convertIfNecessary(TypeConverterSupport.java:76)
... 85 more
Caused by: java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String' to required type 'java.time.Duration': no matching editors or conversion strategy found
at org.springframework.beans.TypeConverterDelegate.convertIfNecessary(TypeConverterDelegate.java:262)
at org.springframework.beans.TypeConverterSupport.convertIfNecessary(TypeConverterSupport.java:73)
... 89 more
I couldn't find what I should add to the context so any of the Spring conversions will be registered and picked.

Spring Boot intializes the context with a conversion service, which is what you need:
public class ConversionConfiguration {
#Bean
fun conversionService() = ApplicationConversionService.getSharedInstance()
}
#SpringJUnitConfig(classes = [
ValueConvertInjection::class,
ConversionConfguration::class
]) {
class JUnitValueConvertInjectionTest {
#Autowired
lateinit var vi: ValueConvertInjection
#Test
fun test() = assert(vi.duration == Duration.ofMillis(2))
}

to use the duration class, can try as below
#Component
#ConfigurationProperties(prefix="time")
class ValueConvertInjection {
Duration val = Duration.ofSeconds(60);
#PostConstruct
fun init() = println("Duration converted: $duration")
}
and in your src/test/resources,create application.yml
time:
val:
2ms

Related

How to register custom configuration properties converter #ConfigurationPropertiesBinding

In Spring Boot project, the configuration in the YML file can be automatically converted to an #ConfigurationProperties annotated bean. But I need to override behavior to make non standard conversion because I inject value from environmental variable (which is a sting) but it should be AS map.
There is error
***************************
APPLICATION FAILED TO START
***************************
Description:
Failed to bind properties under 'app.push.firebase.application-keys' to java.util.Map<com.example.services.push.service.api.model.kafka.Application, java.lang.String>:
Property: app.push.firebase.application-keys
Value: "{"applicationOne": "api=key-one","applicationTwo": "api=key-two"}"
Origin: class path resource [application-local.yml] - 47:25
Reason: org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [java.lang.String] to type [java.util.Map<com.example.services.push.service.api.model.kafka.Application, java.lang.String>]
My application.yml
app:
use-dev-apns: true
kafka.consumer.group: 'local'
push:
errorCallbackUrl: 'callback-url'
firebase:
applicationKeys: '{"applicationOne": "api=key-one","applicationTwo": "api=key-two"}'
defaultKey: 'api-key'
My property class
#ConstructorBinding
#ConfigurationProperties("app.push")
data class PushProperties(
val errorCallbackUrl: String,
val firebase: FirebaseProperties
)
data class FirebaseProperties(
val applicationKeys: Map<Application,String>,
val defaultKey: String
)
And custom converter
#ConfigurationPropertiesBinding
#Component
class StringToMapConverter: Converter<String, Map<Application, String>> {
override fun convert(source: String): Map<Application, String> {
try {
val map = BasicJsonParser().parseMap(source) as Map<String, String>
return map.mapKeys { Application.valueOf(it.key.uppercase()) }
} catch (e: JsonParseException) {
throw Exception("app.callback-mappings property is invalid. Must be a JSON object string")
}
}
}
What could be the problem?
Custom converter bind data from string to Map<Application, String>
I have solved the problem.
The problem was that Kotlin Map class does not match the java.util.Map class.
When I change Kotlin Map class to MutableMap class everything works correctly

Autowired not working in Scala Spring Boot project

Taking into account the following example where I'm trying to use the Sample configuration bean within SampleStarter to start the service with the bean properly filled. The .scala file has SampleStarter.scala as name, with Sample being defined within that exact same file.
#SpringBootApplication
#Service
object SampleStarter {
#(Autowired #setter)
var sample: Sample = _ // always null, #Autowired not working?
def getInstance() = this
def main(args: Array[String]): Unit = {
SpringApplication.run(classOf[Sample], args: _*)
sample.run()
}
}
#Configuration
#ConfigurationProperties("sample")
#EnableConfigurationProperties
class Sample {
#BeanProperty
var myProperty: String = _
def run(): Unit = { // bean config seems OK, when debugging with #PostConstruct the 'myProperty' value is filled properly
print(myProperty)
}
}
Whenever I hit sample.run() after SpringApplication.run(classOf[Sample], args: _*), sample property is always null. I reckon this has something to do with object in Scala since all their members are static AFAIK. I took this SO question How to use Spring Autowired (or manually wired) in Scala object? as inspiration but still can't make my code to work.
Is there something wrong the way I'm instantiating the classes or is it something related to Scala?
EDIT
Following #Rob's answer, this is what I did trying to replicate his solution.
#SpringBootApplication
#Service
object SampleStarter {
#(Autowired #setter)
var sample: Sample = _
def getInstance() = this
def main(args: Array[String]): Unit = {
SpringApplication.run(classOf[Sample], args: _*)
sample.run()
}
}
#Configuration // declares this class a source of beans
#ConfigurationProperties("sample") // picks up from various config locations
#EnableConfigurationProperties // not certain this is necessary
class AppConfiguration {
#BeanProperty
var myProperty: String = _
#Bean
// Error -> Parameter 0 of constructor in myproject.impl.Sample required a bean of type 'java.lang.String' that could not be found.
def sample: Sample = new Sample(myProperty)
}
class Sample(#BeanProperty var myProperty: String) {
def run(): Unit = {
print(myProperty)
}
}
#Configuration declares a source file that is capable of providing #Beans. This is not what you want. You want/need to have Sample as a POJO class (POSO?) and then have another class "AppConfiguration" (say) annotated with #Configuration that creates an #Bean of type Sample
My scala is very rusty so this may not compile at all but the structure should be roughly correct.
#Configuration // declares this class a source of beans
#ConfigurationProperties("sample") // picks up from various config locations
#EnableConfigurationProperties // not certain this is necessary
class AppConfiguration {
#Bean
def getSample(#BeanProperty myProperty: String): Sample = new Sample(myProperty)
}
class Sample {
var myProperty: String
def Sample(property : String) = {
this.myProperty = myProperty
}
def run(): Unit = {
print(myProperty)
}
}
Now you have a Sample class as an #Bean and it can be #Autowired in where ever necessary.
#SpringBootApplication
#Service
object SampleStarter {
// note its typically better to inject variables into a constructor
// rather than directly into fields as the behaviour is more predictable
#Autowired
var sample: Sample
def getInstance() = this // not required ??
def main(args: Array[String]): Unit = {
SpringApplication.run(classOf[Sample], args: _*)
sample.run()
}
}
FWIW, you can start the SpringBoot application independently and have the logic for #Service entirely separate. An #Service is another type of #Bean and is made available to the rest of the SpringBootApplication.

Spring Kotlin DSL: get all beans of certain type

Suppose I have an interface Yoyo and different realizations of this interface:
interface Yoyo {
fun haha() {
println("hello world")
}
}
#Component class Yoyo1 : Yoyo
#Component class Yoyo2 : Yoyo
#Component class Yoyo3 : Yoyo
#Component class YoyoN : Yoyo
Now I would like to instantiate all beans and do some logic after the context has been initialized:
#SpringBootApplication
class YoyoApp
fun main(args: Array<String>) {
SpringApplicationBuilder()
.sources(YoyoApp::class.java)
.initializers(beans {
bean {
CommandLineRunner {
val y1 = ref<Yoyo1>()
val y2 = ref<Yoyo2>()
val y3 = ref<Yoyo3>()
val yN = ref<YoyoN>()
arrayOf(y1, y2, y3, yN).forEach { it.haha() }
}
}
})
.run(*args)
}
Instead of manually getting ref to all beans (which is rather tedious), I would like to do this:
val list = ref<List<Yoyo>>()
list.forEach { it.haha() }
However I get an exception:
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'java.util.List<?>' available
I know I could do this instead, but I would like use the new Kotlin DSL instead:
#Component
class Hoho : CommandLineRunner {
#Autowired
lateinit var list: List<Yoyo>
override fun run(vararg args: String?) {
list.forEach { it.haha() }
}
}
Is it possible? Any ideas?
P.S. Here is the gist.
The context mentioned in the previous answer by #zsmb13 was left internal in favor of the provider<Any>() function (starting with Spring 5.1.1). So in the end I ended up with the following:
interface Yoyo {
fun haha() {
println("hello world from: ${this.javaClass.canonicalName}")
}
}
#Component class Yoyo1 : Yoyo
#Component class Yoyo2 : Yoyo
#Component class Yoyo3 : Yoyo
#Component class YoyoN : Yoyo
#SpringBootApplication
class YoyoApp
fun main(args: Array<String>) {
SpringApplicationBuilder()
.sources(YoyoApp::class.java)
.initializers(beans {
bean {
CommandLineRunner {
val list = provider<Yoyo>().toList()
list.forEach { it.haha() }
}
}
})
.run(*args)
}
The ref function used in the DSL can be found here in the source of the framework. There is no equivalent for getting all beans of a type, but you could add your own extension to the BeanDefinitionDsl class to do this:
inline fun <reified T : Any> BeanDefinitionDsl.refAll() : Map<String, T> {
return context.getBeansOfType(T::class.java)
}
Only problem is that the context required for this is internal in the currently released version of the framework. This commit from 8 days ago makes it publicly available "for advanced use-cases", but there hasn't been a new release of the framework since, so it's not available yet.
(The same commit also makes the class to extend directly the BeanDefinitionDsl class and not BeanDefinitionDsl.BeanDefinitionContext.)
Conclusion: you'll probably have to wait for the next release that includes the commit mentioned above, and then you'll be able to create this extension for yourself. I've also submitted a pull request in hopes that this could be included in the framework itself.

Repository doesn't have a find-one-method declared

I am trying to reproduce code sample from https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#core.web.basic (Basic web support)
I have some controller in Spring Web MVC application:
#Controller // This means that this class is a Controller
#RequestMapping(path = "/demo") // This means URL's start with /demo (after Application path)
#EnableSpringDataWebSupport
public class MainController {
#GetMapping(path = "/getResById/{id}")
#ResponseBody
public Tresource getResById(#PathVariable("id") Tresource tr, Model m) {
m.addAttribute(tr);
return tr;
}
}
The application main entry is the class with #SpringBootApplication annotation.
I also have repository class:
#Transactional(isolation = Isolation.READ_UNCOMMITTED)
public interface UserRepository extends Repository<Tresource, Long> {
// List<Tresource> findByBrief(String brief);
#Query("Select t.brief from Tresource t where t.resourceId=:resourceId")
String qqq(#Param("resourceId") Long resourceId);
Optional<Tresource> findDistinctByResourceIdOrBrief(Long resourceId, String brief);
#Query("Select i from Tresource t "
+ "inner join Tinstitution i on i.institutionId=t.instOwnerId "
+ "where t.resourceId=:resourceId")
Optional<Tinstitution> getResOwner(#Param("resourceId") Long resourceId);
}
This code gets exception
2018-03-21 14:33:15.977 WARN 11464 --- [nio-8080-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Failed to bind request element: org.springframework.web.method.annotation.MethodArgumentTypeMismatchException: Failed to convert value of type 'java.lang.String' to required type 'com.mycompany.eurofatcafns.db.Tresource'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [#org.springframework.web.bind.annotation.PathVariable com.mycompany.eurofatcafns.db.Tresource] for value '2010015161038'; nested exception is java.lang.IllegalStateException: Repository doesn't have a find-one-method declared!
when I am accessing /demo/getResById/2010015161038 .
What am I doing wrong with this code? How to fix this code?
Thank you very much!
Solution found!
In my code sample, I used
public interface UserRepository extends Repository<Tresource, Long> {
but getOne(ID) and findOne(ID) are in the CrudRepository class.
So, I fixed my code to this code and everything works well now:
public interface UserRepository extends CrudRepository<Tresource, Long> {

How to spring inject configuration value for Joda Period

How can I use the #Value annotation to configure a Joda-Time Period field in my spring bean?
E.g. Given the following component class:
#Component
public class MyService {
#Value("${myapp.period:P1D}")
private Period periodField;
...
}
I want to use standard ISO8601 format to define the period in a properties file.
I get this error:
Caused by: java.lang.IllegalStateException: Cannot convert value of type [java.lang.String] to required type [org.joda.time.Period]: no matching editors or conversion strategy found
at org.springframework.beans.TypeConverterDelegate.convertIfNecessary(TypeConverterDelegate.java:302)
at org.springframework.beans.TypeConverterDelegate.convertIfNecessary(TypeConverterDelegate.java:125)
at org.springframework.beans.TypeConverterSupport.doConvert(TypeConverterSupport.java:61)
... 35 more
A simple solution that does not require any java code is to use Spring Expression Language (SpEL).
(My example uses java.time.Duration and not Joda stuff but I think you get it anyway.)
#Value("#{T(java.time.Duration).parse('${notifications.maxJobAge}')}")
private Duration maxJobAge;
What you can do is register a Spring ConversionService bean and implement a proper converter.
#Bean
public ConversionServiceFactoryBean conversionService() {
ConversionServiceFactoryBean conversionServiceFactoryBean = new ConversionServiceFactoryBean();
Set<Converter<?, ?>> myConverters = new HashSet<>();
myConverters.add(new StringToPeriodConverter());
conversionServiceFactoryBean.setConverters(myConverters);
return conversionServiceFactoryBean;
}
public class StringToPeriodConverter implements Converter<String, Period> {
#Override
public Period convert(String source) {
return Period.parse(source);
}
}
Another, not elegant, option, is to use a String setter who invokes the parse method.
#Value("${myapp.period:P1D}")
public void setPeriodField(String periodField)
{
if (isBlank(periodField))
this.periodField= null;
this.periodField= Duration.parse(periodField);
}
For joda-time:2.10.13 and spring-boot:2.3.2.RELEASE next example (like in question) is worked:
#Value("${myapp.period:P1D}")
private Period periodField;
If you use java.time.Period, in addition, worked a simple period property format (org.springframework.boot.convert.PeriodStyle):
#Value("${myapp.period:1d}")
private Period periodField;

Resources