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.