Introduction
Spring has the support for injecting a property via @Value annotation as :
public class Foo {
@Value("${fooVal}")
private Integer integer;
...
}
or Constructor Injectionpublic class Foo {
private final Integer integer;
public Foo(@Value("${fooVal}") Integer integer) {
this.integer = integer;
}
...
}
or Setter Injectionpublic class Foo {
private Integer integer;
@Inject
public setInteger(@Value("${fooVal}") Integer integer) {
this.integer = integer;
}
...
}
Or if using JavaConfig:
@Configuration
public class FooConfig {
@Bean
public Foo foo(@Value("${fooVal}") Integer integer) {
return new Foo(integer);
}
}
If fooVal never changes during the execution of the program, great, you inject it and then its set. What if it were to change, due to it being a configurable value? What would be nice to do is the following:
public class Foo {
private final Provider<Integer> integerProvider;
public Foo(Provider<Integer> integerProvider) {
this.integerProvider = integerProvider;
}
public void someOperation() {
Integer integer = integerProvider.get(); // Current value of fooVal
..
}
}
@Configuration
public class FooConfig {
@Bean
public Foo foo(@Value("${fooVal}") Provider<Integer> integerProvider) {
return new Foo(integer);
}
}
Looks pretty reasonable, however, when you attempt the same with Spring Java Config, you end up with:
org.springframework.beans.TypeMismatchException: Failed to convert value of type 'java.lang.String' to required type 'java.lang.Integer'; nested exception is java.lang.IllegalArgumentException: MethodParameter argument must have its nestingLevel set to 1 at org.springframework.beans.TypeConverterSupport.doConvert(TypeConverterSupport.java:77) at org.springframework.beans.TypeConverterSupport.convertIfNecessary(TypeConverterSupport.java:47) at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:875)This should just work. Simplest way I could get it to work is to fool spring that the nesting level of the Provider<Integer> is actual one level lesser. The way I did it was to create my own org.spring.framework.beans.TypeConverter that delegated to the default TypeConverter used by Spring, i.e., the SimpleTypeConverter:
public class CustomTypeConverter implements TypeConverter {
private final SimpleTypeConverter simpleTypeConverter;
public CustomTypeConverter() {
simpleTypeConverter = new SimpleTypeConverter(); // This is the default used by Spring
}
public <T> T convertIfNecessary(Object newValue, Class<T> requiredType,
MethodParameter methodParam) throws IllegalArgumentException {
Type type = methodParam.getGenericParameterType();
MethodParameter parameterTarget = null;
if (type instanceof ParameterizedType) {
ParameterizedType paramType = ParameterizedType.class.cast(type);
Type rawType = paramType.getRawType();
if (rawType.equals(Provider.class)
&& methodParam.hasParameterAnnotation(Value.class)) {
// If the Raw type is javax.inject.Provider, reduce the nesting level
parameterTarget = new MethodParameter(methodParam);
// Send a new Method Parameter down stream, don't want to fiddle with original
parameterTarget.decreaseNestingLevel();
}
}
return simpleTypeConverter.convertIfNecessary(newValue, requiredType, parameterTarget);
}
...// Delegate other methods to simpleTypeConverter
}
With the above you could do the following:
public class SpringTest {
@Test
public void test() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.getBeanFactory().setTypeConverter(new CustomTypeConverter());
context.register(SimpleConfig.class);
context.refresh();
context.getBean(SimpleBean.class).print();
}
public static class SimpleConfig {
@Bean(name = "props") // Change this to a something that detects file system changes and pulls it in
public PropertyPlaceholderConfigurer propertyPlaceHolderConfigurer() {
PropertyPlaceholderConfigurer configurer = new PropertyPlaceholderConfigurer();
Resource location = new ClassPathResource("appProperties.properties");
configurer.setLocation(location);
return configurer;
}
@Bean
public SimpleBean simpleBean(@Value("${fooVal}") Provider<Integer> propValue,
@Value("${barVal}") Provider<Boolean> booleanVal) {
return new SimpleBean(propValue, booleanVal);
}
}
public static class SimpleBean {
private final Provider<Integer> propVal;
private final Provider<Boolean> booleanVal;
public SimpleBean(Provider<Integer>, Provider<Boolean> booleanVal) {
this.propVal = propValue;
this.booleanVal = booleanVal;
}
public void print() {
System.out.println(propVal.get() + "," + booleanVal.get());
}
}
}
Now with the above if your properties changes, as you are injecting a javax.inject.Provider to the SimpleBean, at runtime, it will obtain the current value to use.
All this is great, I understand that I am hacking Spring to get what I want, is this the best way to handle something like dynamically changeable properties? Thoughts and suggestion welcomed.
Update :
I filed an enhancement request with Spring maintainers and they were fast to respond with this actually being a bug and are back porting fixes to previous versions as well as ensuring current and future versions have this feature available. https://jira.spring.io/browse/SPR-12297
No comments:
Post a Comment