Geronimo Architecture: Part 1

0
63
8 min read

Inversion of Control and dependency injection

Inversion of Control (IoC) is a design pattern used in software engineering that facilitates the creation of loosely-coupled systems. In an IoC system, the flow of control is inverted, that is, the program is called by the framework—unlike in normal linear systems where the program calls the libraries. This allows us to circumvent the tight coupling that arises from the control being with the calling program. Dependency injection is a specific case of IoC where the framework provides an assembler or a configurator that provides the user program with the objects that it needs through injection. The user program declares dependencies on other services (provided by the framework or other user programs), and the assembler injects the dependencies into the user program wherever they are needed.

It is important that you clearly understand the concept of dependency injection before we proceed further into the Geronimo architecture, as that is the core concept behind the functioning of the Geronimo kernel and how services are loosely coupled in it. To help you understand the concept more clearly, we will provide a simple example.

Consider the following two classes:

package packtsamples;
public class RentCalculator{
private float rentRate;
private TaxCalculator tCalc;
public RentCalculator(float rate, float taxRate){
rentRate = rate;
tCalc = new ServiceTaxCalculator(taxRate);
}
public void calculateRent(int noOfDays){
float totalRent = noOfDays * rentRate;
float tax = tCalc.calculateTax(totalRent);
totalRent = totalRent + tax;
System.out.println("Rent is:"+totalRent);
}
}
package packtsamples;
public class ServiceTaxCalculator implements TaxCalculator{
private float taxRate;
public ServiceTaxCalculator(float rate){
taxRate = rate;
}
public float calculateTax(float amount){
return (amount * taxRate/100);
}
}
package packtsamples;
public interface TaxCalculator{
public float calculateTax(float amount);
}
package packtsamples;
public class Main {
/**
* @param args. args[0] = taxRate, args[1] = rentRate, args[2] =
noOfDays
*/
public static void main(String[] args) {
RentCalculator rc = new RentCalculator(Float.parseFloat(args[1]),
Float.parseFloat(args[0]));
rc.calculateRent(Integer.parseInt(args[2]));
}
}

The RentCalculator class calculates the room rent including tax, given the rent rate, and the number of days. The TaxCalculator class calculates the tax on a particular amount, given the tax rate. As you can see from the code snippet given, the RentCalculator class is dependent on the TaxCalculator interface for calculating the tax. In the given sample, the ServiceTaxCalculator class is instantiated inside the RentCalculator class. This makes the two classes tightly coupled, so that we cannot use the RentCalculator with another TaxCalculator implementation. This problem can be solved through dependency injection. If we apply this concept to the previous classes, then the architecture will be slightly different. This is shown in the following code block:>

package packtsamples.di;
public class RentCalculator{
private float rentRate;
private TaxCalculator tCalc;
public RentCalculator(float rate, TaxCalculator tCalc){
rentRate = rate;
this.tCalc = tCalc;
}
public void calculateRent(int noOfDays){
float totalRent = noOfDays * rentRate;
float tax = tCalc.calculateTax(totalRent);
totalRent = totalRent + tax;
System.out.println("Rent is:" +totalRent);
}
}
package packtsamples.di;
public class ServiceTaxCalculator implements TaxCalculator{
private float taxRate;
public ServiceTaxCalculator(float rate){
taxRate = rate;
}
public float calculateTax(float amount){
return (amount * taxRate/100);
}
}
package packtsamples.di;
public interface TaxCalculator{
public float calculateTax(float amount);
}

Notice the difference here from the previous implementation. The RentCalculator class has a TaxCalculator argument in its constructor. The RentCalculator then uses this TaxCalculator instance to calculate tax by calling the calculateTax method. You can pass in any implementation, and its calculateTax method will be called. In the following section, we will see how to write the class that will assemble this sample into a working program.

package packtsamples.di;
import java.lang.reflect.InvocationTargetException;
public class Assembler {
private TaxCalculator createTaxCalculator(String
className, float taxRate){
TaxCalculator tc = null;
try {
Class cls = Class.forName(className);
tc = (TaxCalculator)cls.getConstructors()[0] .newInstance(taxRate);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return tc;
}
private RentCalculator createRentCalculator(float rate,
TaxCalculator tCalc){
return new RentCalculator(rate,tCalc);
}
private void assembleAndExecute(String className,
float taxRate, float rentRate, int noOfDays){
TaxCalculator tc = createTaxCalculator(className, taxRate);
createRentCalculator(rentRate, tc).calculateRent(noOfDays);}
/**
*
* @param args args[0] = className, args[1] = taxRate args[2] =
rentRate args[3] = noOfDays
*/
public static void main(String[] args){
new Assembler().assembleAndExecute(args[0],
Float.parseFloat(args[1]),
Float.parseFloat(args[2]),
Integer.parseInt(args[3]));
}
}

In the given sample code, you can see that there is a new class called the Assembler. The Assembler, in its main method, invokes the implementation class of TaxCalculator that we want RentCalculator to use. The Assembler then instantiates an instance of RentCalculator, injects the TaxCalculator instance of the type we specify into it, and calls the calculateRent method. Thus the two classes are not tightly coupled and the program control lies with the assembler, unlike in the previous case. Thus there is Inversion of Control happening here, as the framework (Assembler in this case) is controlling the execution of the program. This is a very trivial sample. We can write an assembler class that is more generic and is not even coupled to the interface as in the previous case. This is an example of dependency injection. An injection of this type is called constructor injection, where the assembler injects values through the constructor. You can also have other types of dependency injection, namely setter injection and field injection. In the former, the values are injected into the object by invoking the setter methods that are provided by the class, and in the latter, the values are injected into fields through reflection or some other method. The Apache Geronimo kernel uses both setter injection and constructor injection for resolving dependencies between the different modules or configurations that are deployed in it.

The code for these examples is provided under di-sample in the samples. To build the sample, use the following command:

mvn clean install

To run the sample without dependency injection, use the following command:

java –cp di-sample-1.0.jar packtsamples.Main <taxRate> <rentRate>
<noOfDays>

To run the sample with dependency injection, use the following command:

java –cp di-sample-1.0.jar packtsamples.Assembler packtsamples.
di.ServiceTaxCalculator <taxRate> <rentRate> <noOfDays>

GBeans

A GBean is the basic unit in Apache Geronimo. It is a wrapper that is used to wrap or implement different services that are deployed in the kernel. GBeans are similar to MBeans from JMX. A GBean has attributes that store its state and references to other GBeans, and can also register dependencies on other GBeans. GBeans also have lifecycle callback methods and metadata. The Geronimo architects decided to invent the concept of GBeans instead of using MBeans in order to keep the Geronimo architecture independent from JMX. This ensured that they did not need to push in all of the functionality required for the IoC container (that forms Geronimo kernel) into the JMX implementation. Even though GBeans are built on top of MBeans, they can be moved to some other framework as well. A user who is writing a GBean has to follow certain conventions. A sample GBean is shown below:

import org.apache.geronimo.gbean.GBeanInfo;
import org.apache.geronimo.gbean.GBeanInfoBuilder;
import org.apache.geronimo.gbean.GBeanLifecycle;
public class TestGBean implements GBeanLifecycle{
private String name;
public TestGBean(String name){
this.name = name;
}
public void doFail() {
System.out.println("Failed.............");
}
public void doStart() throws Exception {
System.out.println("Started............"+name);
}
public void doStop() throws Exception {
System.out.println("Stopped............"+name);
}
public static final GBeanInfo GBEAN_INFO;
static {
GBeanInfoBuilder infoBuilder = GBeanInfoBuilder
.createStatic(TestGBean
.class, "TestGBean");
infoBuilder.setPriority(2);
infoBuilder.addAttribute("name", String.class, true);
infoBuilder.setConstructor(new String[]{"name"});
GBEAN_INFO = infoBuilder.getGBeanInfo();
}
public static GBeanInfo getGBeanInfo() {
return GBEAN_INFO;
}
}

You will notice certain characteristics that this GBean has from the previous section. We will list these characteristics as follows:

  • All GBeans should have a static getGBeanInfo method, which returns aGBeanInfo object that describes the attributes and references of GBean as well as the interfaces it can implement.
  • All GBeans will have a static block where a GBeanInfoBuilder object is created and linked to that GBean. All of the metadata that is associated with this GBean is then added to the GBeanInfoBuilder object. The metadata includes descriptions of the attributes, references, interfaces, and constructors of GBean.

We can add GBeans to configurations either programmatically, using methods exposed through the configuration manager and kernel, or by making an entry in the plan for the GBean, as follows:

<gbean name="TestGBean" class="TestGBean">
<attribute name="name">Nitya</attribute>
</gbean>

We need to specify the attribute values in the plan, and the kernel will inject those values into the GBean at runtime. There are three attributes for which we need not specify values. These are called the magic attributes, and the kernel will automatically inject these values when the GBeans are being started. These attributes are abstractName, kernel, and classLoader. As there is no way to specify the values of these attributes in the deployment plan (an XML file in which we provide Geronimo specific information while deploying a configuration), we need not specify them there. However, we should declare these attributes in the GBeanInfo and in the constructor. If the abstractName attribute is declared, then the Geronimo kernel will inject the abstractName of the GBean into it. If it is the kernel attribute, then a reference to the kernel that loaded this GBean is injected. If we declare classLoader, then the class loader for that configuration is injected.

LEAVE A REPLY

Please enter your comment!
Please enter your name here