Skip to content
This repository has been archived by the owner on Apr 14, 2022. It is now read-only.

Functional Bean Configuration

Henryk Konsek edited this page Jan 31, 2014 · 10 revisions

#Functional Bean Configuration

In addition to defining beans in XML, Spring Scala offers an alternative that uses Scala classes instead of XML files to configure your Spring beans. This approach is similar to using @Configuration in standard Spring, except that it is based on functions rather than annotations.

Defining beans using FunctionalConfiguration

To create a functional Spring configuration, you simply have to mix in the FunctionalConfiguration trait into your configuration class. Beans are defined by calling the bean method on the trait and passing on a function that creates the bean.

Assume we have the following Person class:

class Person(val firstName: String, val lastName: String) {
    var father: Person = _
    var mother: Person = _
}

In its simplest form, a functional configuration will look something like the following:

class PersonConfiguration extends FunctionalConfiguration {
    bean() {
        new Person("John", "Doe")
    }
}

This configuration will register a singleton Person bean under a auto-generated name. The preceding configuration is exactly equivalent to the following Spring XML:

<beans>
    <bean class="Person">
        <constructor-arg value="John"/>
        <constructor-arg value="Doe"/>
    </bean>
</beans>

Of course, you can also register a bean under a specific name, provide aliases, or set the scope:

class PersonConfiguration extends FunctionalConfiguration {
    bean("john", aliases = Seq("doe"), scope = BeanDefinition.SCOPE_PROTOTYPE) {
        new Person("John", "Doe")
    }
}

This configuration will register a Person under the names "john" and "doe", and also sets the scope to prototype.

FunctionalConfigApplicationContext

In much the same way that Spring XML files are used as input when instantiating a ClassPathXmlApplicationContext and @Configuration classes are used for the AnnotationConfigApplicationContext, a FunctionalConfiguration can be used as input of FunctionalConfigApplicationContext companion object:

object PersonConfigurationDriver extends App {
    val applicationContext = FunctionalConfigApplicationContext[PersonConfiguration]
    val john = applicationContext.getBean(classOf[Person])
    println(john.firstName)
}

Convenience bean registration methods

As an alternative to using the bean method to register a prototype, you can also use the convenience method prototype. Like so:

class PersonConfiguration extends FunctionalConfiguration {
    prototype("john") {
        new Person("John", "Doe")
    }
}

In addition to prototype, you can use the singleton method to register a singleton.

Bean references

Even though we have not used it so far, the bean() method actually has a return value which can be captured in a variable. The return type of bean() is a BeanLookupFunction[T], where T is the class of the bean defined. A BeanLookupFunction is essentially a Scala function that takes no parameters, and looks up the defined bean when invoked. You can use the return value to refer to other beans in the functional configuration, like so:

class PersonConfiguration extends FunctionalConfiguration {
    val jack = bean() {
        new Person("Jack", "Doe")
    }

    val jane = bean() {
        new Person("Jane", "Doe")
    }

    val john = bean() {
        val john = new Person("John", "Doe")
        john.father = jack()
        john.mother = jane()
        john
    }
}

In the example above, we define three beans ("jack", "jane", and "john"), and capture these in three vals. In the bean function where "john" is defined, we set his parents by referring to the "jack" and "jane" vals. Because these vals are BeanLookupFunctions, we need to invoke the function (hence the brackets) to do a bean lookup and get the Person instances.

The reason bean() returns a function () => T as opposed to just T has to do with bean scoping. If bean() would return T, and we would capture that in a variable, that variable would hold the same bean reference each time it is used, even when the bean would have non-singleton scope. By returning a function, we make sure that a bean lookup is performed every time a bean is referenced, and instantiate a new instance if required. Note that the singleton() convenience method does return T, because it can refer to the same bean instance each time it is referenced.

Configuration composition

Because functional configurations are just Scala classes, you can use inheritance, abstract classes, etc. to compose a configuration out of multiple classes:

abstract class PersonConfiguration extends FunctionalConfiguration {
    val firstName: String
    val lastName: String
    bean() {
        new Person(firstName, lastName)
    }
}

class JohnDoeConfiguration extends PersonConfiguration {
    val firstName = singleton() {
        "John"
    }
    val lastName = singleton() {
        "Doe"
    }
}

You can also use traits, but be careful to mark any BeanLookupFunction fields as lazy. Otherwise, they will not be properly initialized, and you will get an exception:

trait FirstNameConfig extends FunctionalConfiguration {
    lazy val firstName = bean() {
        "John"
    }
}

trait LastNameConfig extends FunctionalConfiguration {
    lazy val lastName = bean() {
        "Doe"
    }
}

class JohnDoeConfiguration extends FirstNameConfig with LastNameConfig {
    val john = bean() {
        new Person(firstName(), lastName())
    }
}

Importing XML and @Configuration classes

Not all of the Spring beans have to be defined in a FunctionalConfiguration. You can import Spring XML and @Configuration through the importXml and importClass methods respectively, and you can refer to the beans defined in these other contexts through the getBean() method defined on FunctionalConfiguration.

<beans>
    <bean id="firstName" class="java.lang.String">
        <constructor-arg value="John"/>
    </bean>
    <bean id="lastName" class="java.lang.String">
        <constructor-arg value="Doe"/>
    </bean>
</beans>
class PersonConfiguration extends FunctionalConfiguration {
    importXml("classpath:/names.xml")

    val john = bean() {
        new Person(getBean("firstName"), getBean("lastName"))
    }
}

In the previous example, we imported the Spring XML file shown to give us the firstName and lastName bean definitions. We can do something similar for @Configuration classes:

@Configuration
public class NameConfiguration {
    @Bean
    public String firstName() {
        return "John";
    }
    @Bean
    public String lastName() {
        return "Doe";
    }
}
class PersonConfiguration extends FunctionalConfiguration {
    importClass(classOf[NameConfiguration])

    val john = bean() {
        new Person(getBean("firstName"), getBean("lastName"))
    }
}

Registering init and destroy functions

Similar to how you register init and destroy methods in Spring XML, you can register init and destroy functions in a FunctionalConfiguration. Both the init and destroy functions are defined as T => Unit, where T stands for the type of the bean registered. Essentially, this means that the registered bean is passed on as a parameter to the init and destroy function.

For example, this is how you would typically register a Commons DBCP datasource in a functional configuration:

class DataSourceConfiguration extends FunctionalConfiguration {
    val dataSource = bean("dataSource") {
        val dataSource = new BasicDataSource()
        dataSource.setDriverClassName("com.mysql.jdbc.Driver")
        dataSource.setUrl("jdbc:mysql://localhost/mydb")
        dataSource.setUsername("foo")
        dataSource.setPassword("bar")
        dataSource
    } destroy {
        _.close()
    }
}

In the above example, we define a dataSource bean and set it up with all required properties. We could have done this initialization in the init function instead. In the destroy function, we call the close() method on the passed on BasicDataSource, thus making sure that the connection is closed when the application context is shut down, so that there are no connection leaks.

Bean Profiles

You can wrap any bean definition in a profile block, thus making sure that those beans are registered only when that profile is active:

class DataSourceConfiguration extends FunctionalConfiguration {
    profile("dev") {
        bean("dataSource") {
            val dataSource = new BasicDataSource()
            // Set up properties
            dataSource
        } destroy {
            _.close()
        }
    }
    profile("prod") {
        bean("dataSource") {
            val dataSource = new OracleDataSource()
            // Set up properties
            dataSource
        }
    }
}

In the above example, the BasicDataSource will be registered when the dev profile is active; the OracleDataSource is registered when prod is active.