One really neat feature about Spring that you don't hear very much about is the ability to inject scripting into a Spring bean. This is very powerful functionality that allows you to add dynamic logic into your application and even has the ability to reload the script at a set interval enabling you to make code changes on the fly without rebuilding. There are currently three scripting languages that you can use with this feature and they are Groove, Ruby, and BeanShell. This post will focus on injecting a BeanShell script into a Spring bean.
We are going to take a look at a situation in which we are going to implement various rules for calculating invoice amounts for ACME Consulting Company's clients. Some of ACME's clients pay a set monthly fee that entitles them to a given number of hours of service, then they pay an hourly rate for any hours over the set amount. They also have some clients that only pay an hourly fee for service and don't have a monthly contract setup.
For this situation, we are going to setup two invoice calculation rules, one for calculating invoice amounts for our monthly contracts, and one that only calculates invoices for our hourly contracts.
Before we take a look at the code, you'll need to add the BeanShell library to your class path which can be downloaded from
www.beanshell.org.
First, we'll create a Java class called InvoiceData that will hold all of the information our rules will need to calculate the invoice amounts:
package com.billing.invoice;
import java.math.BigDecimal;
public class InvoiceData {
private int hoursWorked;
private int hoursEntitled;
private BigDecimal monthlyRate;
private BigDecimal hourlyRate;
public void setHoursWorked( int hours ) {
hoursWorked = hours;
}
public void setHoursEntitled( int hours ) {
hoursEntitled = hours;
}
public void setMonthlyRate( BigDecimal rate ) {
monthlyRate = rate;
}
public void setHourlyRate( BigDecimal rate ) {
hourlyRate = rate;
}
public int getHoursWorked() {
return hoursWorked;
}
public int getHoursEntitled() {
return hoursEntitled;
}
public BigDecimal getMonthlyRate() {
return monthlyRate;
}
public BigDecimal getHourlyRate() {
return hourlyRate;
}
}
This is your basic plain old java object (POJO) that will hold the properties we will need to calculate the total invoice amount.
Next, we'll create a Java interface called InvoiceCalculationRule:
package com.billing.invoice.rules;
import java.math.BigDecimal;
import com.billing.invoice.InvoiceData;
public interface InvoiceCalculationRule {
public BigDecimal calculate( InvoiceData invoiceData );
}
This interface contains only one method named calculate, that accepts an invoiceData object. This interface will be used to perform our invoice calculations. The actual implementations are coming up next and they are our BeanShell scripts.
Now we are ready to build our rule classes. These will be BeanShell scripts, but the syntax is exactly the same as Java which is another reason BeanShell is a great option; no need to learn a different language to implement scripting.
Our first rule, MonthlyContractsInvoiceCalculationRule will calculate invoices for our monthly contracts:
import java.math.BigDecimal;
import com.billing.invoice.InvoiceData;
public BigDecimal calculate( InvoiceData invoiceData ) {
int chargeableHours = invoiceData.getHoursWorked() - invoiceData.getHoursEntitled();
BigDecimal chargeableHoursAmount = new BigDecimal( 0 );
BigDecimal invoiceAmount = new BigDecimal( 0 );
if ( chargeableHours > 0 ) {
chargeableHoursAmount = invoiceData.getHourlyRate().multiply( new BigDecimal( chargeableHours ) );
}
invoiceAmount = chargeableHoursAmount.add( invoiceData.getMonthlyRate() );
return invoiceAmount;
}
Our second rule, HourlyContractsInvoiceCalculationRule will calculate invoices for our hourly contracts:
import java.math.BigDecimal;
import com.billing.invoice.InvoiceData;
public BigDecimal calculate( InvoiceData invoiceData ) {
BigDecimal invoiceAmount = invoiceData.getHourlyRate().multiply(
new BigDecimal( invoiceData.getHoursWorked() ) );
return invoiceAmount;
}
Now that we have all of our classes setup, we need to work on our Spring configuration file so that we can actually retrieve our rules when we need them. Here's an example of what our configuration file would look like:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:lang="http://www.springframework.org/schema/lang"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/lang
http://www.springframework.org/schema/lang/spring-lang-2.0.xsd">
<lang:bsh id="hourlyContractsInvoiceCalculationRule"
script-source="classpath:com/billing/invoice/rules/HourlyContractsInvoiceCalculationRule.bsh"
script-interfaces="com.billing.invoice.rules.InvoiceCalculationRule"
refresh-check-delay="60000" />
<lang:bsh id="monthlyContractsInvoiceCalculationRule"
script-source="classpath:com/billing/invoice/rules/MonthlyContractsInvoiceCalculationRule.bsh"
script-interfaces="com.billing.invoice.rules.InvoiceCalculationRule"
refresh-check-delay="60000" />
</beans>
We have everything in place and we are ready to tie it all together. For this example, I am just writing up a quick test, so it isn't a realistic class, but it will give you an idea of how to use our new rules.
package com.billing.invoice;
import com.billing.invoice.rules.InvoiceCalculationRule;
import java.math.BigDecimal;
import java.math.RoundingMode;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class RuleTestMain {
public static void main( String [] args ) {
ApplicationContext context = new ClassPathXmlApplicationContext( "spring.xml" );
InvoiceData invoiceOne = new InvoiceData();
InvoiceData invoiceTwo = new InvoiceData();
invoiceOne.setHoursWorked( 40 );
invoiceOne.setHoursEntitled( 30 );
invoiceOne.setMonthlyRate( new BigDecimal( 1500 ) );
invoiceOne.setHourlyRate( new BigDecimal( 44.25 ) );
InvoiceCalculationRule monthlyRule =
( InvoiceCalculationRule ) context.getBean( "monthlyContractsInvoiceCalculationRule" );
System.out.println( "Monthly Invoice Amount: $" + monthlyRule.calculate( invoiceOne ).setScale( 2, RoundingMode.HALF_UP ) );
invoiceTwo.setHoursWorked( 40 );
invoiceTwo.setHourlyRate( new BigDecimal( 52.25 ) );
InvoiceCalculationRule hourlyRule =
( InvoiceCalculationRule ) context.getBean( "hourlyContractsInvoiceCalculationRule" );
System.out.println( "Hourly Invoice Amount: $" + hourlyRule.calculate( invoiceTwo ).setScale( 2, RoundingMode.HALF_UP ) );
}
}
When we run this code, we will be calculating the first invoice using the monthly rule, then the second invoice will be calculated using the hourly rule. The great thing is, that if we decide we need to change one of these rules, we can actually update the scripts and it will be automatically reloaded every minute and will become active without having to rebuild the application.