Replicable State with Spring

Recently I have come across a problem, which does not have an elegant solution. Imagine you have a setup like this:

  • Nice little (or not so little) application based on the Spring-Framework
  • Lots of utilities and services that are waiting to be injected into something
  • Services/Components which represent singletons and do have state, all managed through Spring
  • Application State uses these services
  • Application State is replicated: for example loaded from a file before injected with the Spring Beans
  • Application State is ‚updated‘: the state is changed from the outside after it is created (think repeated loading replacing the old state)

If it were not for the last point the preferred solution would be to implement you own BeanFactory, create a new Spring context into which the newly created beans are pushed into and make it a child context of the already existing context. The BeanFactory would use the existing context to do the wiring on the newly created beans.

However when this replicating happens repeatedly you would end up with a huge context hierarchy, over which you have no control. What you want instead is some Hot soaping of the state in the existing context, but we all know how well these things work.

In my concrete use case the application consists of multiple parts, which do not run necessarily in the same VM, however share the same state. Another approach would be to have the state not managed by Spring. This however would complicate the application code considerably as the whole state would have to be passed around everywhere and each part would have to navigate through the model hierarchy to retrieve their required components.

So here is what I have come up with:

The basic idea is to have a Bean instantiated by Spring, which does not have its state initialized. Then comes along the state with which the bean should be enriched. For the purpose of this example I chose this simple Model:

@Component
public class Person {
    private String name;
    private String lastName;
    private Address address;
    @Autowired
    private Random rnd;
}

public class Address {
    private String address;
}

For the outside update of these beans I have chosen to unserialize some objects of the same structure (without the bean related stuff) from XML.

Here is the main function:

    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring-context.xml");

        Person p = context.getBean(Person.class);
        assert(p != null);
        Address adr = context.getBean(Address.class);
        assert(adr != null);

        JaxbMarshallerUtil marchallUtil = context.getBean(JaxbMarshallerUtil.class);
        try {
            XMLAddress xmlAdr = marchallUtil.unmarshal("person.xsd", "address.xml", XMLAddress.class);
            assert(xmlAdr != null);
            assert(xmlAdr.getAddress().equals("Main road 4711"));
            XMLPerson xmlPers = marchallUtil.unmarshal("person.xsd", "person.xml", XMLPerson.class);
            assert(xmlPers != null);
            assert(xmlPers.getName().equals("Rudolph"));
            assert(xmlPers.getLastName().equals("Hurst"));
            assert(xmlPers.getAddress().getAddress().equals(xmlAdr.getAddress()));

            marchallUtil.unmarshallAndInject("person.xsd", "person.xml", XMLPerson.class, p);

            assert(p.getName().equals("Rudolph"));
            assert(p.getLastName().equals("Hurst"));
            assert(p.getAddress().getAddress().equals(xmlAdr.getAddress()));

        } catch (JAXBException e) {
            e.printStackTrace();
        } catch (SAXException e) {
            e.printStackTrace();
        }

        System.out.println(adr);
        System.out.println(p);
    }

As you can see the heavy load is delegate to the JaxbMarshallerUtil. There is a XMLPerson which corresponds to person and is annotated, so it can be processed with JAXB. The same is true for XMLAddress and Address.

What the JaxbMarshallerUtil is pretty simple when it is broken down.

  1. The first part is creating the XMLPerson instance from the xml file. I will not bore you with the details here, you can look it up in the project’s code.
  2. Synchronizing the fields of XMLPerson with Person.

This in turn can be split up into two tasks as well:

Figuring out the mapping of the fields. In the example this is done simply by matching the field names. There will be a mismatch for nested types (like the Address in Person). This is intentional here, as the XMLAddress must be synchronized with the Address object. Other approaches using more complex logic or even definition through annotations are thinkable.

/**
 * Map the fields of the unmasshaled class to that of the sync object. If
 * there are any fields that are not defined on the sync Object, an exception
 * is thrown. The matching happens on field names alone (the type is not considered).
 * @param unmashalledClass Class of the object that was unmarshalled
 * @param syncObject Object with which the fields should be synced to
 * @param <S> Type of the unmashalled object
 * @param <T> Type of the sync to object
 * @return map of Fields. Key are fields in S and the values the corresponding fields in T.
 */
private <S, T> Map<Field, Field> mapFields(Class<S> unmashalledClass, T syncObject) {
    Field[] sFields = unmashalledClass.getDeclaredFields();
    Field[] tFields = syncObject.getClass().getDeclaredFields();
    Map<Field, Field> map = new HashMap<>();
    for (Field field1 : sFields) {
        Field field2 = null;
        for (Field field : tFields) {
            if (field.getName().equals(field1.getName())) {
                field2 = field;
                break;
            }
        }
        if (field2 != null) {
            map.put(field1, field2);
        } else {
            throw new IllegalStateException("The field "+field1.getName()+" cannot be found in the synchronize class");
        }
    }
    return map;
}

With the mapping at hand it simple to map the corresponding fields using reflection and with respect to nested types:

/**
 * Copy the fields from <code>unmarshalled</code> to <code>syncObject</code>.
 * @param syncObject Object the values are set on
 * @param unmarshalled Object from where the values are taken
 * @param <S> Type of the unmashalled object
 * @param <T> Type of the sync to object
 */
private <S, T> void injectFields(T syncObject, S unmarshalled) {
    Map<Field, Field> fieldMapping = mapFields(unmarshalled.getClass(), syncObject);
    for (Map.Entry<Field, Field> fieldFieldEntry : fieldMapping.entrySet()) {
        Field syncToField = fieldFieldEntry.getValue();
        Field syncFromField = fieldFieldEntry.getKey();
        boolean accessibleTo = syncToField.isAccessible();
        boolean accessibleFrom = syncFromField.isAccessible();
        if (!accessibleTo) {
            syncToField.setAccessible(true);
        }
        if (!accessibleFrom) {
            syncFromField.setAccessible(true);
        }
        try {
            if (syncToField.getType() == syncFromField.getType()) {
                System.out.println("Level: "+level+" Set field "+syncToField.getName()+" to "+syncFromField.get(unmarshalled));
                syncToField.set(syncObject, syncFromField.get(unmarshalled));
            } else {
                level++;
                Object innerUnmarshalled = syncFromField.get(unmarshalled);
                Object innerSyncTo =  syncToField.getType().newInstance();
                injectFields(innerSyncTo, innerUnmarshalled);
                syncToField.set(syncObject, innerSyncTo);
                level--;
            }
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } finally {
            if (!accessibleTo) {
                syncToField.setAccessible(false);
            }
            if (!accessibleFrom) {
                syncFromField.setAccessible(false);
            }
        }
    }
}

The IntelliJ Maven project can be downloaded as zip archive.

Schreibe einen Kommentar