Repository: alibaba/fescar
Pull request #525 (2019-03-05 13:24:23)

[Feature] add transaction hook 
<!-- Please make sure you have read and understood the contributing guidelines -->

### Ⅰ. Describe what this PR did
Feature add transaction hook 

### Ⅱ. Does this pull request fix one issue?
<!-- If that, add "fixes #xxx" below in the next line, for example, fixes #97. -->
add  transaction hook in the Transaction Life Cycle

### Ⅲ. Why don't you add test cases (unit test/integration test)? 
add test in TransactionHookManagerTest
add test in TransactionTemplateTest

### Ⅳ. Describe how to verify it


### Ⅴ. Special notes for reviews



codecov-io: # [Codecov](https://codecov.io/gh/alibaba/fescar/pull/525?src=pr&el=h1) Report
> Merging [#525](https://codecov.io/gh/alibaba/fescar/pull/525?src=pr&el=desc) into [develop](https://codecov.io/gh/alibaba/fescar/commit/ec825e6a3a9b55f0618cf87ca009c44e34f8fd3f?src=pr&el=desc) will **increase** coverage by `0.11%`.
> The diff coverage is `55.55%`.

[![Impacted file tree graph](https://codecov.io/gh/alibaba/fescar/pull/525/graphs/tree.svg?width=650&token=Hy9a6MCc4p&height=150&src=pr)](https://codecov.io/gh/alibaba/fescar/pull/525?src=pr&el=tree)

```diff
@@              Coverage Diff              @@
##             develop     #525      +/-   ##
=============================================
+ Coverage      31.81%   31.92%   +0.11%     
- Complexity       735      747      +12     
=============================================
  Files            195      198       +3     
  Lines           7595     7645      +50     
  Branches         911      913       +2     
=============================================
+ Hits            2416     2441      +25     
- Misses          4926     4944      +18     
- Partials         253      260       +7
```


| [Impacted Files](https://codecov.io/gh/alibaba/fescar/pull/525?src=pr&el=tree) | Coverage Δ | Complexity Δ | |
|---|---|---|---|
| [...i/transaction/TransactionHookExecuteException.java](https://codecov.io/gh/alibaba/fescar/pull/525/diff?src=pr&el=tree#diff-dG0vc3JjL21haW4vamF2YS9jb20vYWxpYmFiYS9mZXNjYXIvdG0vYXBpL3RyYW5zYWN0aW9uL1RyYW5zYWN0aW9uSG9va0V4ZWN1dGVFeGNlcHRpb24uamF2YQ==) | `0% <0%> (ø)` | `0 <0> (?)` | |
| [...car/tm/api/transaction/TransactionHookAdapter.java](https://codecov.io/gh/alibaba/fescar/pull/525/diff?src=pr&el=tree#diff-dG0vc3JjL21haW4vamF2YS9jb20vYWxpYmFiYS9mZXNjYXIvdG0vYXBpL3RyYW5zYWN0aW9uL1RyYW5zYWN0aW9uSG9va0FkYXB0ZXIuamF2YQ==) | `20% <20%> (ø)` | `1 <1> (?)` | |
| [...m/alibaba/fescar/tm/api/TransactionalTemplate.java](https://codecov.io/gh/alibaba/fescar/pull/525/diff?src=pr&el=tree#diff-dG0vc3JjL21haW4vamF2YS9jb20vYWxpYmFiYS9mZXNjYXIvdG0vYXBpL1RyYW5zYWN0aW9uYWxUZW1wbGF0ZS5qYXZh) | `56.52% <57.5%> (-11.9%)` | `8 <6> (+6)` | |
| [...car/tm/api/transaction/TransactionHookManager.java](https://codecov.io/gh/alibaba/fescar/pull/525/diff?src=pr&el=tree#diff-dG0vc3JjL21haW4vamF2YS9jb20vYWxpYmFiYS9mZXNjYXIvdG0vYXBpL3RyYW5zYWN0aW9uL1RyYW5zYWN0aW9uSG9va01hbmFnZXIuamF2YQ==) | `68.75% <68.75%> (ø)` | `5 <5> (?)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/alibaba/fescar/pull/525?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/alibaba/fescar/pull/525?src=pr&el=footer). Last update [ec825e6...be0aacc](https://codecov.io/gh/alibaba/fescar/pull/525?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

slievrly: @jovany-wang 

Patch:
diff --git a/tm/src/main/java/com/alibaba/fescar/tm/api/TransactionalTemplate.java b/tm/src/main/java/com/alibaba/fescar/tm/api/TransactionalTemplate.java
index 1a83ae089..de43ca9f4 100644
--- a/tm/src/main/java/com/alibaba/fescar/tm/api/TransactionalTemplate.java
+++ b/tm/src/main/java/com/alibaba/fescar/tm/api/TransactionalTemplate.java
@@ -17,69 +17,165 @@
 package com.alibaba.fescar.tm.api;
 
 import com.alibaba.fescar.core.exception.TransactionException;
+import com.alibaba.fescar.tm.api.transaction.TransactionHook;
+import com.alibaba.fescar.tm.api.transaction.TransactionHookManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
 
 /**
  * Template of executing business logic with a global transaction.
  */
 public class TransactionalTemplate {
 
+    private static final Logger LOGGER = LoggerFactory.getLogger(TransactionalTemplate.class);
+
+
     /**
      * Execute object.
      *
      * @param business the business
      * @return the object
-     * @throws ExecutionException the execution exception
+     * @throws TransactionalExecutor.ExecutionException the execution exception
      */
     public Object execute(TransactionalExecutor business) throws TransactionalExecutor.ExecutionException {
 
         // 1. get or create a transaction
         GlobalTransaction tx = GlobalTransactionContext.getCurrentOrCreate();
 
-        // 2. begin transaction
         try {
-            tx.begin(business.timeout(), business.name());
 
-        } catch (TransactionException txe) {
-            throw new TransactionalExecutor.ExecutionException(tx, txe,
-                TransactionalExecutor.Code.BeginFailure);
+            // 2. begin transaction
+            try {
+                triggerBeforeBegin();
+                tx.begin(business.timeout(), business.name());
+                triggerAfterBegin();
+            } catch (TransactionException txe) {
+                throw new TransactionalExecutor.ExecutionException(tx, txe,
+                        TransactionalExecutor.Code.BeginFailure);
 
-        }
+            }
+            Object rs = null;
+            try {
 
-        Object rs = null;
-        try {
+                // Do Your Business
+                rs = business.execute();
 
-            // Do Your Business
-            rs = business.execute();
+            } catch (Throwable ex) {
 
-        } catch (Throwable ex) {
+                // 3. any business exception, rollback.
+                try {
+                    triggerBeforeRollback();
+                    tx.rollback();
+                    triggerAfterRollback();
+                    // 3.1 Successfully rolled back
+                    throw new TransactionalExecutor.ExecutionException(tx, TransactionalExecutor.Code.RollbackDone, ex);
 
-            // 3. any business exception, rollback.
-            try {
-                tx.rollback();
+                } catch (TransactionException txe) {
+                    // 3.2 Failed to rollback
+                    throw new TransactionalExecutor.ExecutionException(tx, txe,
+                            TransactionalExecutor.Code.RollbackFailure, ex);
 
-                // 3.1 Successfully rolled back
-                throw new TransactionalExecutor.ExecutionException(tx, TransactionalExecutor.Code.RollbackDone, ex);
+                }
 
+            }
+            // 4. everything is fine, commit.
+            try {
+                triggerBeforeCommit();
+                tx.commit();
+                triggerAfterCommit();
             } catch (TransactionException txe) {
-                // 3.2 Failed to rollback
+                // 4.1 Failed to commit
                 throw new TransactionalExecutor.ExecutionException(tx, txe,
-                    TransactionalExecutor.Code.RollbackFailure, ex);
+                        TransactionalExecutor.Code.CommitFailure);
+            }
+
+            return rs;
+        }finally {
+            //5. clear
+            triggerAfterCompletion();
+            cleanUp();
+        }
+    }
+
 
+    private void triggerBeforeBegin() {
+        for (TransactionHook hook : getCurrentHooks()) {
+            try {
+                hook.beforeBegin();
+            } catch (Exception e) {
+                LOGGER.error("Failed execute beforeBegin in hook " + e.getMessage());
             }
+        }
+    }
 
+    private void triggerAfterBegin() {
+        for (TransactionHook hook : getCurrentHooks()) {
+            try {
+                hook.afterBegin();
+            } catch (Exception e) {
+                LOGGER.error("Failed execute afterBegin in hook " + e.getMessage());
+            }
         }
+    }
 
-        // 4. everything is fine, commit.
-        try {
-            tx.commit();
+    private void triggerBeforeRollback() {
+        for (TransactionHook hook : getCurrentHooks()) {
+            try {
+                hook.beforeRollback();
+            } catch (Exception e) {
+                LOGGER.error("Failed execute beforeRollback in hook " + e.getMessage());
+            }
+        }
+    }
+
+    private void triggerAfterRollback() {
+        for (TransactionHook hook : getCurrentHooks()) {
+            try {
+                hook.afterRollback();
+            } catch (Exception e) {
+                LOGGER.error("Failed execute afterRollback in hook " + e.getMessage());
+            }
+        }
+    }
+
+    private void triggerBeforeCommit() {
+        for (TransactionHook hook : getCurrentHooks()) {
+            try {
+                hook.beforeCommit();
+            } catch (Exception e) {
+                LOGGER.error("Failed execute beforeCommit in hook " + e.getMessage());
+            }
+        }
+    }
 
-        } catch (TransactionException txe) {
-            // 4.1 Failed to commit
-            throw new TransactionalExecutor.ExecutionException(tx, txe,
-                TransactionalExecutor.Code.CommitFailure);
+    private void triggerAfterCommit() {
+        for (TransactionHook hook : getCurrentHooks()) {
+            try {
+                hook.afterCommit();
+            } catch (Exception e) {
+                LOGGER.error("Failed execute afterCommit in hook " + e.getMessage());
+            }
+        }
+    }
 
+    private void triggerAfterCompletion() {
+        for (TransactionHook hook : getCurrentHooks()) {
+            try {
+                hook.afterCompletion();
+            } catch (Exception e) {
+                LOGGER.error("Failed execute afterCompletion in hook " + e.getMessage());
+            }
         }
-        return rs;
+    }
+
+    private void cleanUp() {
+        TransactionHookManager.clear();
+    }
+
+    private List<TransactionHook> getCurrentHooks() {
+        return TransactionHookManager.getHooks();
     }
 
 }
diff --git a/tm/src/main/java/com/alibaba/fescar/tm/api/transaction/TransactionHook.java b/tm/src/main/java/com/alibaba/fescar/tm/api/transaction/TransactionHook.java
new file mode 100644
index 000000000..5eae2b6b4
--- /dev/null
+++ b/tm/src/main/java/com/alibaba/fescar/tm/api/transaction/TransactionHook.java
@@ -0,0 +1,58 @@
+/*
+ *  Copyright 1999-2018 Alibaba Group Holding Ltd.
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package com.alibaba.fescar.tm.api.transaction;
+
+/**
+ * @author guoyao
+ * @date 2019/3/4
+ */
+public interface TransactionHook {
+
+    /**
+     * before tx begin
+     */
+    void beforeBegin();
+
+    /**
+     * after tx begin
+     */
+    void afterBegin();
+
+    /**
+     * before tx commit
+     */
+    void beforeCommit();
+
+    /**
+     * after tx commit
+     */
+    void afterCommit();
+
+    /**
+     * before tx rollback
+     */
+    void beforeRollback();
+
+    /**
+     * after tx rollback
+     */
+    void afterRollback();
+
+    /**
+     * after tx all Completed
+     */
+    void afterCompletion();
+}
diff --git a/tm/src/main/java/com/alibaba/fescar/tm/api/transaction/TransactionHookAdapter.java b/tm/src/main/java/com/alibaba/fescar/tm/api/transaction/TransactionHookAdapter.java
new file mode 100644
index 000000000..76a178c2a
--- /dev/null
+++ b/tm/src/main/java/com/alibaba/fescar/tm/api/transaction/TransactionHookAdapter.java
@@ -0,0 +1,58 @@
+/*
+ *  Copyright 1999-2018 Alibaba Group Holding Ltd.
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package com.alibaba.fescar.tm.api.transaction;
+
+/**
+ * @author guoyao
+ * @date 2019/3/5
+ */
+public class TransactionHookAdapter implements TransactionHook {
+
+    @Override
+    public void beforeBegin() {
+
+    }
+
+    @Override
+    public void afterBegin() {
+
+    }
+
+    @Override
+    public void beforeCommit() {
+
+    }
+
+    @Override
+    public void afterCommit() {
+
+    }
+
+    @Override
+    public void beforeRollback() {
+
+    }
+
+    @Override
+    public void afterRollback() {
+
+    }
+
+    @Override
+    public void afterCompletion() {
+
+    }
+}
diff --git a/tm/src/main/java/com/alibaba/fescar/tm/api/transaction/TransactionHookManager.java b/tm/src/main/java/com/alibaba/fescar/tm/api/transaction/TransactionHookManager.java
new file mode 100644
index 000000000..39ba5a472
--- /dev/null
+++ b/tm/src/main/java/com/alibaba/fescar/tm/api/transaction/TransactionHookManager.java
@@ -0,0 +1,68 @@
+/*
+ *  Copyright 1999-2018 Alibaba Group Holding Ltd.
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package com.alibaba.fescar.tm.api.transaction;
+
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author guoyao
+ * @date 2019/3/4
+ */
+public final class TransactionHookManager {
+
+    private static final ThreadLocal<List<TransactionHook>> LOCAL_HOOKS = new ThreadLocal<>();
+
+    /**
+     * get the current hooks
+     *
+     * @return
+     * @throws IllegalStateException
+     */
+    public static List<TransactionHook> getHooks() throws IllegalStateException {
+        List<TransactionHook> hooks = LOCAL_HOOKS.get();
+
+        if (hooks == null || hooks.isEmpty()) {
+            return Collections.emptyList();
+        }
+        return Collections.unmodifiableList(hooks);
+    }
+
+    /**
+     * add new hook
+     *
+     * @param transactionHook
+     */
+    public static void registerHook(TransactionHook transactionHook) {
+        if (transactionHook == null) {
+            throw new NullPointerException("transactionHook must not be null");
+        }
+        List<TransactionHook> transactionHooks = LOCAL_HOOKS.get();
+        if (transactionHooks == null) {
+            LOCAL_HOOKS.set(new ArrayList<>());
+        }
+        LOCAL_HOOKS.get().add(transactionHook);
+    }
+
+    /**
+     * clear hooks
+     */
+    public static void clear() {
+        LOCAL_HOOKS.remove();
+    }
+}


Test patch:
diff --git a/tm/src/test/java/com/alibaba/fescar/tm/api/TransactionTemplateTest.java b/tm/src/test/java/com/alibaba/fescar/tm/api/TransactionTemplateTest.java
new file mode 100644
index 000000000..4e31a1df0
--- /dev/null
+++ b/tm/src/test/java/com/alibaba/fescar/tm/api/TransactionTemplateTest.java
@@ -0,0 +1,95 @@
+/*
+ *  Copyright 1999-2018 Alibaba Group Holding Ltd.
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package com.alibaba.fescar.tm.api;
+
+import com.alibaba.fescar.core.model.GlobalStatus;
+import com.alibaba.fescar.core.model.TransactionManager;
+import com.alibaba.fescar.tm.DefaultTransactionManager;
+import com.alibaba.fescar.tm.api.transaction.TransactionHook;
+import com.alibaba.fescar.tm.api.transaction.TransactionHookManager;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import static org.mockito.Mockito.*;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author guoyao
+ * @date 2019/3/6
+ */
+public class TransactionTemplateTest {
+
+    private static final String DEFAULT_XID = "123456789";
+    private static final String DEFAULT_NAME = "test";
+    private static final int DEFAULT_TIME_OUT = 30000;
+
+
+    @Before
+    public void init() throws Exception {
+        TransactionManager transactionManager = mock(TransactionManager.class);
+        when(transactionManager.begin(null, null, DEFAULT_NAME, DEFAULT_TIME_OUT)).thenReturn(DEFAULT_XID);
+        when(transactionManager.commit(DEFAULT_XID)).thenReturn(GlobalStatus.Committed);
+        when(transactionManager.rollback(DEFAULT_XID)).thenReturn(GlobalStatus.Rollbacked);
+        when(transactionManager.getStatus(DEFAULT_XID)).thenReturn(GlobalStatus.Begin);
+        DefaultTransactionManager.set(transactionManager);
+    }
+
+    @After
+    public void assertHooks() {
+        assertThat(TransactionHookManager.getHooks()).isEmpty();
+    }
+
+
+    @Test
+    public void testTransactionCommitHook() throws Exception {
+        TransactionHook transactionHook = Mockito.mock(TransactionHook.class);
+        TransactionalExecutor transactionalExecutor = Mockito.mock(TransactionalExecutor.class);
+        when(transactionalExecutor.name()).thenReturn(DEFAULT_NAME);
+        when(transactionalExecutor.timeout()).thenReturn(DEFAULT_TIME_OUT);
+        TransactionHookManager.registerHook(transactionHook);
+        TransactionalTemplate template = new TransactionalTemplate();
+        template.execute(transactionalExecutor);
+        verify(transactionHook).beforeBegin();
+        verify(transactionHook).afterBegin();
+        verify(transactionHook).beforeCommit();
+        verify(transactionHook).afterCommit();
+        verify(transactionHook).afterCompletion();
+    }
+
+    @Test
+    public void testTransactionRollbackHook() throws Throwable {
+        TransactionHook transactionHook = Mockito.mock(TransactionHook.class);
+        TransactionalExecutor transactionalExecutor = Mockito.mock(TransactionalExecutor.class);
+        when(transactionalExecutor.name()).thenReturn(DEFAULT_NAME);
+        when(transactionalExecutor.timeout()).thenReturn(DEFAULT_TIME_OUT);
+        when(transactionalExecutor.execute()).thenThrow(new RuntimeException());
+        TransactionHookManager.registerHook(transactionHook);
+        TransactionalTemplate template = new TransactionalTemplate();
+        try {
+            template.execute(transactionalExecutor);
+        } catch (Exception e) {
+            //catch rollback exception
+        }
+        verify(transactionHook).beforeBegin();
+        verify(transactionHook).afterBegin();
+        verify(transactionHook).beforeRollback();
+        verify(transactionHook).afterRollback();
+        verify(transactionHook).afterCompletion();
+    }
+
+}
diff --git a/tm/src/test/java/com/alibaba/fescar/tm/api/transaction/TransactionHookManagerTest.java b/tm/src/test/java/com/alibaba/fescar/tm/api/transaction/TransactionHookManagerTest.java
new file mode 100644
index 000000000..c9ad5e63d
--- /dev/null
+++ b/tm/src/test/java/com/alibaba/fescar/tm/api/transaction/TransactionHookManagerTest.java
@@ -0,0 +1,64 @@
+/*
+ *  Copyright 1999-2018 Alibaba Group Holding Ltd.
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package com.alibaba.fescar.tm.api.transaction;
+
+import org.junit.After;
+import org.junit.Test;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author guoyao
+ * @date 2019/3/5
+ */
+public class TransactionHookManagerTest {
+
+
+    @After
+    public void clear() {
+        TransactionHookManager.clear();
+    }
+
+    @Test
+    public void testRegisterHook() {
+        TransactionHookAdapter transactionHookAdapter = new TransactionHookAdapter();
+        TransactionHookManager.registerHook(transactionHookAdapter);
+        List<TransactionHook> hooks = TransactionHookManager.getHooks();
+        assertThat(hooks).isNotEmpty();
+        assertThat(hooks.get(0)).isEqualTo(transactionHookAdapter);
+    }
+
+    @Test
+    public void testGetHooks() {
+        assertThat(TransactionHookManager.getHooks()).isEmpty();
+        TransactionHookManager.registerHook(new TransactionHookAdapter());
+        assertThat(TransactionHookManager.getHooks()).isNotEmpty();
+    }
+
+
+    @Test
+    public void testClear() {
+        assertThat(TransactionHookManager.getHooks()).isEmpty();
+        TransactionHookManager.registerHook(new TransactionHookAdapter());
+        assertThat(TransactionHookManager.getHooks()).isNotEmpty();
+        TransactionHookManager.clear();
+        assertThat(TransactionHookManager.getHooks()).isEmpty();
+    }
+
+
+}