| [//]: # " " |
| [//]: # " Copyright (c) 2013, 2021 Oracle and/or its affiliates. All rights reserved. " |
| [//]: # " " |
| [//]: # " This program and the accompanying materials are made available under the " |
| [//]: # " terms of the Eclipse Public License v. 2.0, which is available at " |
| [//]: # " http://www.eclipse.org/legal/epl-2.0. " |
| [//]: # " " |
| [//]: # " This Source Code may also be made available under the following Secondary " |
| [//]: # " Licenses when the conditions for such availability set forth in the " |
| [//]: # " Eclipse Public License v. 2.0 are satisfied: GNU General Public License, " |
| [//]: # " version 2 with the GNU Classpath Exception, which is available at " |
| [//]: # " https://www.gnu.org/software/classpath/license.html. " |
| [//]: # " " |
| [//]: # " SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 " |
| [//]: # " " |
| |
| * TOC |
| {:toc} |
| |
| # 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][context] must be added to |
| the target ServiceLocator. To create an HK2 Operation [Context][context] all that is required is to extend |
| [OperationContext][operationcontext] and implement the getScope method, returning the class of the scope. |
| |
| The system software will use the [OperationManager][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][operationhandle] is |
| returned. The [OperationHandle][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][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][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: |
| |
| ```java |
| @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][context]. Since this is to be an HK2 Operation the user only need to extend |
| [OperationContext][operationcontext] as below: |
| |
| ```java |
| @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: |
| |
| ```java |
| @Scope |
| @Proxiable(proxyForSameScope = false) |
| @Retention(RetentionPolicy.RUNTIME) |
| @Target(ElementType.TYPE) |
| public @interface WithdrawalScope { |
| } |
| ``` |
| |
| ```java |
| @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: |
| |
| ```java |
| @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: |
| |
| ```java |
| @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: |
| |
| ```java |
| @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: |
| |
| ```java |
| @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: |
| |
| ```java |
| @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][operationhandle] of type WithdrawalScope. The method |
| getWithdrawerBankHandle looks in the Map for an [OperationHandle][operationhandle]. |
| If it does not find one, it uses the createOperation method of |
| [OperationManager][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][operationhandle] associated with the |
| given bank and then associates that [OperationHandle][operationhandle] with the |
| current thread by calling the resume method. Calling the resume method |
| of [OperationHandle][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][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: |
| |
| ```java |
| 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: |
| |
| ```java |
| @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. |
| |
| [context]: apidocs/org/glassfish/hk2/api/Context.html |
| [operationcontext]: apidocs/org/glassfish/hk2/extras/operation/OperationContext.html |
| [operationmanager]: apidocs/org/glassfish/hk2/extras/operation/OperationManager.html |
| [operationhandle]: apidocs/org/glassfish/hk2/extras/operation/OperationHandle.html |