Configuring Java Beans using BeanShell

Inversion of control and dependency injection frameworks have been in use for some years as a way of configuring and managing  beans  in Java applications. Frameworks such as Spring, Pico, Hivemind and Guice generally work well enough but often rely on cumbersome configuration or opaque annotations. An alternate way of  managing beans, simply and transparently, is by  using a dynamic scripting language. A dynamic scripting language that can run within an interpreter in the JVM can be used  as the basis for a bean factory which can perform dependency injection and control runtime configuration. In this example we use BeanShell but the same principle can be extended to other scripting languages such as Jython, JRuby and Groovy.

Objectives

  • Clean bean configuration for both “Singleton” and “Prototype” beans
  • Bean dependency injection
  • Simple way of expressing differences in configuration between environments (e.g. dev, qa, prod)

Why Bean Shell?

BeanShell is a dyanmic language scripting that runs within an interpreter in the JVM. It uses a syntax familiar to Java programmers. I selected it because of it’s simplicity and familiarity. By the looks of the Bean Shell mailing list and source code repository  it seems that activity on the project has dwindled. However, there is a branch of the code being developed here. Regardless of activity, BeanShell just works and is very easy to learn and integrate into a project. The only requirement is the bsh jar file. Once downloaded it’s worth starting up the BeanShell Console and running some example code. The docs are comprehensive enough. In writing our Bean factory we will make use of the following BeanShell features:

  1. Calling the BeanShell interpreter from Java - docs
  2. Method Closures and scripted objects - docs

Defining Beans - beans.bsh

Beans and their configuration are defined within a BeanShell configuration file which we name beans.bsh. This file will be loaded from the classpath into an in-process BeanShell interpreter. It will contain all the necessary information for constructing each bean i.e.  singleton or prototype, configuration parameters, other bean dependencies. The first bean we define is a JDBC DataSource, as a singleton.

// File beans.bsh

// JDBC DataSource - Singleton Bean
dataSourceDef () {

    // JDBC Datasource,  singleton.
    ds = new com.mysql.jdbc.jdbc2.optional.MysqlConnectionPoolDataSource();
    ds.databaseName = "test";
    ds.port = 3306;
    ds.serverName = "localhost";
    ds.user = "test";
    ds.password = "test";

    create() { return ds; }

    return this;
}
dataSource = dataSourceDef();

The bean definition should be self-explanatory, we are defining our data source to be an instance of MysqlConnectionPoolDataSource and are providing the required configuration settings. This configuration is contained within a BeanShell closure. We have defined a method called dataSourceDef() that returns an instance of itself. This instance holds a reference to a datasource object. The create() method will be  used to return the datasource instance. The reason for using the closure is not yet obvious since we could just as well have more simply defined the bean as follows, removing the closure altogether:

dataSource = new com.mysql.jdbc.jdbc2.optional.MysqlConnectionPoolDataSource();
dataSource.databaseName = "test";
dataSource.port = 3306;
dataSource.serverName = "localhost";
dataSource.user = "test";
dataSource.password = "test";

The reason for using the level of indirection allowed by the  closure and the create() method will be demonstrated when we create our first prototype bean. Our bean definition is now complete, we can proceed to write the code that will allow us to access it.

Loading Beans - BeanFactory.java

To load beans  defined in beans.bsh we need to create a Bean Factory that can start a BeanShell interpreter, source beans.bsh  and use it to construct the appropriate Java objects.  BeanFactory.java does this for us:

package demo.bsh.beans;
import org.apache.log4j.Logger;

import bsh.EvalError;
import bsh.Interpreter;

/**
* A singleton class used to create beans defined in a
* bean shell configuration file.
*/
public class BeanFactory {

  private static BeanFactory instance = getInstance();
  private Interpreter i;

  public static synchronized BeanFactory getInstance() {
    if (instance == null) {
        instance = new BeanFactory();
        instance.init();
    }

    return instance;
  }

  private BeanFactory() {}

  private void init() {
    try {
    // global variables - substituted into config
    boolean dev = Boolean.parseBoolean(System.getProperty("dev", "true"));

    // Construct an interpreter
    i = new Interpreter();
    i.set("dev", dev);

    // Source the configuration script file
    i.source("config/beans.bsh");

    } catch (Exception e) {
       throw new RuntimeException(e);
    }
  }

public synchronized Object getBean(String name) {
    try {
        return i.eval(name+".create()");

    } catch (EvalError e) {
        throw new RuntimeException(e);
    }
 }
}

The job of the singleton class BeanFactory is to 1) construct a BeanShell Interpreter and 2)  use the interpreter to look up beans, keyed by name.

Creating the bean shell interpreter

The following four lines of code in the BeanFactory.init() method take care of creating the bean shell interpreter and seeding it with our config file, beans.bsh. It is important that they are executed from the init() method after constructor intialisation to avoid circular dependencies - beans that depend on the BeanFactory - and the creation of multiple BeanFactory instances. By initialising our Beans within the init() method we sidestep this issue.

// a variable the we use to determine runtime mode
// e.g. Prod, QA, Dev. Defaults to Dev.
boolean dev = Boolean.parseBoolean(System.getProperty("dev", "true"));

// Construct an interpreter
i = new Interpreter();
// Set the runtime mode on the interpreter
// this variable can now be used in our bean shell configuration
i.set("dev", dev);

// Source the configuration script file
i.source("config/beans.bsh");

Instantiating the bean shell environment is a simple matter of creating an instance of bsh.Interpreter and  sourcing our bean configuration file. We take the additional step of seeding the interpreter with a boolean variable, dev, which we will later be able to use within beans.bsh to construct conditional configuration statements of the type:

if (dev) {
 // use bean  X
} else {
 // use bean Y
}

Loading Beans

	public synchronized Object getBean(String name) {
	    try {
		return i.eval(name+".create()");

            } catch (EvalError e) {
		throw new RuntimeException(e);
	    }
	}

To create a bean we use a simple method that evaluates the statement <beanname>.create() on the intepreter. We will need to cast the bean object when we call this method. The method is synchronized to ensure that singleton beans are only intialised once  (we could quite easily remove the synchronisation on this method, for a slight performance boost, by simply pre loading all the singleton beans before we allow the user to use them).

Putting it all together

To instantiate our datasource bean using the BeanFactory class requires the following 2 lines of code:

BeanFactory config = BeanFactory.getInstance();
DataSource ds = (DataSource) config.getBean("dataSource");

Our DataSource bean was an example of a simple bean with no dependencies. We can now use our BeanFactory to construct a more complicated bean.

Bean Injection

We can now use our BeanFactory and beans.bsh configuration to construct a bean that has a dependency on another bean, using dependency injection. Our new bean is a PersonDAO. It’s dependency is the DataSource bean we created previously.

The PersonDao class:

package demo.bsh.beans;

import java.util.Date;

public class Person {

    private String name;
    private Date creationTime;
    
    private PersonDao dao;

    public void save() {
        dao.add(name);
    }
   
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setDao(PersonDao dao) {
        this.dao = dao;
    }
    public PersonDao getDao() {
        return dao;
    }

    public Date getCreationTime() {
        return creationTime;
    }

    public void setCreationTime(Date creationTime) {
        this.creationTime = creationTime;
    }
}
 

Note that this bean does not implement an interface, it is not a requirement that they do so and so for brevity we don’t bother.

Next the bean configuration in beans.bsh

// bean.bsh configuration for PersonDao
// DAO - Singleton Bean
personDaoDef () {

    // Singleton DAO Bean.
    pdao = new demo.bsh.beans.PersonDaoImpl();
    pdao.dataSource = dataSource.create();

    create() { return pdao;    }

    return this;
}
personDao = personDaoDef();

It should be clear that the configuration of PersonDao and DataSource are near identical, with the exception that the PersonDao bean satisfies it’s dependency on DataSource with the following simple line of configuration.

pdao.dataSource = dataSource.create();

That’s all that’s required to wire up and inject our personDao with a datasource bean.

We can acquire an instance of a PersonDao as follows:

BeanFactory config = BeanFactory.getInstance();
PersonDao pdao = (PersonDao) config.getBean("dataSource");
pdao.add("Fred");

However instead we will once again use dependency injection and our bean configuration to complete the example by wiring up a Person object that uses PersonDao.

Putting it all Together

Our final example is to create a Person object. Person beans have the following characteristics:

  1. They are prototype beans (i.e. not singletons)
  2. Each person has a name
  3. Once a person has been created it can be saved - and so has a dependency on PersonDao - and implicitly DataSource

The Person class:

package demo.bsh.beans;

import java.util.Date;

public class Person {

	private String name;
	private Date creationTime;

	private PersonDao dao;

	public void save() {
		dao.add(name);
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public void setDao(PersonDao dao) {
		this.dao = dao;
	}
	public PersonDao getDao() {
		return dao;
	}

	public Date getCreationTime() {
		return creationTime;
	}

	public void setCreationTime(Date creationTime) {
		this.creationTime = creationTime;
	}
}

The Person bean configuration:

// Person Model Object - Prototype Bean (1 per request)
personDef () {
    create() {
        p = new demo.bsh.beans.Person();
        p.dao = personDao.create();
        p.creationTime = new Date(); // set creation time to NOW
        return p;   
    }
    return this;
}
person = personDef();

Since this all happens within the create() method a new instance of Person will be created each time one is requested. That’s all the configuration it takes to wire up the Person bean. We have specified in our configuration for Person that we need to inject a PersonDao, and additionally we want to set the creation time to NOW. It is for this reason that we use method closures and the create() method to access our beans;  it allows for a level of  indirection necessary to make this choice between Singleton & Prototype in the beans.bsh  file.

Using the Person bean is simple:

BeanFactory config = BeanFactory.getInstance();

Person foo = (Person) config.getBean("person");
foo.setName("Foo");
foo.save();

Thread.sleep(2000);

Person bar = (Person) config.getBean("person");
bar.setName("Bar");
bar.save();

 // t
log.info(foo.getName() + " was created at " + foo.getCreationTime());
// t + 2s
log.info(bar.getName() + " was created at " + bar.getCreationTime());

Environmental Configuration

Our final requirement is to  configure beans dependent on the environment in which they are being used e.g. DEV, QA, PROD.

To do so we seed our bean shell interpreter with a variable that we can use, in beans.bsh, to distinguish between dev, and all other environments:

boolean dev = Boolean.parseBoolean(System.getProperty("dev", "true"));

// Construct an interpreter
i = new Interpreter();
// seed the environment
i.set("dev", dev);

We could easily extend this principle to have variables called QA, PROD or even hosts names.

Once the interpreter has been seeded we can re-write our bean shell configuration for DataSource:

// beans.bsh

// JDBC DataSource - Singleton Bean
dataSourceDef () {

  // JDBC Datasource,  singleton.
  ds = new  com.mysql.jdbc.jdbc2.optional.MysqlConnectionPoolDataSource();
  ds.port = 3306;
  ds.serverName = "localhost";
  if (dev) {
     ds.databaseName = "test";
     ds.user = "test";
     ds.password = "test";
  } else {
     ds.databaseName = "proddb";
     ds.user = "produser";
     ds.password = "prodpasswd";
  }

  create() { return ds; }
  return this;
}
dataSource = dataSourceDef();

Environmental configuration can now be consolidated within the bean configuration, simply and easily.  This makes it more transparent and easier to manage than some alternative solutions.

Share and Enjoy:
  • Print this article!
  • Digg
  • Sphinn
  • del.icio.us
  • Facebook
  • Mixx
  • Google Bookmarks

Tags: , , , ,

Leave a Reply

CAPTCHA Image Audio Version
Reload Image