Spring Boot - inject static map from application.yml - java

I refered Spring Boot - inject map from application.yml for injecting map from application.yml file
My application.yml snippet is below
easy.app.pairMap:
test1: 'value1'
test2: 'value2'
Properties file is like below
#Component
#Configuration
#ConfigurationProperties("easy.app")
#EnableConfigurationProperties
public class TestProperties {
private Map<String, String> pairMap= new HashMap<String, String>();
public void setPairMap(Map<String, String> pairMap) {
this.pairMap= pairMap;
}
}
The above given code works .Map is not read from application.yml file when the 'pairMap' is set as static as below.
#Component
#Configuration
#ConfigurationProperties("easy.app")
#EnableConfigurationProperties
public class TestProperties {
private static Map<String, String> pairMap= new HashMap<String, String>();
public static void setPairMap(Map<String, String> pairMap) {
TestProperties .pairMap= pairMap;
}
}
PS : The issue is only when injecting map , but not on injecting string. Why is this behaviour?
ie the following injection of string in the following configuration works , but not the map injection
easy.app.key1: 'abc'
easy.app.pairMap:
test1: 'value1'
test2: 'value2'
Properties file like below
#Component
#Configuration
#ConfigurationProperties("easy.app")
#EnableConfigurationProperties
public class TestProperties {
private Map<String, String> pairMap= new HashMap<String, String>();
private static String key1;
public static void setPairMap(Map<String, String> pairMap) {
this.pairMap= pairMap;
}
public static void setKey1(String key1) {
TestProperties.key1= key1;
}
public String getKey1(){
return key1;
}

Fix with this:
easy:
app:
pairMap:
test1: value1
test2: value2
#CompileStatic
#Component
#EnableConfigurationProperties
class ConfigHolder {
#Value(value = '${easy.app.pairMap.test1}')
String test1Valse;
#Value(value = '${easy.app.pairMap.test2}')
String test2Valse;
}
#CompileStatic
#Configuration
#EnableConfigurationProperties
public class TestProperties {
#Autowired
ConfigHolder configHolder;
private Map<String, String> pairMap= new HashMap<String, String>();
public void setPairMap(Map<String, String> pairMap) {
if(pairMap != null && !pairMap.isNotEmpty()) {
this.pairMap = pairMap;
} else {
this.pairMap.put("test 1", ${configHolder.test1Valse});
this.pairMap.put("test 2", ${configHolder.test2Valse});
}
}
}

Related

How to test ConfigurationProperties with JUnit?

This is my first time working with Externalized Configuration and yaml.
I created a yaml where I use the class name as KEY, and the field name as VALUE
YAML:
project:
test:
service:
computator:
# exclude field from beeing randomly valorized
population:
exclude:
InputClass: 'myDate'
AnotherClass: 'myName'
ExcludePopulationProperties:
#Data
#Component
#ConfigurationProperties(prefix = "project")
public class ExcludePopulationProperties {
private Test test;
#Data
public static class Test {
private Service service;
}
#Data
public static class Service {
private Computator computator;
}
#Data
public static class Computator {
private Population population;
}
#Data
public static class Population {
private Map<String, String> exclude;
}
}
Test with JUnit 5:
#ContextConfiguration(classes = { ExcludePopulationProperties.class })
#ExtendWith(SpringExtension.class)
class YamlTest {
#Autowired
private ExcludePopulationProperties excludePopulationProperties;
#Test
void testExternalConfiguration() {
Map<String, String> map = excludePopulationProperties.getTest().getService().getComputator().getPopulation().getExclude();
assertNotNull(map);
}
The problem is that I have a NullPointerException because test is null
So I'm not sure what is wrong here, I was expecting that the map was correctly populated.
I also tried to add
#TestPropertySource(properties = { "spring.config.location=classpath:application-_test.yaml" })
on the YamlTest
Your ExcludePopulationProperties class should not be annotated with #Component. Instead, you should have the annotation #EnableConfigurationProperties(ExcludePopulationProperties.class) on a configuration class in your project (the main application class will work).
Change your properties class to look like this (removing #Component):
#Data
#ConfigurationProperties(prefix = "project")
public class ExcludePopulationProperties {
...
}
Change your application class to enable the configuration properties:
#SpringBootApplication
#EnableConfigurationProperties(ExcludePopulationProperties.class)
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
And the YamlTest class to look like this (using #SpringBootTest):
#SpringBootTest
#ContextConfiguration(classes = { DemoApplication.class })
#TestPropertySource(properties = { "spring.config.location=classpath:application-test.yaml" })
class YamlTest {
#Autowired
private ExcludePopulationProperties excludePopulationProperties;
#Test
void testExternalConfiguration() {
Map<String, String> map = excludePopulationProperties.getTest().getService().getComputator().getPopulation().getExclude();
assertNotNull(map);
assertEquals(map.get("InputClass"), "myDate");
assertEquals(map.get("AnotherClass"), "myName");
}
}
With these little changes, now I'm able to test the properties from YAML file.
I improved the yaml a little bit:
# test placeholders
project:
test:
service:
computator:
# exclude field from beeing randomly valorized
population:
exclude:
InputClass:
- 'myDate'
AnotherClass:
- 'myName'
so now the ExcludePopulationProperties have a Map<String, List<String>> instead of Map<String, String>, in this way I will be able to exclude more than one field from the same class:
#Data
#Configuration
#ConfigurationProperties(prefix = "project")
#PropertySource(value = "classpath:application-_test.yaml", factory = YamlPropertySourceFactory.class)
public class ExcludePopulationProperties {
private Test test;
#Data
public static class Test {
private Service service;
}
#Data
public static class Service {
private Computator computator;
}
#Data
public static class Computator {
private Population population;
}
#Data
public static class Population {
private Map<String, List<String>> exclude;
}
}
YamlPropertySourceFactory is a class implemented by Baeldung in this guide:
#PropertySource with YAML Files in Spring Boot
public class YamlPropertySourceFactory implements PropertySourceFactory {
#Override
public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
factory.setResources(resource.getResource());
Properties properties = factory.getObject();
return new PropertiesPropertySource(resource.getResource().getFilename(), properties);
}
}
Test Class :
#EnableConfigurationProperties
#ContextConfiguration(classes = { ExcludePopulationProperties.class })
#TestPropertySource(properties = { "spring.config.location=classpath:application-_test.yaml" })
#ExtendWith(SpringExtension.class)
class YamlTest {
#Autowired
private ExcludePopulationProperties excludePopulationProperties;
#Test
void testExternalConfiguration() {
Map<String, List<String>> map = excludePopulationProperties.getTest().getService().getComputator().getPopulation().getExclude();
assertNotNull(map);
}
}
Please note that for Mockito you need to use both, SpringExtension and MockitoExtension:
#EnableConfigurationProperties
#ContextConfiguration(classes = { ExcludePopulationProperties.class })
#Extensions({
#ExtendWith(SpringExtension.class),
#ExtendWith(MockitoExtension.class)
})
class YamlTest {
}
UPDATE
I find a better solution in order to avoid writing the annotations on all test classes.
add the jackson jackson-dataformat-yaml dependency
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
<version>${jackson-dataformat-yaml.version}</version>
</dependency>
The configuration properties class will be:
#Data
public class ExcludePopulationProperties {
private Project project;
#Data
public static class Project {
private Test test;
}
#Data
public static class Test {
private Service service;
}
#Data
public static class Service {
private Computator computator;
}
#Data
public static class Computator {
private Population population;
}
#Data
public static class Population {
private Map<String, List<String>> exclude;
}
public static ExcludePopulationProperties build() throws IOException {
InputStream inputStream = new FileInputStream(new File("./src/test/resources/" + "application-_test.yaml"));
YAMLMapper mapper = new YAMLMapper();
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY);
return mapper.readValue(inputStream, ExcludePopulationProperties.class);
}
}
then, wherever you need, simply call the static build method, the test class will be more simple :
#ExtendWith(SpringExtension.class)
class YamlTest {
#Test
void testExternalConfiguration() throws IOException {
Map<String, List<String>> map = ExcludePopulationProperties.build().getProject().getTest().getService().getComputator().getPopulation().getExclude();
assertNotNull(map);
}
}

Spring Boot doesn't load a Map from environment variables when in camel-case

I set environment variables
MY_APP_MY_MAP_A1=a
MY_APP_MY_MAP_A2=b
MY_APP_JUSTMAP_A1=a
MY_APP_JUSTMAP_A2=b
to configure my Spring Boot (2.1.7.RELEASE) application via #ConfigurationProperties:
#SpringBootApplication
#EnableConfigurationProperties(MyApp.MyProperties.class)
public class MyApp {
public static void main(String[] args) {
SpringApplication.run(MyApp.class, args);
}
#Bean
public CommandLineRunner cmd(MyProperties props) {
return args -> {
System.out.println("myMap: " + props.getMyMap());
System.out.println("justmap: " + props.getJustmap());
};
}
#ConfigurationProperties(prefix = "my.app")
#Getter
#Setter
static class MyProperties {
private Map<String, String> myMap;
private Map<String, String> justmap;
}
}
Setting a Map<String,String> doesn't work when the variable name contains an upper letter (is in camelcase), otherwise everything works fine:
myMap: null
justmap: {a1=a, a2=b}
Is there a way how to do it?
If you have following env. variables passed
MY_APP_MYMAP_A1=a
MY_APP_MYMAP_A2=b
MY_APP_JUSTMAP_A1=a
MY_APP_JUSTMAP_A2=b
The below code prints what you are expecting
#SpringBootApplication
#EnableConfigurationProperties(TestSpringBootApplication.MyProperties.class)
public class TestSpringBootApplication {
public static void main(String[] args) {
SpringApplication.run(TestSpringBootApplication.class, args);
}
#Bean
public CommandLineRunner cmd(MyProperties props) {
return args -> {
System.out.println("myMap: " + props.getMyMap());
System.out.println("justmap: " + props.getJustmap());
};
}
#ConfigurationProperties(prefix = "my.app")
static class MyProperties {
private Map<String, String> myMap;
private Map<String, String> justmap;
public Map<String, String> getMyMap() {
return myMap;
}
public void setMyMap(Map<String, String> myMap) {
this.myMap = myMap;
}
public Map<String, String> getJustmap() {
return justmap;
}
public void setJustmap(Map<String, String> justmap) {
this.justmap = justmap;
}
}
}
The output is below
2019-09-04 16:00:07.336 INFO 21204 --- [ main] c.e.demo.TestSpringBootApplication : Started TestSpringBootApplication in 1.012 seconds (JVM running for 2.219)
myMap: {a1=a, a2=b}
justmap: {a1=a, a2=b}
For more details on the rules around this relaxed binding check the documentation here
Elaborating on the Shailendra's answer here's a relevant quote from docs:
To convert a property name in the canonical-form to an environment variable name you can follow these rules:
Replace dots (.) with underscores (_).
Remove any dashes (-).
Convert to uppercase.

Spring Boot read properties without prefix to a map

I need to read all properties in application.properties file in a map
In the code below the property test has the respective value but the map is empty. How can I fill "map" with the values in the application.properties file without adding a prefix to the properties.
This is my application.properties file
AAPL=25
GDDY=65
test=22
I'm using #ConfigurationProperties like this
#Configuration
#ConfigurationProperties("")
#PropertySource("classpath:application.properties")
public class InitialConfiguration {
private HashMap<String, BigInteger> map = new HashMap<>();
private String test;
public HashMap<String, BigInteger> getMap() {
return map;
}
public void setMap(HashMap<String, BigInteger> map) {
this.map = map;
}
public String getTest() {
return test;
}
public void setTest(String test) {
this.test = test;
}
}
This can be achieved using the PropertiesLoaderUtils and #PostConstruct
Please check the sample below:
#Configuration
public class HelloConfiguration {
private Map<String, String> valueMap = new HashMap<>();
#PostConstruct
public void doInit() throws IOException {
Properties properties = PropertiesLoaderUtils.loadAllProperties("application.properties");
properties.keySet().forEach(key -> {
valueMap.put((String) key, properties.getProperty((String) key));
});
System.err.println("valueMap -> "+valueMap);
}
public Map<String, String> getValueMap() {
return valueMap;
}
public void setValueMap(Map<String, String> valueMap) {
this.valueMap = valueMap;
}
}
You can't do it with #ConfigurationProperties as far as I'm aware, those require a prefix to be able to load those properties within the bean.
However, if your goal is to obtain "value Y" for "property X" programmatically, you can always inject Environment and use the getProperty() method to find certain property, for example:
#Configuration
public class InitialConfiguration {
#Autowired
private Environment environment;
#PostConstruct
public void test() {
Integer aapl = environment.getProperty("AAPL", Integer.class); // 25
Integer gddy = environment.getProperty("GDDY", Integer.class); // 65
Integer test = environment.getProperty("test", Integer.class); // 22
}
}
In spring boot, if you need to get a single value from the application.proprties, you just need to use the #Value annotation with the given name
So to get AAPL value just add a class level property like this
#Value("${AAPL}")
private String aapl;
And if you need to load a full properties file as a map, I'm using the ResourceLoader to load the full file as a stream and then parse it as follows
#Autowired
public loadResources(ResourceLoader resourceLoader) throws Exception {
Resource resource = resourceLoader.getResource("classpath:myProperties.properties"));
BufferedReader br = new BufferedReader(new InputStreamReader(resource.getInputStream()));
String line;
int pos = 0;
Map<String, String> map = new HashMap<>();
while ((line = br.readLine()) != null) {
pos = line.indexOf("=");
map.put(line.substring(0, pos), line.substring( pos + 1));
}
}
Indeed you can use #ConfigurationProperties without prefix to get entire properties known to Spring application i.e. application, system and environment properties etc.
Following example creates a fully populated map as a Spring bean. Then wire / inject this bean wherever you need it.
#Configuration
class YetAnotherConfiguration {
#ConfigurationProperties /* or #ConfigurationProperties("") */
#Bean
Map<String, String> allProperties() {
return new LinkedHashMap<>();
}
}
#Autowire
void test(Map<String, String> allProperties) {
System.out.println(allProperties.get("AAPL")); // 25
...
}
#PropertySource("classpath:config.properties")
public class GlobalConfig {
public static String AAPL;
#Value("${AAPL}")
private void setDatabaseUrl(String value) {
AAPL = value;
}
}
You have to use #Value to get value from application.properties file

How to inject prefixed properties into java.util.Properties?

Spring boot provides an elegant way to inject the properties prefixed with a particular key into Configuration class using #ConfigurationProperties(prefix = "foo"). That is shown here and here. Question is, how to inject prefixed properties into java.util.Properties instance as shown below?
#Configuration
#EnableConfigurationProperties
public class FactoryBeanAppConfig {
#Bean
#ConfigurationProperties(prefix = "kafka")
public Producer<String, String> producer(Properties properties) throws Exception {
Producer<String, String> producer = new KafkaProducer<String, String>(properties);
return producer;
}
}
That does not work, since this property injection is based on getters and setters on the object that should hold the #ConfigurationProperties
Define a class holding the properties you want like this:
#ConfigurationProperties(prefix = "kafka.producer")
public class MyKafkaProducerProperties {
private int foo;
private string bar;
// Getters and Setter for foo and bar
}
Then use it in your configuration like this
#Configuration
#EnableConfigurationProperties(MyKafkaProducerProperties.class)
public class FactoryBeanAppConfig {
#Bean
public Producer<String, String> producer(MyKafkaProducerProperties kafkaProperties) throws Exception {
Properties properties = new Properties();
properties.setProperty("Foo", kafkaProperties.getFoo());
properties.setProperty("Bar", kafkaProperties.getBar());
Producer<String, String> producer = new KafkaProducer<String, String>(properties);
return producer;
}
}
UPDATE
Since you commented that you don't want to have each property represented as java code you could use a HashMap as the one and only property in your #ConfigurationProperties
#ConfigurationProperties(prefix = "kafka")
public class MyKafkaProducerProperties {
private Map<String, String> producer= new HashMap<String, String>();
public Map<String, String> getProducer() {
return this.producer;
}
}
In your application.properties you can specify the properties like this:
kafka.producer.foo=hello
kafka.producer.bar=world
And in your configuration you can use it like this:
#Configuration
#EnableConfigurationProperties(MyKafkaProducerProperties.class)
public class FactoryBeanAppConfig {
#Bean
public Producer<String, String> producer(MyKafkaProducerProperties kafkaProperties) throws Exception {
Properties properties = new Properties();
for ( String key : kafkaProperties.getProducer().keySet() ) {
properties.setProperty(key, kafkaProperties.getProducer().get(key));
}
Producer<String, String> producer = new KafkaProducer<String, String>(properties);
return producer;
}
}
You can define a new bean that is annotated with #ConfigurationProperties, like this:
#Bean
#ConfigurationProperties(prefix = "kafka")
public Properties kafkaProperties() {
return new Properties();
}
#Bean
public Producer<String, String> producer() throws Exception {
return new KafkaProducer<String, String>(kafkaProperties());
}
(taken from https://stackoverflow.com/a/50810923/500478)
#Autowired Environment environment;
private Properties getProperties() { return new Properties() { #Override public String getProperty(String name) { return environment.getProperty(name); } }; }

Wire multiple config classes using dependency injection

I have working configuration class in spring. I tried to replace hard-coded string by configuration map using dependency injection.
#Configuration
#Component
public class BwlConfiguration {
#Resource(name="loadParameters")
private Map<ConfigEnum, String> conf;
private String address;
public BwlConfiguration() {
address = conf.get(SPI_BL);
}
...
}
Class that provides conf map:
#Configuration
#Component
public class ConfigLoader {
#Resource(name="returnEnv")
private Map<String, String> env;
#Bean
public Map<ConfigEnum, String> loadParameters() throws ParameterNotSetException{
....
return parameterMap;
}
Class that provides env map:
#Configuration
public class EnvConf {
#Bean
public Map<String, String> returnEnv(){
return System.getenv();
}
}
When I run the program, nullPointerException is thrown at address = conf.get(SPI_BL); line. I tried to replace #Component by #Import(...class), same result and it's losing the point of injection.
Am I using these annotations wrong? Thanks
I replaced constructor in BwlConfiguration with:
#Bean
public String address(){
return conf.get(SPI_BL);
}

Categories

Resources