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