Introduction to HiveMind
HiveMind is a services and configuration microkernel:
- Services: HiveMind services are POJOs (Plain Old Java Objects) that can be easily accessed and combined. Each service ideally defines a Java interface it implements (but this is no longer mandatory). HiveMind takes care of instantiating and configuring each service just as necessary. HiveMind lets services collaborate with each other via dependency injection.
- Configuration: HiveMind allows you to provide complex configuration data to your services in a format you define. HiveMind will integrate the contributions of such data from multiple modules and convert it all into data objects for you. HiveMind configurations allow for powerful, data-driven solutions which combine seemlessly with the service architecture.
- Microkernel: HiveMind is a framework for creating applications, not an application, or even an application server, itself. The 'core' of HiveMind is the startup logic that knows how to parse and understand the module deployment descriptors, and use that information to instantiate and initialize all those services and configurations.
In HiveMind, a service is an implementation of a Java interface. Unlike other SOAs (Service Oriented Architectures, such as a SOAP, or EJBs), HiveMind is explicitly about combining Java code within a single JVM. HiveMind uses a descriptor to describe different services, their lifecycles, and how they are combined. HiveMind takes care of thread-safe, just-in-time creation of singleton service objects so your code doesn't have to.
HiveMind sits between your application code and the underlying J2EE or other APIs:
In this diagram, the application accesses key HiveMind services (the small circles). These services acts as facades; the implementations of the services are dependent on many other services, as well as configurations (the blue stacks of blocks). Service implementations make use of Java and J2EE APIs, and may even "bridge" into other systems such as EJB session beans.
HiveMind is organized around modules: individual building blocks, each providing a particular set of services and configurations. Each module is deployed as its own JAR file, containing its own descriptor. At runtime, HiveMind combines all the modules and their descriptors together ... seemlessly combining the services specific to your application with the services provided by HiveMind and by other third-party libraries.
HiveMind is designed with particular attention to J2EE. Because J2EE applications are multi-threaded, everything in HiveMind is thread-safe. That's one less thing for you to be concerned about.
HiveMind allows you to create more complex applications, yet keep the individual pieces (the individual services) simple and testable. HiveMind enourages the use of common best practices, such as coding to interfaces, seperation of concerns, and keeping code highly testable without a special container.
Status
HiveMind now requires Ant 1.6.2 to build.
James Carman and Achim Huegen have been voted on as HiveMind committers.
You can now omit the package name (generally) when specifying class name in a module descriptor. The <module> element's package attribute provides the package name to use.
The locale (used for localized messages) is no longer fixed, as it was in 1.0. You can change the locale using the hivemind.ThreadLocale service.
The interface for a <service-point> may now be an ordinary class, not an interface. You may now use HiveMind quick-and-dirty without defining an interface for each service.
Upgrade warnings (release 1.0 to release 1.1)
For the most part, code that works with HiveMind 1.0 will work with HiveMind 1.1 as well. There have been a small number of incompatible changes between releases that will not affect the vast majority of users:
- The ServiceImplementationFactory interface has been been simplified, replacing a long list of parameters with a single ServiceImplementationFactoryParameters object.
- The ClassFactory's newClass() method has been simplified; the module parameter has been removed.
- The module parameter has also been removed from the DefaultImplementationBuilder's buildDefaultImplementation() method.
- The SpringLookupFactory and its parameters now specify a Spring BeanFactory instance, rather than a SpringBeanFactorySource.
Acknowledgments
HiveMind represents a generous donation of code to the Apache Software Foundation by WebCT.
HiveMind originated from internal requirements for a flexible, loosely-coupled configuration management and services framework for WebCT's industry-leading flagship enterprise e-learning product, Vista.
Several individuals in WebCT's research and development team, in addition to Howard Lewis Ship, contributed to the requirements and concepts behind HiveMind's initial set of functionality. These include Martin Bayly, Diane Bennett, Bill Bilic, Michael Kerr, Prashant Nayak, Bill Richard and Ajay Sharda. HiveMind is already in use as a significant part of Vista.
Coding
Coding using HiveMind is designed to be as succinct and painless as possible. Since services are, ultimately, simple objects (POJOs -- plain old java objects) within the same JVM, all the complexity of J2EE falls away ... no more JNDI lookups, no more RemoteExceptions, no more home and remote interfaces. Of course, you can still use HiveMind to front your EJBs, in which case the service is responsible for performing the JNDI lookup and so forth (which in itself has a lot of value), before forwarding the request to the EJB.
In any case, the code should be short. To external objects (objects that are not managed by HiveMind, such as a servlet) the code for accessing a service is quite streamlined:
Registry registry = RegistryBuilder.constructDefaultRegistry(); MyService service = (MyService) registry.getService("com.mypackage.MyService", MyService.class); service.perform(...);
You code is responsible for:
- Obtaining a reference to the Registry singleton
- Knowing the complete id of the service to access
- Passing in the interface class
HiveMind is responsible for:
- Finding the service, creating it as needed
- Checking the type of the service against your code's expections
- Operating in a completely thread-safe manner
- Reporting any errors in a useful, verbose fashion
However, a much more common case is for services to collaborate: that's much simpler, since HiveMind will connect the two services together for you. You'll just need to provide an instance variable and either a setter method or a constructor argument.
Documentation
An important part of the HiveMind picture is documentation. Comprehensive documentation about a HiveMind application, HiveDoc, can be automatically generated by your build process. This documentation lists all modules, all extension points (both service and configuration), all contributions (of service constructors, service interceptors and configuration elements) and cross links all extension points to their contributions.
Modules and extension points include a description which is incorporated into the generated documentation.
HiveMind is used to construct very complex systems using a large number of small parts. HiveDoc is an important tool for developers to understand and debug the application.
Why should you use HiveMind?
The concept behind HiveMind, and most other dependency-injection microkernels, is to reduce the amount of code in your application and at the same time, make your application more testable. If your applications are like my applications, there is an awful lot of code in place that deals just with creating objects and hooking them together, and reading and processing configuration files.
HiveMind moves virtually all of that logic into the framework, driven by the module deployment descriptors. Inside the descriptor, you describe your services, your configuration data, and how everything is hooked together within and between modules.
HiveMind can do all the grunt work for you; using HiveMind makes it so that the easiest approach is also the correct approach.
Task: Log method entry and exit
Typical Approach
public String myMethod(String param) { if (LOG.isDebugEnabled()) LOG.debug("myMethod(" + param + ")"); String result = // . . . if (LOG.isDebugEnabled()) LOG.debug("myMethod() returns " + result); return result; }
This approach doesn't do a good or consistent job when a method has multiple return points. It also creates many more branch points within the code ... basically, a lot of clutter. Finally, it doesn't report on exceptions thrown from within the method.
HiveMind Approach
Let HiveMind add a logging interceptor to your service. It will consistently log method entry and exit, and log any exceptions thrown by the method.
The following descriptor snippet defines a service, provides a core service implementation (using <create-instance>), and adds method logging (using <interceptor>):
<service-point id="MyService" interface="com.myco.MyServiceInterface"> <create-instance class="com.myco.impl.MyServiceImpl"/> <interceptor service-id="hivemind.LoggingInterceptor"/> </service-point>
Task: Reference another service
Typical Approach
private SomeOtherService _otherService; public String myMethod(String param) { if (_otherService == null) _otherService = // Lookup other service . . . _otherService.doSomething(. . .); . . . }
How the other service is looked up is specified to the environment; it might be a JNDI lookup for an EJB. For other microkernels, such as Avalon, there will be calls to a specific API.
In addition, this code is not thread-safe; multiple threads could execute it simultaneously, causing unwanted (and possibly destructive) multiple lookups of the other service.
HiveMind Approach
Let HiveMind assign the other service as a property. This is dependency injection. HiveMind can inject dependencies using JavaBeans properties or constructor arguments.
private SomeOtherService _otherService; public void setOtherService(SomeOtherService otherService) { _otherService = otherService; } public String myMethod(String param) { _otherService.doSomething(. . .); . . . }
HiveMind uses a system of proxies to defer creation of services until actually needed. The proxy object assigned to the otherService property will cause the actual service implementation to be instantiated and configured the first time a service method is invoked ... and all of this is done in a thread-safe manner.
Task: Read configuration data
Typical Approach
Find a properties file or XML file (on the classpath? in the filesystem?) and read it, then write code to intepret the raw data and possibly convert it to Java objects.
The lack of a standard approach means that data-driven solutions are often more trouble than they are worth, leading to code bloat and a loss of flexibility.
Even when XML files are used for configuration, the code that reads the content is often inefficient, incompletely tested, and lacking the kind of error detection built into HiveMind.
HiveMind Approach
private List _configuration; public void setConfiguration(List configuration) { _configuration = configuration; } public void myMethod() { Iterator i = _configuration.iterator(); while (i.hasNext()) { MyConfigItem item = (MyConfigItem)i.next(); item.doStuff(. . .); } }
HiveMind will set the configuration property from a configuration point you specify. The objects in the list are constructed from configuration point contributions and converted, by HiveMind, into objects. As with services, a thread-safe, just-in-time conversion takes place.
The type and number of extension points and how and when your code makes use of them is entirely up to you, configured in the module deployment descriptor.
Task: Test your services
Typical Approach
In complex environments, such as EJB containers, you will often have to deploy your code and then test it from the outside. EJB code in particular is hard to test because collaborating EJBs make use of JNDI to access each other. It is very difficult to "stub out" part of the overall application for testing purposes.
HiveMind Approach
Making code testable is a key concern of HiveMind, and shows up in its general testing strategy:
- Because HiveMind services are simply POJOs, your unit tests can simply instantiate them directly.
- HiveMind services are always identified by interface, so it's easy to provide a mocked-up implementation of the interface.
- Services collaborate via dependency injection, so it's easy for a unit test to wire a service to real or mock implementations of collaborating services.
- Because configuration data is just lists of Java objects, unit tests can easily create objects suitable for testing.