Java StartUp Config Properties

config Properties

In this tutorial we demonstrate multiple ways to read, write and persist configuration (config) properties for your project. As soon as you start to deploy your code for production, most of the time you have to configure your program to the user´s requirements. E.g. setting up a path to a database, user name and password.

The larger your project grows, the more configuration will be required. You can of course work via command line arguments. Libraries exist that work with key-value pairs, array data etc. being send via command line at the start up of the program. However, we prefer to do as little as possible via command line arguments. Most of the time we point to some sort of configuration file which the program should read, adapt some modi and then continue to initialize.

In the following we demonstrate three (pretty similar) ways to read your program´s configuration data at start up time. For now the simplest approach, key-value pairs are supported.

You can download the provided examples as Maven project on Github. Check this tutorial to install Maven on your machine.

1. General information

Let us assume we have an application that requires access to a database. We need user name and password as well as the database location. We want to access this data in the code in a static way and maybe in multiple locations.

Doing so via command line arguments (without external library support) means we have to access the arguments array via indices. In my opinion that is cumbersome.

user:admin
password:secret
db:localhost

At start up we want to work with the user “admin” and the password “secret” to connect to a database located at “localhost”. Because the user, password or database location may change for each user setup, hard coding these values into the code leads to a rebuild whenever we have to change these variables. Lets see what other options exist.

2. Java Properties

Java natively supports key-value pairs configuration files. You probably have seen these in the Logger configuration properties which is the same approach.

# Configuration
user=admin
password=secret
db=localhost

That is how a Java properties configuration file may look like. In the provided code we have a class that offers a read and write method to get or change the key-value data.

public class PropertiesConfigReader implements ConfigReader {

	private String filePath = null;
	private Properties properties = null;
	
	/**
	 * Read and parse the properties file and store in class object
	 * @param filePath
	 */
	public PropertiesConfigReader( String filePath ) {
		this.filePath = filePath;
		
    	properties = new Properties();
		// open config file
    	InputStream in = null;
		try {
			in = new FileInputStream( filePath );
			properties.load( in );
		} catch ( Exception e ) {
			e.printStackTrace();
		}
		finally {
    		if ( in != null ) {
    			try {
    				in.close();
    			} catch ( IOException e ) {
    				e.printStackTrace();
    			}
    		}
		}
	}
	
	/**
	 * Read from the in memory class object; no file access required
	 */
	public String read( String key ) {
		return properties.getProperty( key );
	}

	/**
	 * Write to the class object (changes available at runtime) and persist the data onto the file system
	 */
	public void write( String key, String value ) {
		properties.setProperty( key, value );
		
		// persist data into property file
		OutputStream output = null;

    	try {
    		output = new FileOutputStream( filePath );

     		// save properties to given path
    		properties.store( output, null );

    	} catch ( IOException ioe ) {
    		ioe.printStackTrace();
    	} finally {
    		if ( output != null ) {
    			try {
    				output.close();
    			} catch ( IOException e ) {
    				e.printStackTrace();
    			}
    		}

    	}
	}

}

In the constructor we read the provided file and parse it into a Properties object. We can then get and set the data via the read and write methods. The file is parsed once at creation. For the read we access the local object which is kept in memory. The write method adds a new property to the local object and rewrites the file to the hard disk (persistent).

3. XML file

The Java Properties work with XML files as well. We do not need another library or XML parser for that. The following XML configuration file contains the same information as the properties file above.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
	<comment>Some properties</comment>
	<entry key="user">admin</entry>
	<entry key="password">secret</entry>
	<entry key="db">localhost</entry>
</properties>

The “comment” tag is not required. To read and write this data we use almost the exact same class as for Java Properties. The only difference is on the initial read and the write, where we use a XML conform method of the Properties class.

public class XmlConfigReader implements ConfigReader {

	private String filePath = null;
	private Properties properties = null;
	
	/**
	 * Read and parse the XML file and store in class object
	 * @param filePath
	 */
	public XmlConfigReader( String filePath ) {
		this.filePath = filePath;
		
    	properties = new Properties();
		// open config file
    	InputStream in = null;
		try {
			in = new FileInputStream( filePath );
			properties.loadFromXML( in );
		} catch ( Exception e ) {
			e.printStackTrace();
		}
		finally {
    		if ( in != null ) {
    			try {
    				in.close();
    			} catch ( IOException e ) {
    				e.printStackTrace();
    			}
    		}
		}
	}
	
	/**
	 * Read from the in memory class object; no file access required
	 */
	public String read( String key ) {
		return properties.getProperty( key );
	}

	/**
	 * Write to the class object (changes available at runtime) and persist the data onto the file system
	 */
	public void write( String key, String value ) {
		properties.setProperty( key, value );
		
		// persist data into property file
		OutputStream output = null;

    	try {
    		output = new FileOutputStream( filePath );

    		// save properties to given path
    		properties.storeToXML( output, null );

    	} catch ( IOException ioe ) {
    		ioe.printStackTrace();
    	} finally {
    		if ( output != null ) {
    			try {
    				output.close();
    			} catch ( IOException e ) {
    				e.printStackTrace();
    			}
    		}

    	}
	}

}

As you can see it is no effort to switch from Properties to XML using the Java Properties Class. Let´s continue to the third solution offered in this tutorial, the all popular JSON.

4. JSON file

The example configuration from above provided in JSON:

{
	"user": "admin",
	"password": "secret",
	"db": "localhost"
}

For JSON support we use a third party library com.googlecode.json-simple. The dependency is provided in the pom.xml file in the Git repository. The config reader class for JSON is demonstrated below:

public class JsonConfigReader implements ConfigReader {

	private String filePath = null;
	private JSONObject jsonObject = null;
	
	/**
	 * Read and parse the JSON file and store in class object
	 * @param filePath
	 */
	public JsonConfigReader( String filePath ) {
		this.filePath = filePath;
		
		JSONParser parser = new JSONParser();

		// parse and keep the JSON object for fast read access
        try {
            jsonObject = (JSONObject) parser.parse( new FileReader( filePath ) );
        } catch ( Exception e ) {
            e.printStackTrace();
        }
	}
	
	/**
	 * Read from the in memory class object; no file access required
	 */
	public String read( String key ) {
		return (String) jsonObject.get( key );
	}

	/**
	 * Write to the class object (changes available at runtime) and persist the data onto the file system
	 */
	@SuppressWarnings("unchecked")
	public void write( String key, String value ) {
		jsonObject.put( key, value );

		try( FileWriter file = new FileWriter( filePath ) ) {
			file.write( jsonObject.toJSONString() );
			file.flush();
		} catch ( IOException e ) {
			e.printStackTrace();
		}
	}

}

We follow the same approach here: read the file once, access the data from the object in the memory and write changes to the object as well as to the hard disk.

5. Application example

We use an interface “ConfigReader” to provide the read and write methods. Hopefully in production you use only one of these configuration file presentations.

We run a simple test where we create the three readers presented earlier, read an attribute, change that attribute and read it again. If you work with Maven, keep in mind that during runtime we do not access the configuration files in “startup-config-properties\src\main\resources” but in “startup-config-properties\target\classes”. So if you want to see the changed files refer to “startup-config-properties\target\classes”. To reset the adapted config files to the revision from this repository, you have to make a “clean install” via Maven.

public class ConfigReaderRun 
{
	// public static to be accessible anywhere in our code
	// make sure to initialize the config reader before you work with it (e.g. the main entry point, some initialization class) in the code
	// if all data is stored in one config file check for singelton pattern
	// --> this way you can reuse one instance and only read the config file once
	// if one config reader should read multiple config files, keep the existing pattern to initialize more than one instance 
	public static ConfigReader propertyConfigReader = null;
	public static ConfigReader xmlConfigReader 		= null;
	public static ConfigReader jsonConfigReader		= null;

	public static void main( String[] args )
    {
		String path = null;

    	/************************************************************************************************/
    	/* Properties */
    	/************************************************************************************************/
		path = getRelativeResourcePath( "config.properties" );
		
		ConfigReaderRun.propertyConfigReader = new PropertiesConfigReader( path );
    	
    	System.out.println("PropertyConfigReader:");
    	testConfigReader( ConfigReaderRun.propertyConfigReader );
    	System.out.println();
    	
    	/************************************************************************************************/
    	/* XML */
    	/************************************************************************************************/
    	path = getRelativeResourcePath( "config.xml" );
    	
    	ConfigReaderRun.xmlConfigReader = new XmlConfigReader( path );
    	
    	System.out.println("XmlConfigReader:");
    	testConfigReader( ConfigReaderRun.xmlConfigReader );
    	System.out.println();
    	
    	/************************************************************************************************/
    	/* JSON */
    	/************************************************************************************************/
    	path = getRelativeResourcePath( "config.json" );
    	
    	ConfigReaderRun.jsonConfigReader = new JsonConfigReader( path );
    	
    	System.out.println("JsonConfigReader:");
    	testConfigReader( ConfigReaderRun.jsonConfigReader );
    	System.out.println();
    }
	
	// resolve maven specific path for resources
	private static String getRelativeResourcePath( String resource ) {
		if( resource == null || resource.equals("") ) return null;
		
		return ConfigReaderRun.class.getClassLoader().getResource( resource ).getPath();
	}
    
    private static void testConfigReader( ConfigReader reader ) {
    	System.out.println( reader.read( "user" ) );
    	reader.write( "user", "newUser" );
    	System.out.println( reader.read( "user" ) );
    }

}

This main class initializes the three different config readers at startup and reads and writes some user data.

As seen in the comments in line 3-7, we declare the config readers static. This way you can access them anytime without having them to be available as parameters. We would suggest to encapsulate the initialization in another file to keep the code clean.

6. Conclusion

This tutorial provided three ways to read and write configuration files of different formats. In production we suggest to use only one format as far as possible. This reduces the required code, simplifies changes and increases maintainability.

Furthermore, the code is kept simple and only deals with key-value pairs. If you require more complex structures, you have to adapt the code. Arrays, Child nodes / Objects etc. are not supported by the code provided above.

If you have errors or problems, feel free to comment and ask.

Facebooktwitterredditpinterestlinkedinmail

Related posts

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.