Spring: Auto Discovery Driving Programmatic Bean Registration

Spring, does the job, keeps doing the job.
Copyright: Dr Alexander J Turner All Rights Reserved
As I discussed before, I have been porting Sonic Field to Spring. The first cut works now; in the process I ended up implementing an interesting programmatic bean registration approach.

OK - a little background. Sonic Field is a audio processing and synthesis system written in Java. It uses a domain specific language called SFPL (Sonic Field Patch layout) to define the way it processes sound. This language uses processors. These are Java objects which take one or more inputs and convert them to one or more outputs (usually 1). A processor might, for example, take in audio at one volume and change it to another.

Originally, Sonic Field was very static in nature. This is a common attribute of language implementations. A language starts off with a parser which is fed some source code. All this seems quite static and the static nature of the system spreads from there. Moving to spring means getting rid of all that static stuff and moving over to beans. This was surprisingly easy. Eclipse has such good re-factoring tools theses days that major changes can be embarked upon with little trepidation. 

The inherent structure of Sonic Field can now be seen from its XML configuration:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xmlns:context="http://www.springframework.org/schema/context" 
 xsi:schemaLocation="http://www.springframework.org/schema/beans
 http://www.springframework.org/schema/beans/spring-beans.xsd
 http://www.springframework.org/schema/context
 http://www.springframework.org/schema/context/spring-context-3.0.xsd"
 >
 
 <context:component-scan base-package="com.nerdscentral.audio" />
 <context:component-scan base-package="com.nerdscentral.sfpl.beans" />


    <!--  Beans which are the core of the SFPL system
          ===========================================
    -->
 <bean id="MainLogger"      class="com.nerdscentral.sfpl.Logger" >
  <constructor-arg ref="SimpleOutLogger" />
  <constructor-arg ref="SimpleErrLogger" />
 </bean>
 <bean id="SimpleOutLogger" class="com.nerdscentral.sfpl.ProgramOutLogger" />
 <bean id="SimpleErrLogger" class="com.nerdscentral.sfpl.ProgramErrorLogger" />
 <bean id="ThreadFactory" class="com.nerdscentral.sfpl.SFPL_ThreadFactory" >
  <constructor-arg ref="MainLogger" />
 </bean>  
 <bean id="SimpleProfiler" class="com.nerdscentral.sfpl.tooling.SimpleProfiler" />
 <bean id="SFPLParser" class="com.nerdscentral.sfpl.SFPL_Parser">
  <constructor-arg ref="SimpleProfiler" />
 </bean>
 <bean id="SimpleRenderer" name="SimpleRenderer"
  class="com.nerdscentral.sfpl.RenderRunnerImp">
  <constructor-arg ref="MainLogger" />
  <constructor-arg ref="ThreadFactory" />
  <constructor-arg ref="SimpleProfiler" />
  <constructor-arg ref="SFPLParser" />
 </bean>
 <bean id="Parallelizer" class="com.nerdscentral.sfpl.tasks.Parallelizer">
  <constructor-arg ref="ThreadFactory" />
  <constructor-arg ref="SimpleRenderer" /> 
  <constructor-arg ref="SimpleProfiler" /> 
 </bean>
 
    <!--  Beans which are operands to be used by SFPL
          ===========================================
    -->
 <!-- Operands using simple parallel -->
    <bean id="SF_Clip" class="com.nerdscentral.sfpl.tasks.SimpleParallel">
    <constructor-arg ref="SFP_Clip"/>
    <constructor-arg ref="Parallelizer"/>
    </bean> 
    <bean id="SF_DirectMix" class="com.nerdscentral.sfpl.tasks.SimpleParallel">
    <constructor-arg ref="SFP_DirectMix"/>
    <constructor-arg ref="Parallelizer"/>
    </bean> 
    <bean id="SF_Invert" class="com.nerdscentral.sfpl.tasks.SimpleParallel">
    <constructor-arg ref="SFP_Invert"/>
    <constructor-arg ref="Parallelizer"/>
    </bean> 
    <bean id="SF_Power" class="com.nerdscentral.sfpl.tasks.SimpleParallel">
    <constructor-arg ref="SFP_Power"/>
    <constructor-arg ref="Parallelizer"/>
    </bean> 
    <bean id="SF_Rectify" class="com.nerdscentral.sfpl.tasks.SimpleParallel">
    <constructor-arg ref="SFP_Rectify"/>
    <constructor-arg ref="Parallelizer"/>
    </bean> 
    <bean id="SF_Volume" class="com.nerdscentral.sfpl.tasks.SimpleParallel">
    <constructor-arg ref="SFP_Volume"/>
    <constructor-arg ref="Parallelizer"/>
    </bean> 
    <bean id="SF_NumericVolume" class="com.nerdscentral.sfpl.tasks.SimpleParallel">
    <constructor-arg ref="SFP_NumericVolume"/>
    <constructor-arg ref="Parallelizer"/>
    </bean> 
    <bean id="SF_Quench" class="com.nerdscentral.sfpl.tasks.SimpleParallel">
    <constructor-arg ref="SFP_Quench"/>
    <constructor-arg ref="Parallelizer"/>
    </bean> 

</beans>

It all rotates around SimpleRenderer which is the bean equivalent of the old static parser/runner system. There are a few support beans for thread management and logging. At the bottom of the definition are some processors which are wrapped in  utility beans created from SimpleParallel.

From this and my discussion before, the mapping from a processor to a Spring bean should be obvious. All the processors are beans. However, I have not declared them all in the XML. Only those which are complex to construct are declared in the XML. The majority are simple auto discovered and autowired.

Here is an example of an auto-discovered bean:
package com.nerdscentral.audio.utilities;

import org.springframework.stereotype.Component;

import com.nerdscentral.sfpl.Caster;
import com.nerdscentral.sfpl.SFPL_Context;
import com.nerdscentral.sfpl.SFPL_Operator;
import com.nerdscentral.sfpl.SFPL_RuntimeException;

@Component
public class SF_Frequency implements SFPL_Operator
{

    private static final long serialVersionUID = 1L;

    @Override
    public String Word()
    {
        return Messages.getString("SF_Frequency.0"); //$NON-NLS-1$
    }

    @Override
    public Object Interpret(Object input, SFPL_Context context) throws SFPL_RuntimeException
    {
        return 1000.0 / Caster.makeDouble(input);
    }
}

This one is so simple that it does not even require an Autowired attribute. The default constructor is called. The next one as a non default constructor so it is Autowired:

@Component
public class SF_Monitor implements SFPL_Operator
{

    private final SF_Normalise normaliser;

    @Autowired
    protected SF_Monitor(SF_Normalise normiliserIn)
    {
        normaliser = normiliserIn;
    }
...
Eclipse showing a bean defined by having the @Component annotation

Auto-loading Beans Into The Parser
So far we have a bean based system where all processors are beans. It would be tiresome indeed to have to define the beans and then write code to load them into the parser. This was similar to the original model - and that was tiresome. Spring makes it all much simpler!

RenderRunnerImp implements ApplicationContextAware and uses it to store the application context. Then, when the render is performed, it uses the application context to automatically load all beans with names starting SF_ into the parser:

Map<String, SFPL_Operator> beans = applicationContext.getBeansOfType(SFPL_Operator.class);
        for (String name : beans.keySet())
        {
            if (name.startsWith("SF_")) //$NON-NLS-1$
            {
                this.logger.Log(Messages.getString("RenderRunnerImp.0") + name + Messages.getString("RenderRunnerImp.1")); //$NON-NLS-1$ //$NON-NLS-2$
                this.parser.AddKeyWord(beans.get(name));
            }
        }

Now that is really simple! To make a new processor all that is required it to create a new class which:

  1. Implements SF_Operator
  2. If annotated @Component
  3. Has a name starting SF_

The framework does every little thing else for you.

The Eclipse Spring integration is very tight. Here we can see Eclipse listing all the auto discovered beans.
It also shows all the XML defined beans and the autowired beans. Types are all cross checked - amazing!

Last Step - Programatically Generated Beans
Sonic Field as two sets of programmatically generated processors. These are for volume changes. Rather than having to pass a signal and volume setting into the Volume processor, there are 201 each of  dbs and pcnt processors. For example pcnt+50 will reduce volume to 50% and dbs+12 will increase volume 12 dbs (approximately 4 times). Placing all 402 definitions into the XML would be laborious and make the implementation brittle. The solution is to programmatically inject the bean definitions using  BeanDefinitionRegistryPostProcessor.

package com.nerdscentral.audio.utilities;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.stereotype.Component;

import com.nerdscentral.audio.SFConstants;
import com.nerdscentral.audio.SFData;
import com.nerdscentral.sfpl.Caster;
import com.nerdscentral.sfpl.SFPL_Context;
import com.nerdscentral.sfpl.SFPL_Operator;
import com.nerdscentral.sfpl.SFPL_RuntimeException;

@Component
public class SFP_DBs implements SFPL_Operator, BeanDefinitionRegistryPostProcessor
{

    private static final long serialVersionUID = 1L;

    private final double      volume;
    private final String      name;
    private final boolean     initialisedLoadProcess;

    public SFP_DBs()
    {
        volume = SFConstants.fromDBs(0);
        name = null; // should never be used - here to load other beans
        initialisedLoadProcess = false;
    }

    public SFP_DBs(int dbs)
    {
        volume = SFConstants.fromDBs(dbs);
        name = dbs > 0 ? ("DBs+" + dbs) : "DBs" + dbs; //$NON-NLS-1$ //$NON-NLS-2$
        initialisedLoadProcess = true;
    }

    @Override
    public String Word()
    {
        return name;
    }

    @Override
    public Object Interpret(Object input, SFPL_Context context) throws SFPL_RuntimeException
    {
        SFData in = Caster.makeSFData(input);
        int length = in.getLength();
        SFData out = SFData.build(length);
        for (int index = 0; index < length; ++index)
        {
            out.setSample(index, in.getSample(index) * volume);
        }
        return out;
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory arg0) throws BeansException
    {
        // Not Used
    }

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException
    {
        if (initialisedLoadProcess) return;
        for (int v = -100; v < 101; ++v)
        {
            ConstructorArgumentValues cas = new ConstructorArgumentValues();
            cas.addGenericArgumentValue(v);
            BeanDefinition def = new RootBeanDefinition(SFP_DBs.class, cas, null);
            registry.registerBeanDefinition("SF_DBs" + v, def); //$NON-NLS-1$
        }

    }
}

Spring will automatically create a SFP_DBs bean using the default constructor. The RenderRunnerImpl bean will not load it into the parser because its name starts SFP_. Note that the default constructor sets initialisedLoadProcess to false. This means that when Spring sets up the bean the code in postProcessBeanDefinitionRegistry will actually run rather than return straight away. It is this code which then uses the other constructor for SFP_DBs to create the required 201 bean instances all with names starting SF_. Because the non default constructor is called, initialisedLoadProcess is true and so the creation of new beans does not occur in later postProcessBeanDefinitionRegistry calls.

Thoughts And Conclusions:
This was so much simpler than I thought it might be. Spring 3 asks very little of an application. So much can be done by convention and automated that it is a pleasure to port even quite complex code like Sonic Field into Spring.

The other big take home message is how Spring smooths the transition from a 'nice little application' into something much bigger. Moving Sonic Field from a stand alone, static app to something which could be web aware or database enabled (next projects) and run on a server would be a huge task if done by hand. By floating it in the Spring framework, that is now dead easy. It is tempting to suggest that any Java system which is likely to be non trivial should be created from the start as Spring or JEE.