HK2 - Dependency Injection Kernel

A light-weight and dynamic dependency injection framework

Operations

Operations are building blocks for crafting scope/context pairs like RequestScoped, ApplicationScoped or TransactionScoped. HK2 is designed to run in any java process, including those that do not have a container such as Java EE. Even when running on a JVM with no container, many applications still have a concept of user requests or of different user applications running inside the same JVM. In those cases it may be useful for the writers of the system to use HK2 Operations to create a class of service whose life-cycle is controlled by the start or stop of those user requests or controlling application or whatever other demarcation is needed by the process as a whole.

Operations Introduction

A HK2 Operation is defined first by a scope annotation. Any scope annotation will do, but normally these scopes are proxiable since they are meant to be injected into services with different lifecycles, such as Singleton scoped services. Also, they are normally marked to not be proxiable for the same scope. It is not a requirement that scopes used for HK2 operations are proxiable, it is just the normal case that they are.

Once an HK2 Operation scope has been defined an implementation of Context must be added to the target ServiceLocator. To create an HK2 Operation Context all that is required is to extend OperationContext and implement the getScope method, returning the class of the scope.

The system software will use the OperationManager to create instances of the HK2 Operation. A thread may only have have one instance of an HK2 Operation active at any time. The following statements are also true:

  • Any number of HK2 Operations of different types (scopes) can be active on a single thread
  • An instance of a HK2 Operation can be active on more than one thread at a time

When an instance of a HK2 Operation is created an OperationHandle is returned. The OperationHandle can be used to suspend, resume or destroy the Operation instance. It can be used to discover all the threads upon which an Operation instance is active. The OperationHandle is itself registered in HK2 as a service in the Operation scope for which it was created, which is useful because arbitrary data can be associated with the OperationHandle.

The following example will illustrate basic usage of HK2 Operations.

Operations Example

The example can be found under the HK2 source tree at examples/operations. In the example there is an banking application that maintains a deposit ledger and withdrawal ledger indexed by account number. These two ledgers keep track of the funds deposited or those available for withdrawal. Both services are scoped by the bank they represent. There is a banking service that can get the amounts deposited in an account at a bank or available for withdrawal from an account at a bank or it can transfer money from one bank account to another bank account. It does this by switching the bank of the deposit account and/or the withdrawal account using HK2 Operations.

Example Operation Scope/Context pairs

There are two operation types/scopes defined in this example, a Deposit scope for the depositor ledgers and a Withdrawal scope for the withdrawal ledgers. Here is the definition of the Deposit scope:

@Scope
@Proxiable(proxyForSameScope = false)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface DepositScope {
}

Notice that the DepositScope is proxiable, and that it will not be proxied for other services that are also in the DepositScope scope. In HK2 every defined scope needs an implementation of Context. Since this is to be an HK2 Operation the user only need to extend OperationContext as below:

@Singleton
public class DepositScopeContext extends OperationContext<DepositScope> {

    public Class<? extends Annotation> getScope() {
        return DepositScope.class;
    }
    
}

The only method that needs to be implemented is the getScope method which need only return the class of the scope annotation. The DespositScope has now been fully defined as an HK2 Operation. Below find the complete definition of the WithdrawalScope:

@Scope
@Proxiable(proxyForSameScope = false)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface WithdrawalScope {
}
@Singleton
public class WithdrawalScopeContext extends OperationContext<WithdrawalScope> {

    public Class<? extends Annotation> getScope() {
        return WithdrawalScope.class;
    }

}

Operation Scoped Services

Lets take a look at two services that are in the HK2 Operations scopes that we just defined. The first one is the DepositLedger, which keeps track of how much has been deposited in an account by account number:

@DepositScope
public class DepositorService {
    private Map<Long, Integer> accounts = new HashMap<Long, Integer>();
    
    public void depositFunds(long account, int funds) {
        Integer balance = accounts.get(account);
        if (balance == null) {
            accounts.put(account, funds);
        }
        else {
            int original = balance;
            original += funds;
            accounts.put(account, original);
        }
    }
    
    public int getBalance(long account) {
        Integer balance = accounts.get(account);
        if (balance == null) return 0;
        return balance;
        
    }

}

This simple service is in the DepositScope. The important thing to notice about this service is that it does not keep track in any way which bank it is keeping these accounts for. That is because the bank where these accounts reside will be managed by the system software using HK2 Operations. That frees this service to only manage accounts and not the banks where the accounts reside. The withdrawal service is very similar:

@WithdrawalScope
public class WithdrawalService {
    private Map<Long, Integer> accounts = new HashMap<Long, Integer>();
    
    public int withdrawlFunds(long account, int funds) {
        Integer balance = accounts.get(account);
        if (balance == null) {
            // How nice, 100 for me!
            balance = new Integer(100);
        }
        
        int current = balance;
        if (funds > current) {
            funds = current;
        }
        
        current = current - funds;
        accounts.put(account, current);
        
        return funds;
    }
    
    public int getBalance(long account) {
        Integer balance = accounts.get(account);
        if (balance == null) {
            accounts.put(account, 100);
        }
        
        return accounts.get(account);
    }
}

This service, like the previous one is in a proxiable HK2 Operation scope. In this case it is in the WithdrawalScope. Like the previous service, this service only keeps track of accounts with a single map, since the WithdrawalScope will be managed per bank by the software setting up the environment in which these services will get called.

The TransferService is in the Singleton scope, which is just a normal scope, not an HK2 Operations scope. Since a Singelton service has a different lifecycle than the services in either DepositorScope or WithdrawalScope the DepositorService and WithdrawalService injected into it will instead inject a proxy, so that the actual service used will depend on the context of the call. This is the TransferService:

@Singleton
public class TransferService {
    @Inject
    private DepositorService depositor;
    
    @Inject
    private WithdrawalService withdrawer;
    
    public int doTransfer(long depositAccount, long withdrawlAccount, int funds) {
        int recieved = withdrawer.withdrawlFunds(withdrawlAccount, funds);
        depositor.depositFunds(depositAccount, recieved);
        
        return recieved;
    }

}

The TransferService injects the DepositorService and the WithdrawalService in order to transfer funds from the withdrawer to the depositor. This service also doesn’t know which banks are involved in the transfer, so it is also expecting the system software to have used the HK2 Operations system in order to properly set the context of the call to doTransfer.

The Banking Service

The BankingService is the service whose implementation will use the HK2 Operations feature in order to properly setup the context in which the services in the DepositScope and WithdrawalScope will always be called. The BankingService is the service given to end-users to use directly and so is represented by the following contract:

@Contract
public interface BankingService {
    /**
     * Transfers funds between the withdrawal account and the deposit account.  If there
     * is not enough funds in the withdrawal account it may transfer less than the requested
     * amount
     * 
     * @param withdrawlBank The bank from which the withdrawal is being made
     * @param withdrawlAccount The account number to withdrawal funds from
     * @param depositorBank The bank to which the deposit is being done
     * @param depositAccount The account number to deposit funds to
     * @param funds The number of funds to transfer
     * @return The actual funds transferred
     */
    public int transferFunds(String withdrawlBank, long withdrawlAccount, String depositorBank, long depositAccount, int funds);
    
    /**
     * Tells how much money has been deposited in the given account in the
     * given bank
     * 
     * @param bank the name of the bank of the account to check
     * @param account the account number to get the deposited amount from
     * @return the amount of money deposited in this account
     */
    public int getDepositedBalance(String bank, long account);
    
    /**
     * Tells how much money that is available for withdrawal from the
     * given account number at the given bank.  Note that if the
     * account number has never been seen before the amount given
     * at the start is 100 funds
     * 
     * @param bank the name of the bank of the account to check
     * @param account the account number to check
     * @return the amount of money deposited in this account
     */
    public int getWithdrawlBalance(String bank, long account);

}

It is an implementation of the BankingService that the user code will use to perform the balance check and transfer operations. Lets take a look at the implementation of the getWithdrawalBalance in the implementation of BankingService, BankingServiceImpl:

@Singleton
public class BankingServiceImpl implements BankingService {
    @Inject
    private OperationManager manager;
    
    @Inject
    private WithdrawalService withdrawerAgent;
    
    private final Map<String, OperationHandle<WithdrawalScope>> withdrawers = new HashMap<String, OperationHandle<WithdrawalScope>>();
    
    private synchronized OperationHandle<WithdrawalScope> getWithdrawerBankHandle(String bank) {
        OperationHandle<WithdrawalScope> withdrawer = withdrawers.get(bank);
        if (withdrawer == null) {
            // create and start it
            withdrawer = manager.createOperation(WithdrawalScopeImpl.INSTANCE);
            withdrawers.put(bank, withdrawer);
        }
        
        return withdrawer;
    }

    public int getWithdrawalBalance(String bank, long account) {
        OperationHandle<WithdrawalScope> withdrawer = getWithdrawerBankHandle(bank);
        
        // Set the context for the withdrawal balance check
        withdrawer.resume();
        
        try {
            return withdrawerAgent.getBalance(account);
        }
        finally {
            // suspend the operation
            withdrawer.suspend();
        }
    }

}

Code not involved in the implementation of getWithdrawalBalance has been removed from the code snippet above.

The BankingServiceImpl has a Map that goes from a Bank (represented as a String) to an HK2 OperationHandle of type WithdrawalScope. The method getWithdrawerBankHandle looks in the Map for an OperationHandle. If it does not find one, it uses the createOperation method of OperationManager in order to create it. This method does not start the operation, it just retrieves it from the Map or creates it.

The getWithdrawalBalance method of BankingServiceImpl uses the getWithdrawerBankHandle to get the WithdrawalScope OperationHandle associated with the given bank and then associates that OperationHandle with the current thread by calling the resume method. Calling the resume method of OperationHandle sets the calling context for the injected field withdrawerAgent, which will now use the WithdrawalService for that particular bank. Once the underlying operation has completed, the suspend method of OperationHandle is called to disassociate that HK2 Operation from the current thread.

The getDepositedBalance method of BankingServiceImpl works the same way as the getWithdrawalBalance with the difference being that the HK2 Operation used is of type DepositScope as opposed to WithdrawalScope.

Transferring Funds Between Banks

The implementation of the method transferFunds in BankingServiceImpl is seen below:

    public synchronized int transferFunds(String withdrawlBank, long withdrawlAccount,
            String depositorBank, long depositAccount, int funds) {
        OperationHandle<DepositScope> depositor = getDepositBankHandle(depositorBank);
        OperationHandle<WithdrawalScope> withdrawer = getWithdrawerBankHandle(withdrawlBank);
        
        // Set the context for the transfer
        depositor.resume();
        withdrawer.resume();
        
        // At this point the scopes are set properly, we can just call the service!
        try {
            return transferAgent.doTransfer(depositAccount, withdrawlAccount, funds);
        }
        finally {
            // Turn off the two scopes
            withdrawer.suspend();
            depositor.suspend();
        }
        
    }

The interesting thing about this method is that it illustrates that two HK2 Operations of different types (DepositScope and WithdrawalScope) can be active on a single thread at the same time. This duality allows the transfer service to operate between banks simultaneously.

Seeing it Work

To see the example working there is a junit test case. The test case simply creates some Bank Strings (Chase, Bank of America and South Jersey Federal Credit Union) and then checks balances, makes transfers between banks, and checks the balances afterwards to make sure they are as expected. Here it is:

@Test
    public void testTransferBetweenBanks() {
        ServiceLocator locator = getServiceLocator();
        
        BankingService bankingService = locator.getService(BankingService.class);
        
        // First, initialize the accounts of ALICE, BOB and CAROL with 100 funds
        int aliceBalance = bankingService.getWithdrawalBalance(CHASE_BANK, ALICE_ACCOUNT);
        int bobBalance = bankingService.getWithdrawalBalance(BOA_BANK, BOB_ACCOUNT);
        int carolBalance = bankingService.getWithdrawalBalance(SJFCU_BANK, CAROL_ACCOUNT);
        
        Assert.assertEquals(100, aliceBalance);
        Assert.assertEquals(100, bobBalance);
        Assert.assertEquals(100, carolBalance);
        
        // OK, lets transfer that 100 from alice to bob
        int amtTransferred = bankingService.transferFunds(CHASE_BANK, ALICE_ACCOUNT, BOA_BANK, BOB_ACCOUNT, 100);
        
        Assert.assertEquals(100, amtTransferred);
        
        // And lets check the withdrawl funds again, alice should have zero, bob should have 100, carol should still have 100
        aliceBalance = bankingService.getWithdrawalBalance(CHASE_BANK, ALICE_ACCOUNT);
        bobBalance = bankingService.getWithdrawalBalance(BOA_BANK, BOB_ACCOUNT);
        carolBalance = bankingService.getWithdrawalBalance(SJFCU_BANK, CAROL_ACCOUNT);
        
        Assert.assertEquals(0, aliceBalance);
        Assert.assertEquals(100, bobBalance);
        Assert.assertEquals(100, carolBalance);
        
        // But now bob should have 100 in his deposit account
        int toAlice = bankingService.getDepositedBalance(CHASE_BANK, ALICE_ACCOUNT);
        int toBob = bankingService.getDepositedBalance(BOA_BANK, BOB_ACCOUNT);
        int toCarol = bankingService.getDepositedBalance(SJFCU_BANK, CAROL_ACCOUNT);
        
        Assert.assertEquals(0, toAlice);
        Assert.assertEquals(100, toBob);
        Assert.assertEquals(0, toCarol);
        
        // Now lets have Carol transfer to Alice
        amtTransferred = bankingService.transferFunds(SJFCU_BANK, CAROL_ACCOUNT, CHASE_BANK, ALICE_ACCOUNT, 100);
        
        // Now Alice and Carol should have nothing left, while Bob still has his original 100
        aliceBalance = bankingService.getWithdrawalBalance(CHASE_BANK, ALICE_ACCOUNT);
        bobBalance = bankingService.getWithdrawalBalance(BOA_BANK, BOB_ACCOUNT);
        carolBalance = bankingService.getWithdrawalBalance(SJFCU_BANK, CAROL_ACCOUNT);
        
        Assert.assertEquals(0, aliceBalance);
        Assert.assertEquals(100, bobBalance);
        Assert.assertEquals(0, carolBalance);
        
        // At this point Alice and Carol should each have 100 in there deposit account
        toAlice = bankingService.getDepositedBalance(CHASE_BANK, ALICE_ACCOUNT);
        toBob = bankingService.getDepositedBalance(BOA_BANK, BOB_ACCOUNT);
        toCarol = bankingService.getDepositedBalance(SJFCU_BANK, CAROL_ACCOUNT);
        
        Assert.assertEquals(100, toAlice);
        Assert.assertEquals(100, toBob);
        Assert.assertEquals(0, toCarol);
        
        // Yay, the Operations worked properly!
    }

Conclusion

HK2 Operations provide a convenient set of tools for building scopes/context pairs that follow the general rule of “one operation on a thread at a time.” There are many Operations that correspond to this rule, such as RequestScope, ApplicationScope and TransactionScope. Using a consistent facility such as HK2 Operations can reduce the code needed by your application to manage those scopes.