Read a Map with Spring #ConfigurationProperties in test - java

Following a advice from Spring Boot integration tests doesn't read properties files I created the following code, with the intention of reading a map from properties in my JUnit test.
(I am using yml format, and using #ConfigurationProperties instead of #Value)
#RunWith(SpringJUnit4ClassRunner.class)
#TestPropertySource(locations="classpath:application-test.yml")
#ContextConfiguration(classes = {PropertiesTest.ConfigurationClass.class, PropertiesTest.ClassToTest.class})
public class PropertiesTest {
#Configuration
#EnableConfigurationProperties
static class ConfigurationClass {
}
#ConfigurationProperties
static class ClassToTest {
private String test;
private Map<String, Object> myMap = new HashMap<>();
public String getTest() {
return test;
}
public void setTest(String test) {
this.test = test;
}
public Map<String, Object> getMyMap() {
return myMap;
}
}
#Autowired
private ClassToTest config;
#Test
public void testStringConfig() {
Assert.assertEquals(config.test, "works!");
}
#Test
public void testMapConfig() {
Assert.assertEquals(config.myMap.size(), 1);
}
}
My test configuration (in application-test.yml):
test: works!
myMap:
aKey: aVal
aKey2: aVal2
Strangely, the String "works!" is successfully read from the config file, but the map is not populated.
What am I missing?
Note: adding a map setter causes the following exception:
Caused by: org.springframework.validation.BindException: org.springframework.boot.bind.RelaxedDataBinder$RelaxedBeanPropertyBindingResult: 1 errors
Field error in object 'target' on field 'myMap': rejected value []; codes [typeMismatch.target.myMap,typeMismatch.myMap,typeMismatch.java.util.Map,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [target.myMap,myMap]; arguments []; default message [myMap]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'java.util.Map' for property 'myMap'; nested exception is java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String' to required type 'java.util.Map' for property 'myMap': no matching editors or conversion strategy found]
at org.springframework.boot.bind.PropertiesConfigurationFactory.checkForBindingErrors(PropertiesConfigurationFactory.java:359)
at org.springframework.boot.bind.PropertiesConfigurationFactory.doBindPropertiesToTarget(PropertiesConfigurationFactory.java:276)
at org.springframework.boot.bind.PropertiesConfigurationFactory.bindPropertiesToTarget(PropertiesConfigurationFactory.java:240)
at org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor.postProcessBeforeInitialization(ConfigurationPropertiesBindingPostProcessor.java:330)
... 42 more

After some wonderful time with a debugger,
I believe that this is a bug / missing feature in TestPropertySourceUtils.addPropertiesFilesToEnvironment():
try {
for (String location : locations) {
String resolvedLocation = environment.resolveRequiredPlaceholders(location);
Resource resource = resourceLoader.getResource(resolvedLocation);
environment.getPropertySources().addFirst(new ResourcePropertySource(resource));
}
}
ResourcePropertySource can only deal with .properties files and not .yml.
In regular app, YamlPropertySourceLoader registered and can deal with .yml.
As a note:
TestPropertySourceUtils.addPropertiesFilesToEnvironment() is called by:
org.springframework.test.context.support.DelegatingSmartContextLoader.prepareContext()
(inherited from AbstractContextLoader)
DelegatingSmartContextLoader is the default context loader you receive if no loader is specified in #ContextConfiguration.
(in fact #ContextConfiguration specifies an interface, but AbstractTestContextBootstrapper.resolveContextLoader() changes it to a concrete class)
To resolve the problem, I changed my configuration to application-test.properties
and used that file in my test.
test=works!
myMap.aKey: aVal
Another comment: the setter on the map is NOT needed:
https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html#boot-features-external-config-loading-yaml
To bind to properties like that using the Spring DataBinder utilities
(which is what #ConfigurationProperties does) you need to have a
property in the target bean of type java.util.List (or Set) and you
either need to provide a setter, or initialize it with a mutable
value, e.g. this will bind to the properties above

Related

How to retrieve custom annotation fields?

I would like to implement a custom annotation that could be applied to a class (once inside an app), to enable a feature (Access to remote resources). If this annotation is placed on any config class, it will set the access for the whole app. So far it isn't that hard (see example below), but I want to include some definition fields in the #interface that will be used in the access establishing process.
As an example, Spring has something very similar: #EnableJpaRepositories. Access is enabled to the DB, with parameters in the annotation containing definitions. For example: #EnableJpaRepositories(bootstrapMode = BootstrapMode.DEFERRED)
So far, I have:
To create only the access I'm using something like that:
#Target(ElementType.TYPE)
#Retention(RetentionPolicy.RUNTIME)
#Import(AccessHandlerConfiguration.class)
public #interface EnableAccessHandlerAutoconfigure {
String name() default "";
}
Using it:
#EnableAccessHandlerAutoconfigure{name="yoni"}
#Configuration
public class config {}
AccessHandlerConfiguration is a configuration class that contains beans that establish the connection.
The problem I'm having is that I don't know how to retrieve the field name's value. What should I do?
Retrieving the value may be accomplished as follows:
this.getClass().getAnnotation(EnableAccessHandlerAutoconfigure.class).name()
To expand on my comment with an actual example configuration class that uses this:
#EnableAccessHandlerAutoconfigure(name="yoni")
#Configuration
public class SomeConfiguration {
#Bean
SomeBean makeSomeBean() {
return new SomeBean(this.getClass().getAnnotation(EnableAccessHandlerAutoconfigure.class).name());
}
}
This is how you get the value of name, as to what you are going to do next, that depends on you.
After a long research, I found a way: There is a method in Spring's ApplicationContext that retrieves bean names according to their annotations getBeanNamesForAnnotation, then get the annotation itself findAnnotationOnBean, and then simply use the field getter.
#Configuration
public class AccessHandlerConfiguration {
private final ApplicationContext applicationContext;
public AccessHandlerConfiguration(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
String[] beansWithTheAnnotation = applicationContext.getBeanNamesForAnnotation(EnableRabbitAutoconfigure.class);
for (String beanName : beansWithTheAnnotation) {
EnableRabbitAutoconfigure annotationOnBean = applicationContext.findAnnotationOnBean(beanName, EnableRabbitAutoconfigure.class);
System.out.println("**********" + beanName + "*********************" + annotationOnBean.name() + "*******************");
}
}
}
Results:
**********config*********************yoni*******************

Unable to resolve variable from properties file when tried to access as function parameter using #Value annotation

This may be silly question to ask but i'm unable to find any satisfactory solution to my problem. In java we don't have the concept of default variables so i am trying to give default value from properties file to my function parameters/arguments using #Value annotation, but i'm always getting null and i'm unable to figure why is this happening. Please help me to solve the issue or provide me some appropriate link/reference which may solve my issue.
MainApplication.java
#SpringBootApplication
public class Application
{
public static void main(String[] args)
{
ApplicationContext context = SpringApplication.run(NetappApplication.class, args);
Sample sample = context.getBean(Sample.class);
System.out.println(sample.check(null));
}
}
Sample.java
public interface Sample
{
public String check(String message);
}
SampleImpl.java
#Service
#PropertySource("classpath:app.properties")
public class SampleImpl implements Sample
{
#Value("${test}")
String message1;
#Override
public String check(#Value("${test}") String message)
{
return message;
}
}
app.properties
test=anand
But you are passing null to your method...
Perhaps what you want to do is to assign default value to test in case it's not defined in property file:
#Value("${test:default}");
Then, when properties are autowired by Spring if placeholder resolver doesn't get the value from props file, it will use what is after :.
The best use case for this (that I can think of) is when you create Spring configuration.
Let's say you have a configuration class: for DB access. Simply put:
#Configuration
public class DbConfig {
#Value("${url:localhost}")
String dbUrl;
// rest for driver, user, pass etc
public DataSource createDatasource() {
// here you use some DataSourceBuilder to configure connection
}
}
Now, when Spring application starts up, properties' values are resolved, and as I wrote above you can switch between value from property and a default value. But it is done once, when app starts and Spring creates your beans.
If you want to check incoming argument on runtime, simple null check will be enough.
#Value("${test}")
String message1;
#Override
public String check(String message) {
if (message == null) {
return message1;
}
}

Filterregisteration Bean and injection of a collection #Value

We have a configuration class:
public class FilterConfigBase {
#Bean
public FilterRegisterationBean
corsFilter(#value("${client.ip.headers:}") List<String>
clientIpHeaders)
{
....
...
...
}
}
Application.properties:
client.ip.headers=A,B,C
We have a DefaultConversion service bean (which has StringToCollectionConverter)
In FilterConfigBase (Which contains only FIlterRegisterationBean), #Value annotation is not calling the StringToCollectionConverter class instead the conversion result is:
clientIpHeaders[0]="A,B,C"
Whereas in other configuration classes, #Value annotation is able to resolve the property, It calls the StringToCollectionConverter and the values are properly populated.
clientIpHeaders.get(0)="A"
clientIpHeaders.get(1)="B"
clientIpHeaders.get(2)="C"
For more details:
https://github.com/spring-projects/spring-boot/issues/11585
You can try to add a bit of EL there to split into list:
#Bean
public FilterRegisterationBean
corsFilter(#value("#{'${client.ip.headers:}'.split(',')}") List<String>
clientIpHeaders)

#ConditionalOnProperty for lists or arrays?

I'm using Spring Boot 1.4.3 #AutoConfiguration where I create beans automatically based on properties user specifies. User can specify an array of services, where name and version are required fields:
service[0].name=myServiceA
service[0].version=1.0
service[1].name=myServiceB
service[1].version=1.2
...
If the user forgets to specify a required field on even just one service, I want to back-off and not create any beans. Can I accomplish this with #ConditionalOnProperty? I want something like:
#Configuration
#ConditionalOnProperty({"service[i].name", "service[i].version"})
class AutoConfigureServices {
....
}
This is the custom Condition I created. It needs some polishing to be more generic (ie not hardcoding strings), but worked great for me.
To use, I annotated my Configuration class with #Conditional(RequiredRepeatablePropertiesCondition.class)
public class RequiredRepeatablePropertiesCondition extends SpringBootCondition {
private static final Logger LOGGER = LoggerFactory.getLogger(RequiredRepeatablePropertiesCondition.class.getName());
public static final String[] REQUIRED_KEYS = {
"my.services[i].version",
"my.services[i].name"
};
#Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
List<String> missingProperties = new ArrayList<>();
RelaxedPropertyResolver resolver = new RelaxedPropertyResolver(context.getEnvironment());
Map<String, Object> services = resolver.getSubProperties("my.services");
if (services.size() == 0) {
missingProperties.addAll(Arrays.asList(REQUIRED_KEYS));
return getConditionOutcome(missingProperties);
}
//gather indexes to check: [0], [1], [3], etc
Pattern p = Pattern.compile("\\[(\\d+)\\]");
Set<String> uniqueIndexes = new HashSet<String>();
for (String key : services.keySet()) {
Matcher m = p.matcher(key);
if (m.find()) {
uniqueIndexes.add(m.group(1));
}
}
//loop each index and check required props
uniqueIndexes.forEach(index -> {
for (String genericKey : REQUIRED_KEYS) {
String multiServiceKey = genericKey.replace("[i]", "[" + index + "]");
if (!resolver.containsProperty(multiServiceKey)) {
missingProperties.add(multiServiceKey);
}
}
});
return getConditionOutcome(missingProperties);
}
private ConditionOutcome getConditionOutcome(List<String> missingProperties) {
if (missingProperties.isEmpty()) {
return ConditionOutcome.match(ConditionMessage.forCondition(RequiredRepeatablePropertiesCondition.class.getCanonicalName())
.found("property", "properties")
.items(Arrays.asList(REQUIRED_KEYS)));
}
return ConditionOutcome.noMatch(
ConditionMessage.forCondition(RequiredRepeatablePropertiesCondition.class.getCanonicalName())
.didNotFind("property", "properties")
.items(missingProperties)
);
}
}
Old question, but I hope my answer will help for Spring2.x:
Thanks to #Brian, I checked migration guide, where I was inspired by example code. This code works for me:
final List<String> services = Binder.get(context.getEnvironment()).bind("my.services", List.class).orElse(null);
I did try to get List of POJO (as AutoConfigureService) but my class differs from AutoConfigureServices. For that purpose, I used:
final Services services = Binder.get(context.getEnvironment()).bind("my.services", Services.class).orElse(null);
Well, keep playing :-D
Here's my take on this issue with the use of custom conditions in Spring autoconfiguration. Somewhat similar to what #Strumbels proposed but more reusable.
#Conditional annotations are executed very early in during the application startup. Properties sources are already loaded but ConfgurationProperties beans are not yet created. However we can work around that issue by binding properties to Java POJO ourselves.
First I introduce a functional interface which will enable us to define any custom logic checking if properties are in fact present or not. In your case this method will take care of checking if the property List is empty/null and if all items within are valid.
public interface OptionalProperties {
boolean isPresent();
}
Now let's create an annotation which will be metannotated with Spring #Conditional and allow us to define custom parameters. prefix represents the property namespace and targetClass represents the configuration properties model class to which properties should be mapped.
#Target({ElementType.TYPE, ElementType.METHOD})
#Retention(RetentionPolicy.RUNTIME)
#Documented
#Conditional(OnConfigurationPropertiesCondition.class)
public #interface ConditionalOnConfigurationProperties {
String prefix();
Class<? extends OptionalProperties> targetClass();
}
And now the main part. The custom condition implementation.
public class OnConfigurationPropertiesCondition extends SpringBootCondition {
#Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
MergedAnnotation<ConditionalOnConfigurationProperties> mergedAnnotation = metadata.getAnnotations().get(ConditionalOnConfigurationProperties.class);
String prefix = mergedAnnotation.getString("prefix");
Class<?> targetClass = mergedAnnotation.getClass("targetClass");
// type precondition
if (!OptionalProperties.class.isAssignableFrom(targetClass)) {
return ConditionOutcome.noMatch("Target type does not implement the OptionalProperties interface.");
}
// the crux of this solution, binding properties to Java POJO
Object bean = Binder.get(context.getEnvironment()).bind(prefix, targetClass).orElse(null);
// if properties are not present at all return no match
if (bean == null) {
return ConditionOutcome.noMatch("Binding properties to target type resulted in null value.");
}
OptionalProperties props = (OptionalProperties) bean;
// execute method from OptionalProperties interface
// to check if condition should be matched or not
// can include any custom logic using property values in a type safe manner
if (props.isPresent()) {
return ConditionOutcome.match();
} else {
return ConditionOutcome.noMatch("Properties are not present.");
}
}
}
Now you should create your own configuration properties class implementing OptionalProperties interface.
#ConfigurationProperties("your.property.prefix")
#ConstructorBinding
public class YourConfigurationProperties implements OptionalProperties {
// Service is your POJO representing the name and version subproperties
private final List<Service> services;
#Override
public boolean isPresent() {
return services != null && services.stream().all(Service::isValid);
}
}
And then in Spring #Configuration class.
#Configuration
#ConditionalOnConfigurationProperties(prefix = "", targetClass = YourConfigurationProperties.class)
class AutoConfigureServices {
....
}
There are two downsides to this solution:
Property prefix must be specified in two locations: on #ConfigurationProperties annotation and on #ConditionalOnConfigurationProperties annotation. This can partially be alleviated by defining a public static final String PREFIX = "namespace" in your configuration properties POJO.
Property binding process is executed separately for each use of our custom conditional annotation and then once again to create the configuration properties bean itself. It happens only during app startup so it shouldn't be an issue but it still is an inefficiency.
You can leverage the org.springframework.boot.autoconfigure.condition.OnPropertyListCondition class. For example, given you want to check for the service property having at least one value:
class MyListCondition extends OnPropertyListCondition {
MyListCondition() {
super("service", () -> ConditionMessage.forCondition("service"));
}
}
#Configuration
#Condition(MyListCondition.class)
class AutoConfigureServices {
}
See the org.springframework.boot.autoconfigure.webservices.OnWsdlLocationsCondition used on org.springframework.boot.autoconfigure.webservices.WebServicesAutoConfiguration#wsdlDefinitionBeanFactoryPostProcessor for an example within Spring itself.

Given that names of parameters are lost during compilation, how does Spring autowire by parameter name?

Consider following configuration with two beans of the same type created:
#Configuration
#ComponentScan(basePackageClasses = TwoStrings.class)
public class Config {
#Bean
public String one() {
return "one";
}
#Bean
public String two() {
return "two";
}
}
Another bean that depends on the two beans above is created by component scan:
#Component
public class TwoStrings {
public final String a;
public final String b;
#Autowired
public TwoStrings(String one, String two) {
this.a = one;
this.b = two;
}
}
The names of local variables/parameters are lost during compilation and are not available at runtime:
However, Spring somehow autowires two String beans correctly. The example test below
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration(classes = Config.class )
public class Example {
#Autowired
private TwoStrings twoStrings;
#Test
public void test() {
System.out.println(twoStrings.a);
System.out.println(twoStrings.b);
}
}
prints
one
two
Given the name of constructor parameters are lost, I would expect Spring to throw the exception saying that there is more than 1 bean of type String, however Spring somehow autowires beans using parameters names.
The question is how Spring knows the names of constructor parameters?
Spring is using debug information in this case - it will fail if you compile your code without debug information, i.e. with -g:none flag of javac.
Extract from documentation:
Keep in mind that to make this work out of the box your code must be
compiled with the debug flag enabled so that Spring can look up the
parameter name from the constructor. If you can’t compile your code
with debug flag (or don’t want to) you can use #ConstructorProperties
JDK annotation to explicitly name your constructor arguments.
Java 8 has introduced a way for reading this information using Reflection API but the classes has to be compiled with a -parameters flag of javac

Categories