Introduction
Spring has the support for injecting a property via @Value annotation as :
public class Foo { @Value("${fooVal}") private Integer integer; ... }or Constructor Injection
public class Foo { private final Integer integer; public Foo(@Value("${fooVal}") Integer integer) { this.integer = integer; } ... }or Setter Injection
public 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