There are
many ways to perform DI, and many container frameworks that use them.
The main element of DI is that you create relationships between objects. These relationships are normally uni-directional, but can be bi-directional. The main challenge for any container to solve is how to link together objects, even when they form a circular dependency. The secondary aspect of a container is how to manage the lifecycle of these relationships.
In fact, very few containers need to manage a complex lifecycle of their contents and their inter-relationships.
Containers in general are just a set of related objects. They provide interfaces to be able to access and manipulate certain objects within the container.
An XML container would simply be a defintion of Java Objects where their relationships were injected by using references in the XML. Taking that XML and turning it into Java Objects will automatically create a set of related objects, in other words a simple container.
What the XML container will not do is give you any form of lifecycle. The contents of the container exist, ready to run, they have no construction step.
Looking at what is inside a container. If we model the contents as a Collection, which for practical reasons that we will go into later is implemented with an ArrayList. You might define the Movie example container as follows:
<list>
<example.xstream.container.ColonDelimitedMovieFinder>
<filename>mydb.txt</filename>
</example.xstream.container.ColonDelimitedMovieFinder>
<example.xstream.container.MovieLister>
<finder class='example.xstream.container.ColonDelimitedMovieFinder'
reference='../../example.xstream.container.ColonDelimitedMovieFinder'/>
</example.xstream.container.MovieLister>
</list>
The MovieLister depends on having a MovieFinder which it refers to as 'finder', here is the relevant source:
public class ColonDelimitedMovieFinder implements MovieFinder {
private String filename;
...
}
public class MovieLister {
private MovieFinder finder;
...
}
It does not matter whether the related objects are set by constructors or setters, because XStream renders them whole and fully formed. They can even be declared as final without issue.
In some circumstances you might have multiple objects of the same type in your container. To deal with this you can use the other aspects of XPath, in this case branches, as follows:
reference='../../example.xstream.container.ColonDelimitedMovieFinder[2]'
As a matter of style you should keep the XPath references as relative paths so that you can manipulate the high level structure of your XML files without upsetting your relationships.
The reason for using a list as the implementation is to imply an order. You would sensibly create an XML container definition file with the objects defined in order of dependency. But there are times when this is not possible or desirable. One such time is when there is a bi-directional dependency. To define this you need to describe one object nested inside the other as in this example:
<list>
<experiment.example.xstream.OneThing>
<other>
<one reference='../..'/>
</other>
</experiment.example.xstream.OneThing>
<experiment.example.xstream.OtherThing
reference='../experiment.example.xstream.OneThing/other'/>
</list>
The two objects of OneThing and OtherThing type both relate to the other. They are both defined in the representation of OneThing, but because the OtherThing is also a high level member of the container it is referenced at the top level also.
On the inside the XML container has a collection of objects, but from the outside how do you go about gaining access to those objects?
There are probably lots of ways to do this. But I would suggest that this one is worth looking into.
XStream has a powerful feature of being able to recreate Proxy objects. By using a Proxy object to wrap a container you can delegate method calls on the container to its contents using a simple set of steps. These steps are:
Here is the invocation handler code:
public class ContainerInvocationHandler implements InvocationHandler {
private static final XStream XSTREAM = new XStream();
private final String filename = null;
private transient Collection contents;
public Object invoke(Object proxy, Method method, Object[] arguments)
throws Throwable {
// lazy load
if (contents == null) {
contents =
(Collection) XSTREAM.fromXML(
new InputStreamReader(
this.getClass().getResourceAsStream(filename)));
}
// get the interface class
Class interfaceClass = method.getDeclaringClass();
for (Iterator each = contents.iterator(); each.hasNext();) {
Object content = each.next();
if (interfaceClass.isInstance(content)) {
return method.invoke(content, arguments);
}
}
throw new NoSuchMethodException();
}
}
And to provide a suitable container to use your definition of the external face of your container is like this:
<dynamic-proxy>
<interface>example.xstream.container.MovieResource</interface>
<interface>example.xstream.container.MovieFinder</interface>
<handler class='example.xstream.container.ContainerInvocationHandler'>
<filename>myfile.xml</filename>
</handler>
</dynamic-proxy>
The proxy definition lists the supported interfaces of the container and defined the xml file to read the contents from. You can define the contents directly in your handler if you prefer, but having separate files for the external and internal points of view make sense from a separation of concerns perspective.
So in order to use your container you can write a container factory that has the code:
Object container = XSTREAM.fromXML(
new InputStreamReader(
this.getClass().getResourceAsStream(filename)));
The factory can then be used as:
MovieResource myMovieResource =
(MovieResource) myContainerFactory.create("filename.xml");
The same created container can be also cast to any of the other interfaces supported by the container courtesy of the Proxy object.
It is strongly recomended that you make the first element of your contents an identifier of some kind, when using the InvocationHandler defined above. The reason for this is that the Object methods of hashcode(), equals() and toString() are implemented by all contents. You can use simple strings to do this, but you might want to use a more complex identifier.
<list> <string>this container is for movie stuff</string> ... </list>You can also easily make nested container relationships by defining the proxy as a top level element of your list:
<list>
<string>this container is for two subcontainers</string>
<dynamic-proxy>
<interface>example.xstream.container.MovieResource</interface>
<handler class='example.xstream.container.ContainerInvocationHandler'>
<filename>mymoviecontentfile.xml</filename>
</handler>
</dynamic-proxy>
<dynamic-proxy>
<interface>example.xstream.container.DVDResoure</interface>
<handler class='example.xstream.container.ContainerInvocationHandler'>
<filename>mydvdcontentfile.xml</filename>
</handler>
</dynamic-proxy>
</list>
To help you write your XML you can create a little utility that generates the XML for a given object. You can then use this to construct your container and bind its related objects with references. Here is the code for one such utility:
public class PrototypeFileWriter {
private static final XStream XSTREAM = new XStream();
public static void main(String[] args) {
String className = args[0];
try {
Object object = Class.forName(className).newInstance();
System.out.println(XSTREAM.toXML(object));
} catch (Exception e) { // naughty
e.printStackTrace();
}
}
}
The drawback of using this method is that you have to define your objects in XML, and while XStream does a good job of making this a realistic exercise it still falls down when the objects being contained get complicated. This is common when the container needs to hold object from closed sources or java libraries.
The most common form of this is Logger objects, which require some thirty lines of definition.
There are two things you can do about it, firstly you can extend the converters of XStream to support a lightweight representation of the messy object type. And secondly you can put a delegating object in the container that the InvocationHandler knows about, to locate the messy object on demand.
For the Logger example a custom converter works well. Here is an example of the source of such a converter for
public class LoggerConverter implements Converter {
public boolean canConvert(Class clazz) {
return Logger.class.isAssignableFrom(clazz);
}
public void marshal(
Object object,
HierarchicalStreamWriter writer,
MarshallingContext context) {
Logger logger = (Logger) object;
writer.setValue(logger.getName());
}
public Object unmarshal(
HierarchicalStreamReader reader,
UnmarshallingContext context) {
String name = reader.getValue();
return Logger.getLogger(name);
}
}
This makes your container definition a lot simpler:
<list> <string>this container is for movie stuff</string> <org.apache.log4j.Logger>my movie channel</org.apache.log4j.Logger> ... </list>And to make it work you just need to register the new converter with your XStream instance:
XSTREAM.registerConverter(new LoggerConverter());In general to avoid messy object definitions in your xml files you should aim to contain factories and accessors rather than the objects themselves. This has the added advantage of avoiding imposing a cardinality on the objects.
As mentioned above, the XML container works for a static container. Because there is no construction, then there is no implicit lifecycle. For most uses of containers will be sufficient. But if you need a lifecycle then you will need to implement this in your container object - the InvocationHandler instance.
if (contents == null) {
contents =
(Collection) XSTREAM.fromXML(
new InputStreamReader(
this.getClass().getResourceAsStream(filename)));
// put initialisation code here
}
An
example of what you might do is use reflection to check for an init()
method and call it on each element in turn. Or you could go the whole
way and implement the instantiation, logging, contextualization,
service, configuration, parameterisation, initialisation, execution,
suspension, resumption, and disposal lifecycle definition.
Another way to achieve a lifecycle is to wrap
your list definition in a custom class, e.g. LifecycleContainer, and
then use a LifecycleContainerConverter to execute the lifecycle upon
unmarshalling.
If you have a complex lifecycle then you should consider using something like Avalon instead.