The Why and How Behind Listener-Based Dependency Injection

The Why and How Behind Listener-Based Dependency Injection

he Spring Framework introduced many people to the idea of dependency injection, the notion that instead of looking up objects you need to collaborate with, you simply have them injected into your class via JavaBean-style getters and setters. This article introduces a new, complimentary form of dependency injection that is based not on properties but on the idea of channels and callback listeners. Much like classic dependency injection (which I consider property-based), this listener-based injection has the ability to reduce boilerplate code, promote looser coupling, and help make code more testable and reusable.

In the same way that the JavaBean introspection used in dependency injection can infer properties by the presence of methods such as setPropertyName/getPropertyName, the listener introspection used in listener injection can infer communication channels on an object from methods such as addChannelName/removeChannelName, as the method signature defines the listener or callback interface. This article demonstrates how to use listener injection to clearly communicate relationships between POJOs using the Spring framework.

Spring-Based Injection Example
Suppose you have an application to which you want to introduce file-based input. So you need something that will poll a directory for files being added. Enter JPoller, an open source project that has all the capabilities you need. You decide to add it to your application using Spring, which makes it easy to define an instance of your poller as well as your listener class.

JPoller defines a PollManager callback interface. You provide an instance of this interface to receive the “file found” events. The basic configuration in Spring might look as follows:

    <bean id="poller"          class="org.sadun.util.polling.ManagedDirectoryPoller"           init-method="startUp"          destroy-method="shutDown">        <property name="autoMove" value="true" />        <property name="pollInterval" value="2000" />        <property name="directories">            <list>                <value>/polldir</ value>            </list>        </property>    </bean>    <bean id="listener" class="example1.Callback" /></beans>

With an instance of your poller and your listener class defined, the only remaining issue is how you register your listener bean with the poller. Somewhere, your code has to call the ManagedDirectoryPoller.addPollManager() method to associate your listener with the polling object. Standard Spring effectively offers only three options for where to place this code:

  1. You could subclass ManagedDirectoryPoller and add an init or setter method that can be called from Spring for registration.
  2. You could add the code to your listener class and pass it a reference to the poller bean so that it could add itself.
  3. You could define a third class that knows about both the poller and the listener and adds them.

All three of these approaches requires writing code, and none of them would convey exactly where the addPollManager() method is invoked just from looking at the configuration.

Enter Listener-Based dependency injection
With listener-based dependency injection you can state this relationship explicitly. Listener injection introspects the communication channels on ManagedDirectoryPoller and identifies the addPollManager(PollManager) method as a defining messaging channel (named PollManager). Just as with JavaBeans introspection, you can then register your bean with this channel by providing an appropriate listener.

I’ve used Spring’s namespace extensions to extend the Spring XML to support syntax for injecting listeners by channel name. The example application can now fully define the relationship between poller and callback listener (see Listing 1).

Notice the line in Listing 1 containing the tag. This injects the listener bean into the poller bean’s PollManager channel. The process is analogous to injecting a property. The beauty of this is that it communicates the relationship between the objects clearly (you’re also not forced to find a class off of which to hang the call to addPollManager()).

This is the basic usage of listener injection, which is enough for most people to immediately see the benefit, but there really is much, much more.

Staying True to the POJO Model
The PollManager interface from JPoller has nine methods, of which only two are of interest to the example application previously discussed. Not only does this force you to implement several empty methods, it also violates the Inversion of Control you typically associate with dependency injection. A framework (in this case JPoller) is requiring you to implement a specific interface as an implementation technique. The goal of a POJO programming model is to minimize these sorts of requirements from frameworks.

Here too, listener injection can help. You actually aren’t required to implement the listener interface at all. As a first step, consider an example where you define a POJO with the two methods you are interested in from the PollManager interface. The class would look as follows:

public class Callback {	public void fileFound(FileFoundEvent evt) {          ...	}		public void fileSetFound(FileSetFoundEvent evt) {	    ...	}}

Notice you are not implementing the PollManager interface at all, just providing the two methods of interest with the same names and signatures. You just need to specify the mapping of methods in which you are interested, from the PollManager interface to your Callback in configuration (see Listing 2).

The bold section of the configuration in Listing 2 shows you mapping methods to your POJO. By not specifying mappings for the other methods, those invocations are silently dropped. This is really great in that it reduces boilerplate code, yet still provides some coupling that ties you specifically to JPoller. That is, the FileFoundEvent and the FileSetFoundEvent objects still reflect a compile-time dependence on the JPoller libraries. If you could remove your dependence on these objects, you would be able to use JPoller as an implementation while maintaining loose coupling with your POJO programming model.

So for the third and final example, change the callback more severely by defining the methods you want to define without using any JPoller-specific classes as either an ancestor or in the method signatures:

public class Callback {	public void handleFile(Object src, File f) {		...	}		public void handleFiles(Object src, File[] files) {		...	}}

Notice that your callback methods, in addition to using different names, don’t even have the same number of arguments in the method calls. Still with listener injection, the mapping is straightforward (see Listing 3).

In Listing 3, you can see the map-method section is fleshed out a little more. The method name mapping is the same as in Listing 2, but there’s also a section listing arguments. These arguments correspond to the arguments that will be passed into the new method on the callback class. Under the covers, listener injection is using the Jakarta Commons beanUtils notion of nested properties.

In the first mapped method, the fileFound method is mapped to the handleFile method. The first argument passed into handleFile will be the poller bean (as retrieved from the BeanFactory), while the second argument passed into handleFile is the file property on the FileFoundEvent. (For more details, see the javadocs of the BeanFactoryAwareGenericMessage and its parent GenericMessage, which is the actual object to which the nested properties are relative).

Now That You’ve Scratched the Surface…
This basic introduction to listener-based dependency injection is really just the beginning. With listener injection, you could also inject sophisticated listeners that, for example, adapt a POJO listener/callback style of programming for publishing to JMS destinations or for an ESB-based implementation?while preserving a unit-testable POJO class design. You could even start with an example like the one discussed at the beginning of this article and convert it to use JMS or an ESB entirely with configuration.



About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist