In a Java application, we shouldn't hardcode something which changes frequently. For example, database URL, username, passwords etc. If we hardcode those things we need to recompile, rebuild and redeploy the whole application possibly for a single line of change. Restarting a production server is costly. We can overcome this problem using the properties file. We need only redeploy then. No need to recompile and rebuild the application again for some lines of changes.
Properties is a java class present in the java.util package. It was introduced in version 1.0. This Properties class extends the Hashtable<Object,Object> class. This Hashtable class extends the Dictonary<Object,Object> class and implements Map, Clonable and Serializable interfaces. So, in this way, Properties is a Map type collection in Java. In a normal Map(like HashMap, TreeMap etc), the keys and values can be any type. But in Properties, the types of keys and values should be strings.
In Java, properties are files. Conventionally, the extension of the file is '.properties'. But we can put anything or nothing as an extension of the file. Because Java IO is based on UNIX where the extension isn't important.
Key methods
String getProperty(String key)
Searches for the property with the specified key in the property list. If not found then return null.
String getProperty(String key, String defaultValue)
Same as above but return the default value if the key doesn't exist.
Object setProperty(String key, String value)
Calls the Hashtable method put(). This sets a new property in the property file. If the specified property already exists then the new value replaces the old value and returns the old value. This method can't write to the property file, it needs the store() method.
Enumaration<?> propertyNames()
Returns all property names present in the properties object.
void load(InputStream in)
This method loads all properties from the properties file to a Java properties object. Here InputStream is a byte stream. There is also a method that reads from the input character stream. That is
void load(Reader reader)
void store(OutputStream out, String comment)
Writes the Java properties object to the properties file. It also appends the comment specified here.
There are also many more methods in the Properties class.
Code example
public static void main(String[] args) throws IOException {
Properties p = new Properties();
String fileName = "example.properties";
FileInputStream fis = new FileInputStream(fileName);
try(fis) {
//loads properties file
p.load(fis);
String url = p.getProperty("user.name");
System.out.println(url);
String nothing = p.getProperty("nothing.key", "nothing value");
System.out.println(nothing);
System.out.println("=====================================");
//Sets a property in java property object.
p.setProperty("user.motorcycle", "Honda");
/*
* We must open the FileOutputStream after the load() method.
* Otherwise, the original properties will be lost.
*/
try(FileOutputStream fos = new FileOutputStream(fileName)) {
p.store(fos, "updated motorcycle info");
}
//Iterate all keys
p.propertyNames()
.asIterator()
.forEachRemaining(element-> {
String result = MessageFormat.format(
"key is=> {0} and value is=> {1}",
element,
p.getProperty(element.toString())
);
System.out.println(result);
});
//For testing purpose. Without compile it will not reflect
int i = 100;
System.out.println(i);
}
}
Now the point is, first run this code with compilation. Next, make some changes. For example, change the user.name value from Leeon to John Doe. Also, change the value of i from 100 to 500. Now, run this code without compilation. In Intellij Idea(Version: 2021.2.2, Community edition) the option resides in the "Edit configuration" > Modify Options > Check "Do not build before run".
we will see the value of i is unchanged but the user.name has the updated value. The full code is here.
Properties file in Spring via annotations
Spring 3.1 introduces the @PropertySource
annotation for adding a PropertySource to Spring's environment. We need to use this annotation in conjunction with @Configuration
annotation.
@Configuration
@PropertySource("classpath:custom-property1.properties")
public class CustomPropertiesConfig {
}
We can use a placeholder for registering a a properties file dynamically at runtime.
Defining multiple property locations
If we use Java 8 or higher, we can repeat the @PropertySource
annotation.
@Configuration
@PropertySource("classpath:custom-property1.properties")
@PropertySource("classpath:custom-property2.properties")
public class CustomPropertiesConfig {
}
Also, we can use the @PropertySources
annotation and specify array of @PropertySource
annotations.
@Configuration
@PropertySources({
@PropertySource("classpath:custom-property1.properties"),
@PropertySource("classpath:custom-property2.properties")
})
public class CustomPropertiesConfig {
}
Injecting properties
We have to use the @Value
annotation to inject a specific property.
@Value("${name}")
private String name;
We can also define a default value using a colon(:).
@Value("${nameless:this is a default value}")
private String name;
Properties in spring boot
Spring boot simplifies the configuration of the properties file. It uses the application.properties as a default properties file under the/src/main/resources property. Spring boot will auto-detect this file under that specific location.
Handle duplicate key values
When a property key exists on more than one property file, the last property file will override all the previous property files which have the keys with the same name.
Scenario-1
@Configuration
@PropertySources({
@PropertySource("classpath:custom-property1.properties"),
@PropertySource("classpath:custom-property2.properties")
})
public class CustomPropertiesConfig {
}
Scenario-2: Java 1.8 or above
@Configuration
@PropertySource("classpath:custom-property1.properties")
@PropertySource("classpath:custom-property2.properties")
public class CustomPropertiesConfig {
}
Scenario-3
@Configuration
@PropertySource("classpath:custom-property3.properties")
public class CustomPropertiesConfigA {
}
@Configuration
@PropertySource("classpath:custom-property4.properties")
public class CustomPropertiesConfigB {
}
The custom-property1.properties contains
motorcycle.brand=yamaha
The custom-property2.properties contains
motorcycle.brand=honda
The custom-property3.properties contains
car.brand=audi
The custom-property4.properties contains
car.brand=ford
Now if we run the test cases, we can see in both scenarios 1 and 2, the motorcycle.brand will return "honda". And in the 3rd scenario, the car.brand will return "ford".
Sometimes, it is not possible to tightly control the properties files' source ordering using the @PropertySource
annotation.
@SpringBootTest
public class MotorcyclePropertyTest {
@Autowired
private Motorcycle motorcycle;
@Test
public void getMotorcycleBrandFromProperty2() {
assertEquals("honda", motorcycle.getName());
}
}
@SpringBootTest
public class CarPropertyTest {
@Autowired
private Car car;
@Test
public void getCarBrandFromProperty4() {
assertEquals("ford", car.getName());
}
}
Test a specific properties file
Spring boot handles the test-specific property files by scanning the '/src/test/resources directory. We can put the properties files here, spring boot will auto-detect the properties files.
But if we need more control over properties files then we need the @TestPropertySource
annotation for injecting a specific properties file.
@ExtendWith(SpringExtension.class)
@TestPropertySource("/custom-property1.properties")
public class InjectPropertyTest {
@Value("${motorcycle.brand}")
private String name;
@Test
public void nameTest() {
assertEquals("yamaha", name);
}
}
Test property sources have higher precedence than the properties added by the @PropertySource
annotation or other Java or OS-based properties files. But for property collision, how does spring boot resolve precedence? It takes the last source as a high precedence. But as I've used @ComponentScan
auto-configuration, it is difficult to predict the precedence. In such cases, if the ordering is important, we have to use ConfigurableEnvironment and MutablePropertySources. I will discuss those in later posts. The above code is here.
Hierarchical properties
Almost all the properties in a typical properties file are in groups. In that scenario, we can use the @ConfigurationProperties
annotataion. It will map those grouped properties into a Java object graph.
#hierarchical-example1.properties
spring.mail.port=3303
spring.mail.host=192.168.11.102
spring.mail.username=admin
spring.mail.password=root
@Component
@ConfigurationProperties(prefix = "spring.mail")
public class Mail {
private String host;
private int port;
private String username;
private String password;
//Getters and Setters
}
@SpringBootTest
public class HierarchicalTest {
@Autowired
private Mail mail;
@Test
public void mailPropertiesValidation() {
assertEquals(3303, mail.getPort());
assertEquals("192.168.11.102", mail.getHost());
assertEquals("admin", mail.getUsername());
assertEquals("root", mail.getPassword());
}
}
The above code is here.
Nested hierarchical properties
There are 3 ways to define nested properties. They are List, Map and Classes.
#hierarchical-example2.properties
# Object type
spring.data.mongodb.host=fake mongo host
spring.data.mongodb.port=fake mongo port
# Map type
spring.data.redis.host=fake redis host
spring.data.redis.connect-timeout=100
# List type
spring.data.test[0] = custom data 1
spring.data.test[1] = custom data 2
class MongoDB {
private String host;
private String port;
//Setters and Getters
}
@Configuration
@Component
@PropertySource("classpath:hierarchical-example2.properties")
@ConfigurationProperties(prefix = "spring.data")
public class Data {
//Object type
private MongoDB mongodb;
//Map type
private Map<String, String> redis;
//List type
private List<String> test;
// Setters and Getters
}
@SpringBootTest
public class NestedHierarchicalTest {
@Autowired
private Data data;
@Test
public void listValidation() {
List<String> testList = List.of("custom data 1", "custom data 2");
assertEquals(testList, data.getTest());
}
@Test
public void mapValidation() {
Map<String, String> testMap = Map.ofEntries(
Map.entry("host", "fake redis host"),
Map.entry("connect-timeout", "100")
);
assertEquals(testMap, data.getRedis());
}
@Test
public void objectValidation() {
MongoDB m = new MongoDB();
m.setHost("fake mongo host");
m.setPort("fake mongo port");
assertEquals(m.getHost(), data.getMongoDB().getHost());
assertEquals(m.getPort(), data.getMongoDB().getPort());
}
Activating a profile in Spring boot
Profiles allow us to map beans to different profiles. The common profiles are dev, test and prod. There is a common property in spring boot for activating a profile
spring.profiles.active=dev
Profile-specific properties file
The naming format is application-[profile].properties. For example, for dev, application-dev.properties and production, application-prod.properties. We can configure necessary things especially data sources in different profiles.
Property Conversion
We can convert some properties with their corresponding beans using the @ConfigurationProperties
annotation. This is a vast topic, let's see an example of data size conversion.
file.size1=2KB
file.size2=2
@Component
@ConfigurationProperties(prefix = "file")
public class ConversionBean {
private DataSize size1;
@DataSizeUnit(DataUnit.MEGABYTES)
private DataSize size2;
// Getters and Setters
}
@SpringBootTest
public class ConversionBeanTest {
@Autowired
private ConversionBean cb;
@Test
public void KBValidation() {
DataSize d = DataSize.of(2, DataUnit.KILOBYTES);
assertEquals(d, cb.getSize1());
}
@Test
public void MBValidation() {
DataSize d = DataSize.of(2, DataUnit.MEGABYTES);
assertEquals(d, cb.getSize2());
}
}
Custom converter
We can also implement our converter. We need the interface Converter<Source, Target>
and @ConfigurationPropertiesBinding
annotation.
Let's add a class
public class Fruit {
private String name;
private int price;
// Constructor with params, getters and setters
}
The property I've defined
shop.fruit=mango,200
Let's implement the Converter interface
@Component
@ConfigurationPropertiesBinding
public class FruitConverter implements Converter<String, Fruit> {
@Override
public Fruit convert(String value) {
String[] values = value.split(",");
return new Fruit(values[0], Integer.parseInt(values[1]));
}
}
Now, configure the bean
@Component
@ConfigurationProperties(prefix = "shop")
public class FruitBean {
private Fruit fruit;
//Getters and setters
}
Now, test the properties
@SpringBootTest
public class CustomConversionTest {
@Autowired
private FruitBean fruit;
@Test
public void getCustomObject() {
Assertions.assertEquals("mango", fruit.getFruit().getName());
Assertions.assertEquals(200, fruit.getFruit().getPrice());
}
}
Common properties
Properties | Description |
spring.profiles.active=dev | Activating a profile |
server.port=3000 | Change the default server port |
spring.jpa.show-sql | Whether showing the SQL query in the console |
spring.datasource.url | Standard database url |
spring.datasource.username | Database username |
spring.datasource.password | Database password |
And there are many more. Here is the list of all common properties for spring-boot.
All source codes of this article are here.
Please let me know if this post is helpful. I will appreciate any suggestions for improving this blog.