How does Spring work I?

11 Aug

One thing I really hate is believing in magic when writing software. It may be possible that we just don’t understand some things, but it is not magic, they are just some things we not know (yet). Just google for “Programming by Coincidence” for more information smiley

There are some frameworks which I often hear about they are doing magic. And I admit, most of them are really not easy to understand. One example is Spring. How can we overcome this? Well, we could read the source. This is actually a very good approach most of the time.  But some Frameworks are just … too complex. So let’s try something different: we implement a very small Dependency Injection (DI) Framework in the style of Spring. This brings us the opportunity to explore the core idea behind Spring. In the first part, we use an XML configuration for this (let’s try Annotations some time in Part 2!). What we want to learn is: how can we build an object graph from an XML file. A word of warning: what we’re doing here is very simple stuff that just explains how things work: We skip things like transaction support etc. But perhaps the first steps are enough to get an idea was is possible and what is not… So let’s start.

I shared the whole project on github. I will talk about the important files here, but have a look at github to see how these files are organized and what glue-code exists.

The Scenario

The first thing we should decide is how the XML file should look. We want to support beans with ids and classes. Beans can have simple values (such as ints or Strings) or other beans as property values. The XML schema behind this is very easy:

<?xml version="1.0" encoding="UTF-8"? >
<schema xmlns="http://www.w3.org/2001/XMLSchema"
   targetNamespace="http://jowisoftware.de/MyDI"
   xmlns:tns="http://jowisoftware.de/MyDI"
   elementFormDefault="qualified">
 
    <element name="bean" type="tns:Tbean" />
    <element name="property" type="tns:Tproperty" />
 
    <element name="beans">
        <complexType>
            <sequence>
                <element ref="tns:bean" maxOccurs="unbounded" />
            </sequence>
        </complexType>
    </element>
 
    <complexType name="Tbean">
        <sequence>
            <element ref="tns:property" minOccurs="0" maxOccurs="unbounded" />
        </sequence>
        <attribute name="id" type="ID" use="required" />
        <attribute name="class" type="string" use="required" />
    </complexType>
 
    <complexType name="Tproperty">
        <attribute name="name" type="string" use="required" />
        <attribute name="ref" type="IDREF" />
        <attribute name="value" type="string" />
    </complexType>
</schema>

Okay, now let’s write two beans and a bean definition file:

<?xml version="1.0" encoding="UTF-8"? >
<beans xmlns="http://jowisoftware.de/MyDI"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema- instance"
   xsi:schemaLocation="http://jowisoftware.de/MyDI ../../../main/xsd/MyDI.xsd ">
 
    <bean class="de.jowisoftware.myDI.integrationtests.test2.Class1" id="bean1">
        <property name="prop" value="Hello From Class 1"/>
        <property name="bean" ref="bean2"/>
    </bean>
   
    <bean class="de.jowisoftware.myDI.integrationtests.test2.Class2" id="bean2">
        <property name="prop" value="Hello From Class 2"/>
        <property name="bean" ref="bean1"/>
    </bean>
</beans>

 

public class Class1 {
    private String prop;
    private Class2 bean;
     
    public String getProp() {
        return prop;
    }
     
    public void setProp(final String prop) {
        this.prop = prop;
    }
  
    public Class2 getBean() {
        return bean;
    }
     
    public void setBean(Class2 bean) {
        this.bean = bean;
    }
}
     
public class Class2 {
    private String prop;
    private Class1 bean;
     
    public String getProp() {
        return prop;
    }
     
    public void setProp(final String prop) {
        this.prop = prop;
    }
    
    public Class1 getBean() {
        return bean;
    }
     
    public void setBean(final Class1 bean) {
        this.bean = bean;
    }
}

We have both kinds of properties here: simple values and references (they are even circular!). Next we create an acceptance test to define a few expectations (there are more tests on github):

public class Test2IT {
    @Test
    public void contextIsInitializedCorrectly() {
        final DIContext context = new DIXmlContext(
                "/integrationTests/dependencies.test2.xml") ;
 
        final Class1 bean1 = (Class1) context.getBean("bean1");
        final Class2 bean2 = (Class2) context.getBean("bean2");
 
        assertThat (bean1.getBean() , is(instanceOf(Class2.class)));
        assertThat (bean2.getBean() , is(instanceOf(Class1.class)));
 
        assertThat (bean1.getBean() .getProp(), is(equalTo(bean2.getProp())));
        assertThat (bean2.getBean() .getProp(), is(equalTo(bean1.getProp())));
 
        assertThat (bean1.getBean() , is(sameInstance(bean2)));
        assertThat (bean2.getBean() , is(sameInstance(bean1)));
    }
}

And this is the mentioned Intefaces DIContext:

public interface DIContext {
    Object getBean(String id);
    <T> T getBean(Class<? extends T> clazz);
}

Well, that really looks like Spring, doesn’t it?

Step 1: Read the XML File

So far, there shouldn’t be anything surprising. The funny stuff is still ahead. Now let’s dive into the real DI work. As this is the really interesting part, we go through it a littlemore in detail. The idea is to build up our object graph in four steps:

  1. Read the provided XML file
  2. Create an object for every specified bean – do nothing with it
  3. Iterate over all beans again: resolve all properties
  4. Call some kind of init-Method

The first step should be clear. But the rest is not that obvious, so let’s go through all of them one by one.

The first question which arises is: how do we parse the XML file. There a several aproaches. The example project is a maven Project, so let’s use jaxb; this will cost us only a single line of code. We tell Maven to generate the model classes for us and use JAXB.unmarshal to load an XML file. This is the pom snippet:

       <build>
            <plugins>
                ...
                <plugin>
                    <groupId>org.codehaus.mojo</groupId>
                    <artifactId>jaxb2-maven-plugin</artifactId>
                    <version>1.5</version>
     
                    <executions>
                        <execution>
                            <id>xjc</id>
                            <goals>
                                <goal>xjc</goal>
                            </goals>
                        </execution>
                    </executions>
                    <configuration>
                        <packageName>de.jowisoftware.myDI.model</packageName>
                    </configuration>
                </plugin>
            </plugins>
        </build>

This is the beginning of our DIContext class:

public class DIXmlContext implements DIContext {
    private final Map<String, Object> beans = new HashMap<>();
 
    public DIXmlContext(final String xmlFile) {
        final Beans beansDef = loadXmlFile(xmlFile);
        initializeBeans (beansDef);
    }
 
    private Beans loadXmlFile(final String xmlFile) {
        try (final InputStream is = getClass().getResourceAsStream(xmlFile) ) {
            return JAXB.unmarshal(is, Beans.class);
        } catch (final IOException e) {
            throw new DIInitializationException("Could not read xml file", e);
        }
    }

We ignore the DIInitializationException for now, it is just a class which extends RuntimeException, see the github project for more details. JAXB is used to read our XML file. But where does the class Beans come from? This is the place where the defined maven plugin kicks in: in generates the classes Beans, TBean and TProperty from the XSD file (you can run “mvn generate-sources” to generate the files; otherwise maven will generate them, if they are needed). This code is exactly what the first step’s task was: read the XML file.

Step 2: Create the Bean Objects

After we have objects which represent our bean definitions, let’s create the real beans. This is the second item in our task list (this is the continuation the DIXmlContext class):

   private void initializeBeans(final Beans beansDef) {
        firstPassInit (beansDef);
        secondPassInit (beansDef);
        thirdPassInit (beans);
    }
 
    private void firstPassInit(final Beans beansDef) {
        final BeanCreator creator = new BeanCreator();
        for (final Tbean bean : beansDef.getBean()) {
            final String id = bean.getId();
            beans.put(id, creator.createBean(id, bean.getClazz()));
        }
    }

We extract the responsibility of creating the bean into an own class, but it is very small:

public class BeanCreator {
    public Object createBean(final String id, final String className) {
        try {
            final Class<?> classInstance = Class.forName(className);
            return classInstance.newInstance();
        } catch (ClassNotFoundException | InstantiationException
                | IllegalAccessException e) {
            throw new DIInitializationException("Could not initialize bean "
                    + id + " of type " + className, e);
        }
    }
}

What happens here? The for-Loop reads our list of Beans. Every bean has an id and an class attribute. We have a Map named beans which holds all our beans indexed by id. So we create a bean and put it into the map. How is the bean created? By using java reflections! Class.forName takes a String as argument and returns the corrosponding Class object. This object can be used do dive into the class: we can enumerate the fields and methods, and we can call them! For the moment, we only need newInstance (but we will come back to the Class object in the next step). newInstance call the default constructor of a class. If no default constructor exists, we throw an exception. As we have no idea what we are dealing with, all Beans are Objects.

If we take the above bean definition, this code does the following: it finds two beans, one with the id “bean1” and the class “de.jowisoftware.myDI.integrationtests.test2.Class1”, and one with “bean2” and the class “de.jowisoftware.myDI.integrationtests.test2.Class2”. It looks up a class “de.jowisoftware.myDI.integrationtests.test2.Class1” and creates a new instance of it. Then the instance is stored as “bean1”.  The same happens for “bean2”.

At this moment, the beans are not initialized. There is a reason for this: we have no other way to resolve circular dependencies. bean1 can only be initialized when bean2 is created, and bean2 depends on bean1. If the definition of the beans would be a directed acyclic graph (DAG), then we could initialize the beans bottom up, but we use a simpler approach here. When we now fill the properties of bean1, we can inject a reference to bean2 – which is not initialized yet, but at least it exists. That’s enough for the moment, so we inject it and finish initializing bean1. Then we initialize bean2 and add a reference to the already initialized bean1. At the end, we have both beans initialized.

Go fetch yourself a coffee. Read the article again up to this paragraph. Download the project, start it, debug it up to the call to secondPassInit. Get a feeling what happened so far before reading further.

 

Step 3: Initialize the Beans

So far, we are able to read an XML file and use reflections to create the java objects by their class name. We store them in a map. If you don’t feel insecure, go ahead with this new method in DIXmlContxt:

    private void secondPassInit(final Beans beansDef) {
        final BeanInitializer initializer = new BeanInitializer (beans);
        for (final Tbean bean : beansDef.getBean()) {
            initializer.initializeBean(bean);
        }
    }

Another iteration initializes the bean. What does BeanInitializer do? Let’s have a look:

public class BeanInitializer {
    private final Map<String, Object> beans;
 
    public BeanInitializer(final Map<String, Object> beans) {
        this.beans = beans;
    }
 
    public void initializeBean(final Tbean bean) {
        for (final Tproperty property : bean.getProperty()) {
            checkProperty(property, bean.getId ());
 
            final Object beanObject = beans.get(bean.getId());
 
            final Method setterMethod = findSetter(beanObject,
                    property.getName() , bean.getId() );
 
            final Object value = getPropertyValue(property, setterMethod,
                    bean.getId() );
 
            setValue(beanObject, setterMethod, value, bean.getId());
        }
    }

To initialize a bean, we iterate over its properties. First we check that exactly one xml attribute is set: value or ref (we will see the code in a second). Then we search a setter method to inject the value into the bean. Next, we calculate the value to be set. The last step is a call to the setter. Let’s go through the code method by method:

    private void checkProperty(final Tproperty property, final String id) {
        if (property.getRef() != null && property.getValue () != null) {
            throw new DIInitializationException("Error in bean " + id
                    + ": property " + property.getName()
                    + ": either ref (" + ((Tbean) property.getRef()).getId()
                    + ") or property ('" + property.getValue()
                    + "') must not be defined") ;
        } else if (property.getRef() == null && property.getValue()==null) {
            throw new DIInitializationException("Error in bean " + id
                    + ": property " + property.getName()
                    + ": either an existing ref or value must be defined");
        }
    }

Well, this was simple and only helps us debugging our context. Let’s talk about findSetter:

    private Method findSetter(final Object beanObject, final String name, final String id) {
        final String upperCaseName = name.substring(0, 1) .toUpperCase()
                + name.substring(1);
        final String methodName = "set" + upperCaseName;
 
        for (final Method method : beanObject.getClass() .getMethods() ) {
            if (method.getName().equals(methodName)
                    && method.getParameterTypes().length == 1
                    && method.getReturnType().equals(Void.TYPE)) {
                return method;
            }
        }
        throw new DIInitializationException("Could not find method "
                + methodName + " in bean " + id) ;
    }

Let’s assume we have the property “myProperty”. We are looking for a Method which:

  • has the name setMyProperty
  • has no return value (i.e. it is a void method)
  • has exactly one argument

Here, we use our Class-object again. We do not know the type of the first argument so we look through all methods. You may notice that we will run into a problem if there are two methods setMyProperty(String) an setMyProperty(int) – and you’re right. We ignoring this problem for now; it would perhaps be nicer to throw an exceptions if two methods with the same name exists, so be brave and add this functionality if you play with your own project wink

We now have our setter method – again only by looking it up by reflection. Now we need the value. This is much code, sorry

    private Object getPropertyValue(final Tproperty property,
            final Method setterMethod, final String id) {
        final String ref = getRefById(property);
        final Object value;
        if (ref != null) {
            value = beans.get(ref);
        } else {
            value = createValue(setterMethod, property.getValue(), id,
                    property.getName() );
        }
        return value;
    }
 
    private Object createValue(final Method setterMethod, final String value,
            final String id, final String property) {
        final Class<?> type = setterMethod.getParameterTypes()[0];
        try {
            if (type.equals(String.class)) {
                return value;
            } else if (type.equals(Integer.TYPE) || type.equals(Integer.class)) {
                return Integer.parseInt(value);
            } else if (type.equals(Float.TYPE) || type.equals(Float.class)) {
                return Float.parseFloat(value);
            } else if (type.equals(Double.TYPE) || type.equals(Double.class)) {
                return Double.parseDouble(value);
            } else if (type.equals(Byte.TYPE) || type.equals(Byte.class)) {
                return Byte.parseByte(value);
            } else if (type.equals(Short.TYPE) || type.equals(Short.class)) {
                return Short.parseShort(value);
            } else {
                throw new DIInitializationException(
                        "Don't know how to transform '" + value + "' into "
                                + type.getSimpleName() + " in bean " + id
                                + ": " + property);
            }
        } catch (final IllegalArgumentException e) {
            throw new DIInitializationException("Could not use '" + value
                    + "' as value of type " + type.getSimpleName()
                    + " in bean " + id, e);
        }
    }

There are two cases: either we have a reference to another bean or we have a value. It may surprise you, but this is the smaller part. If we have a value then… well, we need to transform the value into the right object. We do this by inspecting the setter’s parameter type: is it int or Integer? Then use Integer.parseIt and return an Integer (java’s autoboxing feature does this for us, as we return an Object. There is no need for a call to Integer.valueOf, this is implicitly done here). Is it a Float? Then… okay, just read the code smiley

We do now have a setter method and an object. The object is either a bean or a specified value – we don’t care anymore, it’s just the right object. Now comes the last part: inject the object.

    private void setValue(final Object beanObject,
            final Method setterMethod, final Object value, final String id) {
        try {
            setterMethod.invoke(beanObject, value) ;
        } catch (IllegalAccessException | IllegalArgumentException
                | InvocationTargetException e) {
            throw new DIInitializationException("Could not set [" + value
                    + "] in " + setterMethod.toString() + " in bean "
                    + id);
        }
    }

As a method is associated with a class (and not with a specific object) we need to pass the object and the parameters to its invoke method. The rest is just error handling.

We’re mostly done! We do now have a map of initialized beans. As we created all beans in step 1, the order of initializing them was not important.

Step 4: Call Initialization Methods

There is one last step which is useful if the beans needs a constructor. As we call the setter methods after the bean is created, the constructor will only see null values. So we will provide some kind of init method, which we call after the bean is fully initialized. There are two more features that we will add: telling the bean which name it has and giving the DIContext itself to the bean. These are just examples what is possible.

There are at least three ways we could implement this:

  1. By convention over configuration: If there is a method “init”, call it
  2. By configuration: provide a init-method attribute in the XML config (like spring does)
  3. By using interfaces

By now, you should have an idea how the first two solutions work, so let’s do the first one. We define some interfaces:

public interface Initializable {
    void initialize();
}
 
public interface SelfAware {
    void setName(String name);
}
 
public interface ContextAware {
    void setContext(DIContext context);
}

This was not very complicated. Now the DIXmlContext:

    private void thirdPassInit(final Map<String, Object> beans2) {
        final BeanFunctionCaller caller = new BeanFunctionCaller ();
        for (final Entry<String, Object> entry : beans.entrySet() ) {
            caller.initialize(entry.getValue(), this, entry.getKey());
        }
    }

And the BeanFunctionCaller:

public class BeanFunctionCaller {
    public void initialize(final Object bean, final DIContext context,
            final String id) {
        try {
            if (bean instanceof SelfAware) {
                ((SelfAware) bean).setName(id);
            }
 
            if (bean instanceof ContextAware) {
                ((ContextAware) bean).setContext(context);
            }
 
            if (bean instanceof Initializable) {
                ((Initializable) bean).initialize ();
            }
        } catch (final RuntimeException e) {
            throw new DIInitializationException("Could not initialize bean "
                    + id, e);
        }
    }
}

No more reflections – we can do all this with instanceof. If we implement “SelfAware” in bean1, it’s setName method is called automatically after all setters are filled.

Obtaining the Beans

One question remains: how do we get our beans? The question is simple: if you know the name, just look it up in the map. We will also provide a method to lookup a method by type (this is the last par of our DIXmlContext):

@Override
    public Object getBean(final String id) {
        if (beans.containsKey (id)) {
            return beans.get(id);
        } else {
            throw new InvalidBeanException ("Bean with id " + id
                    + " does not exist");
        }
    }
 
    @Override
    public <T> T getBean(final Class<? extends T> clazz) {
        Object result = null;
        for (final Object bean : beans.values()) {
            if (clazz.isInstance(bean)) {
                if (result == null) {
                    result = bean;
                } else {
                    throw new InvalidBeanException(
                            "Ambiguous class "
                                    + clazz.getName()
                                    + ": at least two beans implement this class");
                }
            }
        }
 
        if (result == null) {
            throw new InvalidBeanException ("A bean which implements "
                    + clazz.getName() + " does not exist");
        }
 
        @SuppressWarnings("unchecked")
        final T castedResult = (T) result;
        return castedResult;
    }
}

We’re done. This code (it’s less than 500 LOC + Tests) is enough for a small DI Framework. It does not have the same flexibility that Spring has, and it won’t be able to replace spring in the most cases. But the goal was to get an idea of what spring does internally.

What are the next steps? Spring can do more: one thing I will perhaps blog about is the initializing of beans by using annotations instead of an XML file. Another thing is aspect oriented programming: Spring can wrap Beans into objects which will transparently open and close database transactions before calling your real method. This will only work in beans, not in any other class with is not managed by spring (can you explain why?). A third thing would be factories, which generate the real bean objects on the fly.

I wanted to show that there is no magic, everything can be explained. We dont’s use a customized java compiler, we don’t even generate java bytecode. We only use reflections to achive our goal. I hope it was fun to play around with this feature and perhaps you do your own experiment now, however: keep exploring wink

One Reply to “How does Spring work I?”

  1. Pingback: Using Java’s Proxy Class to Transparently Manage Transactions | JoWiSoftware

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert