Spring properties – multiple values representing a list of objects
There are many cases in a recent project I’ve worked on where we need to configure the app using properties and the properties values represent a list of objects. The list can be a variable size and the objects might not have all the same fields defined. The solution to this that I’ve seen is less than ideal. It involves writing all the fields as one really long string separated by a certain character value. That string is then read in and split when needed. I had a look yesterday on the net for a better solution, but couldn’t find one (might be that I wasn’t sure what to search for). So here’s my simple solution to the problem.
Rather than having a single key in the properties file like:
key=A|B|C
I’d like a properties file to look like:
event.class=uk.co.vsf.utilities.bean.Event
event.size=3
event.0.name=Ball
event.0.date=06/30/2012
event.0.numberOfGuests=1200
event.0.description=Work Ball
event.1.name=Ball
event.1.date=07/14/2012
event.1.numberOfGuests=97
event.1.description=Own Ball
event.2.name=Ball
event.2.date=07/28/2012
event.2.description=Olympics Weekend
The first thing I wrote was the test class:
@ContextConfiguration(locations={"/spring-cfg.xml"})
public class EventsTest extends AbstractTestNGSpringContextTests {
@Autowired
private Events events;
@Test
public void getListOfEventsOk()
{
Event firstEvent = new Event("Ball", "06/30/2012", "1200", "Work Ball");
List<Event> eventsFound = events.getEvents();
assertEquals(eventsFound.size(), 3);
assertEquals(eventsFound.get(0), firstEvent);
}
}
It’s not particularly interesting test class. It simply checks that when the list of events is retrieved, there should be 3 in the list, the first object should equal the object defined above. It’ll also test the spring wiring is correct when running the test.
The next thing I needed was a bean to represent the data from the properties file
public class Event {
public static final String NAME = "name";
public static final String DATE = "date";
public static final String NUMBER_OF_GUESTS = "numberOfGuests";
public static final String DESCRIPTION = "description";
private String name;
private Date date;
private int numberOfGuests;
private String description;
public Event() {
}
public Event(String name, String date, String numberOfGuests, String description) {
this.name = name;
this.date = new Date(date);
if (numberOfGuests != null) {
this.numberOfGuests = Integer.parseInt(numberOfGuests);
}
this.description = description;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((date == null) ? 0 : date.hashCode());
result = prime * result + ((description == null) ? 0 : description.hashCode());
result = prime * result + ((name == null) ? 0 : name.hashCode());
result = prime * result + numberOfGuests;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Event other = (Event) obj;
if (date == null) {
if (other.date != null)
return false;
} else if (!date.equals(other.date))
return false;
if (description == null) {
if (other.description != null)
return false;
} else if (!description.equals(other.description))
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
if (numberOfGuests != other.numberOfGuests)
return false;
return true;
}
public String getName() {
return name;
}
public Date getDate() {
return date;
}
public int getNumberOfGuests() {
return numberOfGuests;
}
public String getDescription() {
return description;
}
public void setName(String name) {
this.name = name;
}
public void setDate(Date date) {
this.date = date;
}
public void setNumberOfGuests(int numberOfGuests) {
this.numberOfGuests = numberOfGuests;
}
public void setDescription(String description) {
this.description = description;
}
}
The beans fairly self explanatory. (The setters are for the second part)
I then needed a very simple interface for the Events “service”.
public interface Events {
public List<Event> getEvents();
}
And a spring config file which is fairly basic, but loads the properties file into a spring class PropertiesFactoryBean and injects the properties file into EventsImpl.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd">
<bean id="propertyFactoryBean"
class="org.springframework.beans.factory.config.PropertiesFactoryBean">
<property name="locations">
<list>
<value>classpath:default.properties</value>
</list>
</property>
</bean>
<bean id="events"
class="uk.co.vsf.utilities.properties.EventsImpl"
init-method="afterPropertiesSet">
<property name="properties" ref="propertyFactoryBean" />
<property name="propertyKey" value="event" />
</bean>
</beans>
Last but not least is the EventsImpl class which does all the work.
public class EventsImpl implements Events {
private Properties properties;
private String propertyKey;
private List<Event> events = new ArrayList<Event>();
public void setProperties(Properties properties) {
this.properties = properties;
}
public void setPropertyKey(String propertyKey) {
this.propertyKey = propertyKey;
}
public void afterPropertiesSet() throws Exception {
int countOfPropertiesForKey = Integer.parseInt(properties.getProperty(propertyKey + ".size"));
for (int i = 0; i < countOfPropertiesForKey; i++) {
String name = properties.getProperty(propertyKey + "." + i + "." + Event.NAME);
String date = properties.getProperty(propertyKey + "." + i + "." + Event.DATE);
String numberOfGuests = properties.getProperty(propertyKey + "." + i + "." + Event.NUMBER_OF_GUESTS);
String description = properties.getProperty(propertyKey + "." + i + "." + Event.DESCRIPTION);
Event event = new Event(name, date, numberOfGuests, description);
events.add(event);
}
}
@Override
public List<Event> getEvents() {
return events;
}
}
On startup the afterPropertiesSet method will be called. First of all the size property is read from the properties file. With that count, a loop is created and the properties are read from the file. An Event object is then created and filled in with the values. The Event object is then added to the Event field list.
This solution works, but it’s not very generic as the programmer has to know the types of fields which will be present in the properties file and object and also create an instance of the specific type of object.
The alternative is generic and makes use of BeanUtils from apache commons and also creates a specific class based on the properties value:
public class EventsImpl implements Events {
private Properties properties;
private String propertyKey;
private List<Object> events = new ArrayList<Object>();
public void setProperties(Properties properties) {
this.properties = properties;
}
public void setPropertyKey(String propertyKey) {
this.propertyKey = propertyKey;
}
public void afterPropertiesSet() throws Exception {
int countOfPropertiesForKey = Integer.parseInt(properties.getProperty(propertyKey + ".size"));
String className = properties.getProperty(propertyKey + ".class");
Object clazz = ClassUtils.getClass(className).newInstance();
Map propertyMap = BeanUtils.describe(clazz);
Set<String> fieldNames = propertyMap.keySet();
for (int i = 0; i < countOfPropertiesForKey; i++) {
Object instance = ClassUtils.getClass(className).newInstance();
for(String fieldName: fieldNames)
{
String fieldValue = properties.getProperty(propertyKey + "." + i + "." + fieldName);
BeanUtils.setProperty(instance, fieldName, fieldValue);
}
events.add(instance);
}
}
@Override
public List<Object> getEvents() {
return events;
}
}
Please enable the Disqus feature in order to add comments