Properties in java and spring boot

Properties in java and spring boot

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

PropertiesDescription
spring.profiles.active=devActivating a profile
server.port=3000Change the default server port
spring.jpa.show-sqlWhether showing the SQL query in the console
spring.datasource.urlStandard database url
spring.datasource.usernameDatabase username
spring.datasource.passwordDatabase 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.

References