diff --git a/all/pom.xml b/all/pom.xml
index dfffb07feea..8148247ed4b 100644
--- a/all/pom.xml
+++ b/all/pom.xml
@@ -339,6 +339,11 @@
seata-saga-spring
${project.version}
+
+ org.apache.seata
+ seata-saga-annotation
+ ${project.version}
+
io.seata
seata-all
diff --git a/bom/pom.xml b/bom/pom.xml
index 722c97660f9..8162f25b5ca 100644
--- a/bom/pom.xml
+++ b/bom/pom.xml
@@ -348,6 +348,11 @@
seata-saga-engine-store
${project.version}
+
+ org.apache.seata
+ seata-saga-annotation
+ ${project.version}
+
org.apache.seata
seata-sqlparser-core
diff --git a/changes/en-us/2.x.md b/changes/en-us/2.x.md
index 22a961edcae..07e86efecbd 100644
--- a/changes/en-us/2.x.md
+++ b/changes/en-us/2.x.md
@@ -7,6 +7,7 @@ Add changes here for all PR submitted to the 2.x branch.
- [[#6876](https://github.com/apache/incubator-seata/pull/6876)] support kingbase
- [[#6881](https://github.com/apache/incubator-seata/pull/6881)] support grpc
- [[#6864](https://github.com/apache/incubator-seata/pull/6864)] support shentong database
+- [[#6973](https://github.com/apache/incubator-seata/pull/6973)] support saga annotation
- [[#6974](https://github.com/apache/incubator-seata/pull/6974)] support fastjson2 undolog parser
- [[#6992](https://github.com/apache/incubator-seata/pull/6992)] support grpc serializer
diff --git a/changes/zh-cn/2.x.md b/changes/zh-cn/2.x.md
index 89bfb6438f6..456c0299784 100644
--- a/changes/zh-cn/2.x.md
+++ b/changes/zh-cn/2.x.md
@@ -7,10 +7,12 @@
- [[#6876](https://github.com/apache/incubator-seata/pull/6876)] 支持人大金仓数据库(kingbase)
- [[#6881](https://github.com/apache/incubator-seata/pull/6881)] client和server支持grpc协议
- [[#6864](https://github.com/apache/incubator-seata/pull/6864)] 支持神通数据库(oscar)
+- [[#6973](https://github.com/apache/incubator-seata/pull/6973)] 支持saga注解化
- [[#6974](https://github.com/apache/incubator-seata/pull/6974)] 支持UndoLog的fastjson2序列化方式
- [[#6992](https://github.com/apache/incubator-seata/pull/6992)] 支持grpc序列化器
- [[#6995](https://github.com/apache/incubator-seata/pull/6995)] 升级过时的 npmjs 依赖
+
### bugfix:
- [[#6899](https://github.com/apache/incubator-seata/pull/6899)] 修复file.conf打包后的读取
- [[#6890](https://github.com/apache/incubator-seata/pull/6890)] 修复saga设计json转标准json过程中: 子状态机补偿节点无法被识别
diff --git a/common/src/main/java/org/apache/seata/common/util/ReflectionUtil.java b/common/src/main/java/org/apache/seata/common/util/ReflectionUtil.java
index 5da00a4b611..51e33b03892 100644
--- a/common/src/main/java/org/apache/seata/common/util/ReflectionUtil.java
+++ b/common/src/main/java/org/apache/seata/common/util/ReflectionUtil.java
@@ -25,11 +25,13 @@
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.Collections;
+import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Predicate;
/**
* Reflection tools
@@ -495,6 +497,34 @@ public static Method getMethod(final Class> clazz, final String methodName)
return getMethod(clazz, methodName, EMPTY_CLASS_ARRAY);
}
+ /**
+ * Recursively get clazz and their interfaces match matchCondition method-class mapping
+ *
+ * @param clazz clazz
+ * @param matchCondition matchCondition
+ * @return Set
+ */
+ public static Map> findMatchMethodClazzMap(Class> clazz, Predicate matchCondition) {
+ Map> methodClassMap = new HashMap<>();
+
+ for (Method method : clazz.getMethods()) {
+ if (matchCondition.test(method)) {
+ methodClassMap.put(method, clazz);
+ }
+ }
+
+ Set> interfaceClasses = getInterfaces(clazz);
+ for (Class> interClass : interfaceClasses) {
+ for (Method method : interClass.getMethods()) {
+ if (matchCondition.test(method)) {
+ methodClassMap.put(method, interClass);
+ }
+ }
+ }
+
+ return methodClassMap;
+ }
+
/**
* invoke Method
*
diff --git a/compatible/src/main/java/io/seata/rm/tcc/interceptor/parser/TccActionInterceptorParser.java b/compatible/src/main/java/io/seata/rm/tcc/interceptor/parser/TccActionInterceptorParser.java
index 3d929757e83..7d7e24f661d 100644
--- a/compatible/src/main/java/io/seata/rm/tcc/interceptor/parser/TccActionInterceptorParser.java
+++ b/compatible/src/main/java/io/seata/rm/tcc/interceptor/parser/TccActionInterceptorParser.java
@@ -16,17 +16,22 @@
*/
package io.seata.rm.tcc.interceptor.parser;
-import java.lang.reflect.Method;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.Set;
-
+import io.seata.rm.tcc.api.BusinessActionContext;
+import io.seata.rm.tcc.api.BusinessActionContextParameter;
import io.seata.rm.tcc.api.TwoPhaseBusinessAction;
import io.seata.rm.tcc.interceptor.TccActionInterceptorHandler;
+import org.apache.seata.common.exception.FrameworkException;
import org.apache.seata.common.util.ReflectionUtil;
+import org.apache.seata.common.util.StringUtils;
+import org.apache.seata.core.model.Resource;
import org.apache.seata.integration.tx.api.interceptor.handler.ProxyInvocationHandler;
-import org.apache.seata.integration.tx.api.interceptor.parser.DefaultResourceRegisterParser;
+import org.apache.seata.rm.tcc.TCCResource;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
/**
* The type Tcc action interceptor parser.
@@ -36,40 +41,72 @@ public class TccActionInterceptorParser extends org.apache.seata.rm.tcc.intercep
@Override
public ProxyInvocationHandler parserInterfaceToProxy(Object target, String objectName) {
- // eliminate the bean without two phase annotation.
- Set methodsToProxy = this.tccProxyTargetMethod(target);
+ Map> methodClassMap = ReflectionUtil.findMatchMethodClazzMap(target.getClass(), method -> method.isAnnotationPresent(getAnnotationClass()));
+ Set methodsToProxy = methodClassMap.keySet();
if (methodsToProxy.isEmpty()) {
return null;
}
+
// register resource and enhance with interceptor
- DefaultResourceRegisterParser.get().registerResource(target, objectName);
- return new TccActionInterceptorHandler(target, methodsToProxy);
+ registerResource(target, methodClassMap);
+
+ return new TccActionInterceptorHandler(target, methodsToProxy.stream().map(Method::getName).collect(Collectors.toSet()));
}
- private Set tccProxyTargetMethod(Object target) {
- Set methodsToProxy = new HashSet<>();
- //check if it is TCC bean
- Class> tccServiceClazz = target.getClass();
- Set methods = new HashSet<>(Arrays.asList(tccServiceClazz.getMethods()));
- Set> interfaceClasses = ReflectionUtil.getInterfaces(tccServiceClazz);
- if (interfaceClasses != null) {
- for (Class> interClass : interfaceClasses) {
- methods.addAll(Arrays.asList(interClass.getMethods()));
- }
- }
+ @Override
+ protected Class extends Annotation> getAnnotationClass() {
+ return TwoPhaseBusinessAction.class;
+ }
- TwoPhaseBusinessAction twoPhaseBusinessAction;
- for (Method method : methods) {
- twoPhaseBusinessAction = method.getAnnotation(TwoPhaseBusinessAction.class);
- if (twoPhaseBusinessAction != null) {
- methodsToProxy.add(method.getName());
- }
+ protected Resource createResource(Object target, Class> targetServiceClass, Method m, Annotation annotation) throws NoSuchMethodException {
+ TwoPhaseBusinessAction twoPhaseBusinessAction = (TwoPhaseBusinessAction) annotation;
+ TCCResource tccResource = new TCCResource();
+ if (StringUtils.isBlank(twoPhaseBusinessAction.name())) {
+ throw new FrameworkException("TCC bean name cannot be null or empty");
}
+ tccResource.setActionName(twoPhaseBusinessAction.name());
+ tccResource.setTargetBean(target);
+ tccResource.setPrepareMethod(m);
+ tccResource.setCommitMethodName(twoPhaseBusinessAction.commitMethod());
+ tccResource.setCommitMethod(targetServiceClass.getMethod(twoPhaseBusinessAction.commitMethod(),
+ twoPhaseBusinessAction.commitArgsClasses()));
+ tccResource.setRollbackMethodName(twoPhaseBusinessAction.rollbackMethod());
+ tccResource.setRollbackMethod(targetServiceClass.getMethod(twoPhaseBusinessAction.rollbackMethod(),
+ twoPhaseBusinessAction.rollbackArgsClasses()));
+ // set argsClasses
+ tccResource.setCommitArgsClasses(twoPhaseBusinessAction.commitArgsClasses());
+ tccResource.setRollbackArgsClasses(twoPhaseBusinessAction.rollbackArgsClasses());
+ // set phase two method's keys
+ tccResource.setPhaseTwoCommitKeys(this.getTwoPhaseArgs(tccResource.getCommitMethod(),
+ twoPhaseBusinessAction.commitArgsClasses()));
+ tccResource.setPhaseTwoRollbackKeys(this.getTwoPhaseArgs(tccResource.getRollbackMethod(),
+ twoPhaseBusinessAction.rollbackArgsClasses()));
+ return tccResource;
+ }
- if (methodsToProxy.isEmpty()) {
- return Collections.emptySet();
+ protected String[] getTwoPhaseArgs(Method method, Class>[] argsClasses) {
+ Annotation[][] parameterAnnotations = method.getParameterAnnotations();
+ String[] keys = new String[parameterAnnotations.length];
+ /*
+ * get parameter's key
+ * if method's parameter list is like
+ * (BusinessActionContext, @BusinessActionContextParameter("a") A a, @BusinessActionContextParameter("b") B b)
+ * the keys will be [null, a, b]
+ */
+ for (int i = 0; i < parameterAnnotations.length; i++) {
+ for (int j = 0; j < parameterAnnotations[i].length; j++) {
+ if (parameterAnnotations[i][j] instanceof BusinessActionContextParameter) {
+ BusinessActionContextParameter param = (BusinessActionContextParameter) parameterAnnotations[i][j];
+ String key = io.seata.integration.tx.api.interceptor.ActionContextUtil.getParamNameFromAnnotation(param);
+ keys[i] = key;
+ break;
+ }
+ }
+ if (keys[i] == null && !(argsClasses[i].equals(BusinessActionContext.class))) {
+ throw new IllegalArgumentException("non-BusinessActionContext parameter should use annotation " +
+ "BusinessActionContextParameter");
+ }
}
- // sofa:reference / dubbo:reference, AOP
- return methodsToProxy;
+ return keys;
}
-}
+}
\ No newline at end of file
diff --git a/compatible/src/main/java/io/seata/rm/tcc/resource/parser/TccRegisterResourceParser.java b/compatible/src/main/java/io/seata/rm/tcc/resource/parser/TccRegisterResourceParser.java
deleted file mode 100644
index 53168ff6bc2..00000000000
--- a/compatible/src/main/java/io/seata/rm/tcc/resource/parser/TccRegisterResourceParser.java
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You 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 io.seata.rm.tcc.resource.parser;
-
-import java.lang.annotation.Annotation;
-import java.lang.reflect.Method;
-import java.util.Set;
-
-import io.seata.integration.tx.api.interceptor.ActionContextUtil;
-import io.seata.rm.tcc.api.BusinessActionContext;
-import io.seata.rm.tcc.api.BusinessActionContextParameter;
-import io.seata.rm.tcc.api.TwoPhaseBusinessAction;
-import org.apache.seata.common.exception.FrameworkException;
-import org.apache.seata.common.util.StringUtils;
-import org.apache.seata.rm.DefaultResourceManager;
-import org.apache.seata.rm.tcc.TCCResource;
-
-/**
- * The type Tcc register resource parser.
- */
-@Deprecated
-public class TccRegisterResourceParser extends org.apache.seata.rm.tcc.resource.parser.TccRegisterResourceParser {
-
- protected void executeRegisterResource(Object target, Set methods, Class> targetServiceClass) throws NoSuchMethodException {
- for (Method m : methods) {
- TwoPhaseBusinessAction twoPhaseBusinessAction = m.getAnnotation(TwoPhaseBusinessAction.class);
- if (twoPhaseBusinessAction != null) {
- TCCResource tccResource = new TCCResource();
- if (StringUtils.isBlank(twoPhaseBusinessAction.name())) {
- throw new FrameworkException("TCC bean name cannot be null or empty");
- }
- tccResource.setActionName(twoPhaseBusinessAction.name());
- tccResource.setTargetBean(target);
- tccResource.setPrepareMethod(m);
- tccResource.setCommitMethodName(twoPhaseBusinessAction.commitMethod());
- tccResource.setCommitMethod(targetServiceClass.getMethod(twoPhaseBusinessAction.commitMethod(),
- twoPhaseBusinessAction.commitArgsClasses()));
- tccResource.setRollbackMethodName(twoPhaseBusinessAction.rollbackMethod());
- tccResource.setRollbackMethod(targetServiceClass.getMethod(twoPhaseBusinessAction.rollbackMethod(),
- twoPhaseBusinessAction.rollbackArgsClasses()));
- // set argsClasses
- tccResource.setCommitArgsClasses(twoPhaseBusinessAction.commitArgsClasses());
- tccResource.setRollbackArgsClasses(twoPhaseBusinessAction.rollbackArgsClasses());
- // set phase two method's keys
- tccResource.setPhaseTwoCommitKeys(getTwoPhaseArgs(tccResource.getCommitMethod(),
- twoPhaseBusinessAction.commitArgsClasses()));
- tccResource.setPhaseTwoRollbackKeys(getTwoPhaseArgs(tccResource.getRollbackMethod(),
- twoPhaseBusinessAction.rollbackArgsClasses()));
- //registry tcc resource
- DefaultResourceManager.get().registerResource(tccResource);
- }
- }
- }
-
- @Override
- protected String[] getTwoPhaseArgs(Method method, Class>[] argsClasses) {
- Annotation[][] parameterAnnotations = method.getParameterAnnotations();
- String[] keys = new String[parameterAnnotations.length];
- /*
- * get parameter's key
- * if method's parameter list is like
- * (BusinessActionContext, @BusinessActionContextParameter("a") A a, @BusinessActionContextParameter("b") B b)
- * the keys will be [null, a, b]
- */
- for (int i = 0; i < parameterAnnotations.length; i++) {
- for (int j = 0; j < parameterAnnotations[i].length; j++) {
- if (parameterAnnotations[i][j] instanceof BusinessActionContextParameter) {
- BusinessActionContextParameter param = (BusinessActionContextParameter) parameterAnnotations[i][j];
- String key = ActionContextUtil.getParamNameFromAnnotation(param);
- keys[i] = key;
- break;
- }
- }
- if (keys[i] == null && !(argsClasses[i].equals(BusinessActionContext.class))) {
- throw new IllegalArgumentException("non-BusinessActionContext parameter should use annotation " +
- "BusinessActionContextParameter");
- }
- }
- return keys;
- }
-
-}
diff --git a/core/src/main/java/org/apache/seata/core/model/BranchType.java b/core/src/main/java/org/apache/seata/core/model/BranchType.java
index f3a868f93d3..37ae5342b75 100644
--- a/core/src/main/java/org/apache/seata/core/model/BranchType.java
+++ b/core/src/main/java/org/apache/seata/core/model/BranchType.java
@@ -38,6 +38,11 @@ public enum BranchType {
*/
SAGA,
+ /**
+ * The SAGA_ANNOTATION.
+ */
+ SAGA_ANNOTATION,
+
/**
* The XA.
*/
diff --git a/integration-tx-api/src/main/java/org/apache/seata/integration/tx/api/interceptor/ActionContextUtil.java b/integration-tx-api/src/main/java/org/apache/seata/integration/tx/api/interceptor/ActionContextUtil.java
index ad6b7be469d..914ef9d7340 100644
--- a/integration-tx-api/src/main/java/org/apache/seata/integration/tx/api/interceptor/ActionContextUtil.java
+++ b/integration-tx-api/src/main/java/org/apache/seata/integration/tx/api/interceptor/ActionContextUtil.java
@@ -29,7 +29,9 @@
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
+import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
+import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -37,7 +39,6 @@
/**
* Extracting TCC Context from Method
- *
*/
public final class ActionContextUtil {
@@ -97,7 +98,7 @@ public static Map fetchContextFromObject(@Nonnull Object targetP
* @param actionContext the action context
*/
public static void loadParamByAnnotationAndPutToContext(@Nonnull final ParamType paramType, @Nonnull String paramName, Object paramValue,
- @Nonnull final BusinessActionContextParameter annotation, @Nonnull final Map actionContext) {
+ @Nonnull final BusinessActionContextParameter annotation, @Nonnull final Map actionContext) {
if (paramValue == null) {
return;
}
@@ -131,7 +132,7 @@ public static void loadParamByAnnotationAndPutToContext(@Nonnull final ParamType
public static Object getByIndex(@Nonnull ParamType paramType, @Nonnull String paramName, @Nonnull Object paramValue, int index) {
if (paramValue instanceof List) {
@SuppressWarnings("unchecked")
- List
-
com.h2database
h2
@@ -183,6 +182,12 @@
rocketmq-client
test
+
+ org.apache.seata
+ seata-saga-annotation
+ ${project.version}
+ test
+
diff --git a/test/src/test/java/org/apache/seata/core/rpc/netty/mockserver/RmClientTest.java b/test/src/test/java/org/apache/seata/core/rpc/netty/mockserver/RmClientTest.java
index c7f100bce74..c9bd6d0f941 100644
--- a/test/src/test/java/org/apache/seata/core/rpc/netty/mockserver/RmClientTest.java
+++ b/test/src/test/java/org/apache/seata/core/rpc/netty/mockserver/RmClientTest.java
@@ -17,6 +17,9 @@
package org.apache.seata.core.rpc.netty.mockserver;
import io.netty.channel.Channel;
+import org.apache.seata.common.exception.FrameworkException;
+import org.apache.seata.common.util.ReflectionUtil;
+import org.apache.seata.common.util.StringUtils;
import org.apache.seata.core.context.RootContext;
import org.apache.seata.core.exception.TransactionException;
import org.apache.seata.core.model.BranchStatus;
@@ -24,13 +27,17 @@
import org.apache.seata.core.protocol.HeartbeatMessage;
import org.apache.seata.core.rpc.netty.ChannelManagerTestHelper;
import org.apache.seata.core.rpc.netty.RmNettyRemotingClient;
-import org.apache.seata.integration.tx.api.interceptor.parser.DefaultResourceRegisterParser;
+import org.apache.seata.integration.tx.api.interceptor.ActionContextUtil;
import org.apache.seata.rm.DefaultResourceManager;
import org.apache.seata.rm.RMClient;
+import org.apache.seata.rm.tcc.TCCResource;
+import org.apache.seata.rm.tcc.api.TwoPhaseBusinessAction;
import org.junit.jupiter.api.Assertions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.lang.reflect.Method;
+import java.util.Map;
import java.util.concurrent.ConcurrentMap;
/**
@@ -76,10 +83,55 @@ public static DefaultResourceManager getRm(String resourceId) {
//register:TYPE_REG_RM = 103 , TYPE_REG_RM_RESULT = 104
Action1 target = new Action1Impl();
- DefaultResourceRegisterParser.get().registerResource(target, resourceId);
+ registryTccResource(target);
LOGGER.info("registerResource ok");
return rm;
}
+ /**
+ * only compatible history ut
+ * TODO fix
+ */
+ @Deprecated
+ private static void registryTccResource(Action1 target) {
+ Map> matchMethodClazzMap = ReflectionUtil.findMatchMethodClazzMap(target.getClass(), method -> method.isAnnotationPresent(TwoPhaseBusinessAction.class));
+ if (matchMethodClazzMap.keySet().isEmpty()) {
+ return;
+ }
+
+ try {
+ for (Map.Entry> methodClassEntry : matchMethodClazzMap.entrySet()) {
+ Method method = methodClassEntry.getKey();
+ Class> methodClass = methodClassEntry.getValue();
+
+ TwoPhaseBusinessAction twoPhaseBusinessAction = method.getAnnotation(TwoPhaseBusinessAction.class);
+ TCCResource tccResource = new TCCResource();
+ if (StringUtils.isBlank(twoPhaseBusinessAction.name())) {
+ throw new FrameworkException("TCC bean name cannot be null or empty");
+ }
+ tccResource.setActionName(twoPhaseBusinessAction.name());
+ tccResource.setTargetBean(target);
+ tccResource.setPrepareMethod(method);
+ tccResource.setCommitMethodName(twoPhaseBusinessAction.commitMethod());
+ tccResource.setCommitMethod(methodClass.getMethod(twoPhaseBusinessAction.commitMethod(),
+ twoPhaseBusinessAction.commitArgsClasses()));
+ tccResource.setRollbackMethodName(twoPhaseBusinessAction.rollbackMethod());
+ tccResource.setRollbackMethod(methodClass.getMethod(twoPhaseBusinessAction.rollbackMethod(),
+ twoPhaseBusinessAction.rollbackArgsClasses()));
+ // set argsClasses
+ tccResource.setCommitArgsClasses(twoPhaseBusinessAction.commitArgsClasses());
+ tccResource.setRollbackArgsClasses(twoPhaseBusinessAction.rollbackArgsClasses());
+ // set phase two method's keys
+ tccResource.setPhaseTwoCommitKeys(ActionContextUtil.getTwoPhaseArgs(tccResource.getCommitMethod(),
+ twoPhaseBusinessAction.commitArgsClasses()));
+ tccResource.setPhaseTwoRollbackKeys(ActionContextUtil.getTwoPhaseArgs(tccResource.getRollbackMethod(),
+ twoPhaseBusinessAction.rollbackArgsClasses()));
+ DefaultResourceManager.get().registerResource(tccResource);
+ }
+ } catch (Throwable t) {
+ throw new FrameworkException(t, "register tcc resource error");
+ }
+ }
+
}
diff --git a/test/src/test/java/org/apache/seata/saga/annotation/BranchSessionMock.java b/test/src/test/java/org/apache/seata/saga/annotation/BranchSessionMock.java
new file mode 100644
index 00000000000..26fc1c0a962
--- /dev/null
+++ b/test/src/test/java/org/apache/seata/saga/annotation/BranchSessionMock.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.seata.saga.annotation;
+
+
+import org.apache.seata.core.model.BranchType;
+
+public class BranchSessionMock {
+
+ private String xid;
+
+ private long branchId;
+
+ private String resourceGroupId;
+
+ private String resourceId;
+
+
+ private BranchType branchType;
+
+
+ private String applicationData;
+
+
+ public String getXid() {
+ return xid;
+ }
+
+ public void setXid(String xid) {
+ this.xid = xid;
+ }
+
+ public long getBranchId() {
+ return branchId;
+ }
+
+ public void setBranchId(long branchId) {
+ this.branchId = branchId;
+ }
+
+ public String getResourceGroupId() {
+ return resourceGroupId;
+ }
+
+ public void setResourceGroupId(String resourceGroupId) {
+ this.resourceGroupId = resourceGroupId;
+ }
+
+ public String getResourceId() {
+ return resourceId;
+ }
+
+ public void setResourceId(String resourceId) {
+ this.resourceId = resourceId;
+ }
+
+ public BranchType getBranchType() {
+ return branchType;
+ }
+
+ public void setBranchType(BranchType branchType) {
+ this.branchType = branchType;
+ }
+
+ public String getApplicationData() {
+ return applicationData;
+ }
+
+ public void setApplicationData(String applicationData) {
+ this.applicationData = applicationData;
+ }
+}
\ No newline at end of file
diff --git a/test/src/test/java/org/apache/seata/saga/annotation/NormalSagaAnnotationAction.java b/test/src/test/java/org/apache/seata/saga/annotation/NormalSagaAnnotationAction.java
new file mode 100644
index 00000000000..273696071a1
--- /dev/null
+++ b/test/src/test/java/org/apache/seata/saga/annotation/NormalSagaAnnotationAction.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.seata.saga.annotation;
+
+import org.apache.seata.rm.tcc.api.BusinessActionContext;
+import org.apache.seata.rm.tcc.api.BusinessActionContextParameter;
+import org.apache.seata.rm.tcc.api.LocalTCC;
+import org.apache.seata.saga.rm.api.CompensationBusinessAction;
+
+import java.util.List;
+
+/**
+ * The interface saga action.
+ */
+@LocalTCC
+public interface NormalSagaAnnotationAction {
+
+ /**
+ * Prepare boolean.
+ *
+ * @param actionContext the action context
+ * @param a the a
+ * @param b the b
+ * @param sagaParam the saga param
+ * @return the boolean
+ */
+ @CompensationBusinessAction(name = "sagaActionForTest", compensationMethod = "compensation", compensationArgsClasses = {BusinessActionContext.class, SagaParam.class})
+ boolean commit(BusinessActionContext actionContext,
+ @BusinessActionContextParameter("a") int a,
+ @BusinessActionContextParameter(paramName = "b", index = 0) List b,
+ @BusinessActionContextParameter(isParamInProperty = true) SagaParam sagaParam);
+
+ /**
+ * Rollback boolean.
+ *
+ * @param actionContext the action context
+ * @return the boolean
+ */
+ boolean compensation(BusinessActionContext actionContext, @BusinessActionContextParameter("sagaParam") SagaParam param);
+}
\ No newline at end of file
diff --git a/test/src/test/java/org/apache/seata/saga/annotation/NormalSagaAnnotationActionImpl.java b/test/src/test/java/org/apache/seata/saga/annotation/NormalSagaAnnotationActionImpl.java
new file mode 100644
index 00000000000..4cbdf781e98
--- /dev/null
+++ b/test/src/test/java/org/apache/seata/saga/annotation/NormalSagaAnnotationActionImpl.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.seata.saga.annotation;
+
+import java.util.List;
+import org.apache.seata.rm.tcc.api.BusinessActionContext;
+
+/**
+ *
+ */
+public class NormalSagaAnnotationActionImpl implements NormalSagaAnnotationAction {
+
+ private boolean isCommit;
+
+
+ @Override
+ public boolean commit(BusinessActionContext actionContext, int a, List b, SagaParam sagaParam) {
+ isCommit = true;
+ return a > 1;
+ }
+
+ @Override
+ public boolean compensation(BusinessActionContext actionContext, SagaParam param) {
+ isCommit = false;
+ return true;
+ }
+
+ public boolean isCommit() {
+ return isCommit;
+ }
+}
\ No newline at end of file
diff --git a/test/src/test/java/org/apache/seata/saga/annotation/SagaParam.java b/test/src/test/java/org/apache/seata/saga/annotation/SagaParam.java
new file mode 100644
index 00000000000..63ee611e3a7
--- /dev/null
+++ b/test/src/test/java/org/apache/seata/saga/annotation/SagaParam.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.seata.saga.annotation;
+
+import org.apache.seata.rm.tcc.api.BusinessActionContextParameter;
+
+/**
+ * The type param.
+ */
+public class SagaParam {
+
+ /**
+ * The Num.
+ */
+ protected int num;
+
+ /**
+ * The Email.
+ */
+ @BusinessActionContextParameter(paramName = "email")
+ protected String email;
+
+ /**
+ * Instantiates a new param.
+ *
+ * @param num the num
+ * @param email the email
+ */
+ public SagaParam(int num, String email) {
+ this.num = num;
+ this.email = email;
+ }
+
+ /**
+ * Gets num.
+ *
+ * @return the num
+ */
+ public int getNum() {
+ return num;
+ }
+
+ /**
+ * Sets num.
+ *
+ * @param num the num
+ */
+ public void setNum(int num) {
+ this.num = num;
+ }
+}
\ No newline at end of file
diff --git a/test/src/test/java/org/apache/seata/saga/annotation/rm/interceptor/parser/SagaActionInterceptorParserTest.java b/test/src/test/java/org/apache/seata/saga/annotation/rm/interceptor/parser/SagaActionInterceptorParserTest.java
new file mode 100644
index 00000000000..ca6f2317da1
--- /dev/null
+++ b/test/src/test/java/org/apache/seata/saga/annotation/rm/interceptor/parser/SagaActionInterceptorParserTest.java
@@ -0,0 +1,210 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.seata.saga.annotation.rm.interceptor.parser;
+
+import org.apache.seata.core.exception.TransactionException;
+import org.apache.seata.core.model.BranchType;
+import org.apache.seata.core.model.GlobalStatus;
+import org.apache.seata.core.model.ResourceManager;
+import org.apache.seata.core.model.TransactionManager;
+import org.apache.seata.integration.tx.api.interceptor.handler.ProxyInvocationHandler;
+import org.apache.seata.integration.tx.api.util.ProxyUtil;
+import org.apache.seata.rm.DefaultResourceManager;
+import org.apache.seata.saga.annotation.BranchSessionMock;
+import org.apache.seata.saga.annotation.NormalSagaAnnotationActionImpl;
+import org.apache.seata.saga.annotation.SagaParam;
+import org.apache.seata.saga.rm.SagaAnnotationResourceManager;
+import org.apache.seata.saga.rm.interceptor.parser.SagaAnnotationActionInterceptorParser;
+import org.apache.seata.tm.TransactionManagerHolder;
+import org.apache.seata.tm.api.GlobalTransaction;
+import org.apache.seata.tm.api.GlobalTransactionContext;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ *
+ */
+public class SagaActionInterceptorParserTest {
+
+ public static String DEFAULT_XID = "default_xid";
+
+ @BeforeAll
+ public static void init() throws IOException {
+ System.setProperty("config.type", "file");
+ System.setProperty("config.file.name", "file.conf");
+ System.setProperty("txServiceGroup", "default_tx_group");
+ System.setProperty("service.vgroupMapping.default_tx_group", "default");
+ }
+
+ @AfterEach
+ public void clearTccResource() {
+ DefaultResourceManager.get().getResourceManager(BranchType.SAGA_ANNOTATION).getManagedResources().clear();
+ }
+
+ @Test
+ void parserInterfaceToProxy() {
+ NormalSagaAnnotationActionImpl sagaAction = new NormalSagaAnnotationActionImpl();
+
+ SagaAnnotationActionInterceptorParser sagaAnnotationActionInterceptorParser = new SagaAnnotationActionInterceptorParser();
+
+ ProxyInvocationHandler proxyInvocationHandler = sagaAnnotationActionInterceptorParser.parserInterfaceToProxy(sagaAction, "sagaAction");
+ Assertions.assertNotNull(proxyInvocationHandler);
+ }
+
+
+ @Test
+ public void testSagaAnnotation_should_commit() throws TransactionException {
+ DefaultResourceManager.get();
+ DefaultResourceManager.mockResourceManager(BranchType.SAGA_ANNOTATION, resourceManager);
+
+ TransactionManagerHolder.set(transactionManager);
+
+ NormalSagaAnnotationActionImpl sagaActionProxy = ProxyUtil.createProxy(new NormalSagaAnnotationActionImpl());
+
+ SagaParam sagaParam = new SagaParam(2, "abc@163.com");
+ List listB = Arrays.asList("b");
+
+ GlobalTransaction tx = GlobalTransactionContext.getCurrentOrCreate();
+
+ try {
+ tx.begin(60000, "testBiz");
+
+ boolean result = sagaActionProxy.commit(null, 2, listB, sagaParam);
+
+ Assertions.assertTrue(result);
+
+ if (result) {
+ tx.commit();
+ } else {
+ tx.rollback();
+ }
+ } catch (Exception exx) {
+ tx.rollback();
+ throw exx;
+ }
+
+ Assertions.assertTrue(sagaActionProxy.isCommit());
+ }
+
+ @Test
+ public void testSagaAnnotation_should_rollback() throws TransactionException {
+ DefaultResourceManager.get();
+ DefaultResourceManager.mockResourceManager(BranchType.SAGA_ANNOTATION, resourceManager);
+
+ TransactionManagerHolder.set(transactionManager);
+
+ NormalSagaAnnotationActionImpl sagaActionProxy = ProxyUtil.createProxy(new NormalSagaAnnotationActionImpl());
+
+ SagaParam sagaParam = new SagaParam(1, "abc@163.com");
+ List listB = Arrays.asList("b");
+
+ GlobalTransaction tx = GlobalTransactionContext.getCurrentOrCreate();
+
+ try {
+ tx.begin(60000, "testBiz");
+
+ boolean result = sagaActionProxy.commit(null, 1, listB, sagaParam);
+
+ Assertions.assertFalse(result);
+
+ if (result) {
+ tx.commit();
+ } else {
+ tx.rollback();
+ }
+ } catch (Exception exx) {
+ tx.rollback();
+ throw exx;
+ }
+
+ Assertions.assertFalse(sagaActionProxy.isCommit());
+ }
+
+ private static Map> applicationDataMap = new ConcurrentHashMap<>();
+
+
+ private static TransactionManager transactionManager = new TransactionManager() {
+ @Override
+ public String begin(String applicationId, String transactionServiceGroup, String name, int timeout) throws TransactionException {
+ return DEFAULT_XID;
+ }
+
+ @Override
+ public GlobalStatus commit(String xid) throws TransactionException {
+ return GlobalStatus.Committed;
+ }
+
+ @Override
+ public GlobalStatus rollback(String xid) throws TransactionException {
+
+ rollbackAll(xid);
+
+ return GlobalStatus.Rollbacked;
+ }
+
+ @Override
+ public GlobalStatus getStatus(String xid) throws TransactionException {
+ return GlobalStatus.Begin;
+ }
+
+ @Override
+ public GlobalStatus globalReport(String xid, GlobalStatus globalStatus) throws TransactionException {
+ return globalStatus;
+ }
+ };
+
+
+ private static ResourceManager resourceManager = new SagaAnnotationResourceManager() {
+
+ @Override
+ public Long branchRegister(BranchType branchType, String resourceId, String clientId, String xid, String applicationData, String lockKeys) throws TransactionException {
+
+ long branchId = System.currentTimeMillis();
+
+ List branches = applicationDataMap.computeIfAbsent(xid, s -> new ArrayList<>());
+ BranchSessionMock branchSessionMock = new BranchSessionMock();
+ branchSessionMock.setXid(xid);
+ branchSessionMock.setBranchType(branchType);
+ branchSessionMock.setResourceId(resourceId);
+ branchSessionMock.setApplicationData(applicationData);
+ branchSessionMock.setBranchId(branchId);
+
+ branches.add(branchSessionMock);
+
+ return branchId;
+ }
+ };
+
+
+ public static void rollbackAll(String xid) throws TransactionException {
+
+ List branches = applicationDataMap.computeIfAbsent(xid, s -> new ArrayList<>());
+ for (BranchSessionMock branch : branches) {
+ resourceManager.branchRollback(branch.getBranchType(), branch.getXid(), branch.getBranchId(), branch.getResourceId(), branch.getApplicationData());
+ }
+ }
+
+}
\ No newline at end of file