This is an automated email from the ASF dual-hosted git repository.
jianbin pushed a commit to branch 2.x
in repository https://gitbox.apache.org/repos/asf/incubator-seata.git
The following commit(s) were added to refs/heads/2.x by this push:
new d859fef4eb optimize: Replace @LocalTCC with @SagaTransactional in the
saga annotation pattern (#7443)
d859fef4eb is described below
commit d859fef4eb8be9e814e69de63538cbd0bed8165f
Author: Eric Wang <[email protected]>
AuthorDate: Wed Sep 3 09:48:58 2025 +0800
optimize: Replace @LocalTCC with @SagaTransactional in the saga annotation
pattern (#7443)
---
changes/en-us/2.x.md | 2 +
changes/zh-cn/2.x.md | 2 +
.../remoting/parser/LocalTCCRemotingParser.java | 2 +
.../saga/rm/api/CompensationBusinessAction.java | 29 ++
.../seata/saga/rm/api/SagaTransactional.java | 103 ++++++
.../parser/SagaTransactionalRemotingParser.java | 155 +++++++++
...eata.integration.tx.api.remoting.RemotingParser | 17 +
.../seata/saga/rm/api/SagaTransactionalTest.java | 233 +++++++++++++
.../SagaTransactionalRemotingParserTest.java | 256 +++++++++++++++
.../java/org/apache/seata/rm/tcc/api/LocalTCC.java | 38 ++-
.../seata/rm/tcc/api/TwoPhaseBusinessAction.java | 9 +-
.../remoting/parser/LocalTCCRemotingParser.java | 85 ++++-
.../parser/LocalTCCRemotingParserTest.java | 215 ++++++++++--
.../saga/annotation/AnnotationConflictTest.java | 364 +++++++++++++++++++++
.../saga/annotation/DualParserIntegrationTest.java | 308 +++++++++++++++++
.../SagaTransactionalAnnotationAction.java | 50 +++
.../SagaTransactionalAnnotationActionImpl.java | 57 ++++
17 files changed, 1872 insertions(+), 53 deletions(-)
diff --git a/changes/en-us/2.x.md b/changes/en-us/2.x.md
index c1b6719f1a..0058d3c60d 100644
--- a/changes/en-us/2.x.md
+++ b/changes/en-us/2.x.md
@@ -49,6 +49,7 @@ Add changes here for all PR submitted to the 2.x branch.
- [[#7608](https://github.com/seata/seata/pull/7608)] modify the parameter
name in refreshToken method
- [[#7603](https://github.com/seata/seata/pull/7603)] upgrade Apache Tomcat
dependency from 9.0.106 to 9.0.108
- [[#7614](https://github.com/seata/seata/pull/7614)] update README.md
+- [[#7443](https://github.com/seata/seata/pull/7443)] Replace @LocalTCC with
@SagaTransactional in the saga annotation pattern
### security:
@@ -88,6 +89,7 @@ Thanks to these contributors for their code commits. Please
report an unintended
- [funky-eyes](https://github.com/funky-eyes)
- [keepConcentration](https://github.com/keepConcentration)
- [sunheyi6](https://github.com/sunheyi6)
+- [WangzJi](https://github.com/WangzJi)
Also, we receive many valuable issues, questions and advices from our
community. Thanks for you all.
diff --git a/changes/zh-cn/2.x.md b/changes/zh-cn/2.x.md
index 49db4360c8..45ba533167 100644
--- a/changes/zh-cn/2.x.md
+++ b/changes/zh-cn/2.x.md
@@ -48,6 +48,7 @@
- [[#7608](https://github.com/seata/seata/pull/7608)] 修改refreshToken方法中的参数名称
- [[#7603](https://github.com/seata/seata/pull/7603)] 将Apache
Tomcat依赖项从9.0.106升级到9.0.108
- [[#7614](https://github.com/seata/seata/pull/7614)] 更新 README.md
+- [[#7443](https://github.com/seata/seata/pull/7443)]
将saga注释模式中的@LocalTCC替换为@SagaTransactional
### security:
@@ -88,6 +89,7 @@
- [funky-eyes](https://github.com/funky-eyes)
- [keepConcentration](https://github.com/keepConcentration)
- [sunheyi6](https://github.com/sunheyi6)
+- [WangzJi](https://github.com/WangzJi)
同时,我们收到了社区反馈的很多有价值的issue和建议,非常感谢大家。
diff --git
a/compatible/src/main/java/io/seata/rm/tcc/remoting/parser/LocalTCCRemotingParser.java
b/compatible/src/main/java/io/seata/rm/tcc/remoting/parser/LocalTCCRemotingParser.java
index ae27f82f4c..65eb5264ef 100644
---
a/compatible/src/main/java/io/seata/rm/tcc/remoting/parser/LocalTCCRemotingParser.java
+++
b/compatible/src/main/java/io/seata/rm/tcc/remoting/parser/LocalTCCRemotingParser.java
@@ -27,6 +27,8 @@ import java.util.Set;
/**
* The type Local tcc remoting parser.
+ * Compatible module maintains backward compatibility with Seata versions
prior to 2.1.
+ * Only supports @LocalTCC annotation.
*/
@Deprecated
public class LocalTCCRemotingParser extends
org.apache.seata.rm.tcc.remoting.parser.LocalTCCRemotingParser {
diff --git
a/saga/seata-saga-annotation/src/main/java/org/apache/seata/saga/rm/api/CompensationBusinessAction.java
b/saga/seata-saga-annotation/src/main/java/org/apache/seata/saga/rm/api/CompensationBusinessAction.java
index 1128154234..8c17eb1d2c 100644
---
a/saga/seata-saga-annotation/src/main/java/org/apache/seata/saga/rm/api/CompensationBusinessAction.java
+++
b/saga/seata-saga-annotation/src/main/java/org/apache/seata/saga/rm/api/CompensationBusinessAction.java
@@ -27,6 +27,35 @@ import java.lang.annotation.Target;
/**
* Saga annotation.
* Define a saga interface, which added on the commit method, if occurs
rollback, compensation will be called.
+ *
+ * When using this annotation for local (non-remote) services, you should also
add @SagaTransactional
+ * annotation on the interface or implementation class to enable proper proxy
enhancement.
+ * This avoids the need to use @LocalTCC annotation in Saga scenarios, which
can be confusing.
+ *
+ * Recommended Usage Pattern:
+ *
+ * @SagaTransactional // Use this instead of @LocalTCC for Saga scenarios
+ * public interface PaymentSagaService {
+ *
+ * @CompensationBusinessAction(compensationMethod = "compensatePayment")
+ * boolean processPayment(BusinessActionContext context, String orderId,
double amount);
+ *
+ * boolean compensatePayment(BusinessActionContext context);
+ * }
+ *
+ * Why Use @SagaTransactional with Saga:
+ * - Semantic clarity: @SagaTransactional indicates general transaction
participation
+ * - Avoids confusion: @LocalTCC specifically implies TCC mode semantics
+ * - Future compatibility: Works with multiple transaction modes
+ * - Better maintainability: Clear intent for Saga compensation patterns
+ *
+ * Legacy Support:
+ * While @LocalTCC still works with Saga scenarios for backward compatibility,
+ * @SagaTransactional is the recommended approach for new implementations.
+ *
+ * @see SagaTransactional Recommended annotation for Saga scenarios
+ * @see org.apache.seata.rm.tcc.api.LocalTCC Legacy annotation (still
supported but not recommended for Saga)
+ * @see org.apache.seata.rm.tcc.api.BusinessActionContext Context parameter
type
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
diff --git
a/saga/seata-saga-annotation/src/main/java/org/apache/seata/saga/rm/api/SagaTransactional.java
b/saga/seata-saga-annotation/src/main/java/org/apache/seata/saga/rm/api/SagaTransactional.java
new file mode 100644
index 0000000000..c301fd5aa1
--- /dev/null
+++
b/saga/seata-saga-annotation/src/main/java/org/apache/seata/saga/rm/api/SagaTransactional.java
@@ -0,0 +1,103 @@
+/*
+ * 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.rm.api;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * SagaTransactional annotation for marking Saga transaction participant beans.
+ *
+ * This annotation is specifically designed for Saga transaction scenarios,
providing
+ * clearer semantic meaning compared to @LocalTCC when used in Saga
compensation patterns.
+ * It enables proper proxy enhancement and integration with Saga transaction
management.
+ *
+ * Purpose:
+ * - Marks services that participate in Saga transactions
+ * - Enables automatic proxy creation for compensation action support
+ * - Provides clear semantic separation from TCC-specific functionality
+ * - Supports both interface and implementation class annotation
+ *
+ * Key Benefits over @LocalTCC in Saga scenarios:
+ * 1. Semantic Clarity: "Saga" clearly indicates compensation-based
transaction mode
+ * 2. Purpose-built: Designed specifically for Saga patterns, not retrofitted
from TCC
+ * 3. Future-proof: Can evolve independently to support Saga-specific features
+ * 4. Maintainability: Makes codebase intentions clearer for developers
+ *
+ * Typical Usage Pattern:
+ *
+ * @SagaTransactional
+ * public interface OrderSagaService {
+ *
+ * @CompensationBusinessAction(
+ * name = "createOrder",
+ * compensationMethod = "cancelOrder"
+ * )
+ * OrderResult createOrder(BusinessActionContext context,
CreateOrderRequest request);
+ *
+ * boolean cancelOrder(BusinessActionContext context);
+ * }
+ *
+ * Advanced Usage with Multiple Actions:
+ *
+ * @SagaTransactional
+ * public interface PaymentSagaService {
+ *
+ * @CompensationBusinessAction(name = "reserveAmount", compensationMethod
= "releaseAmount")
+ * boolean reserveAmount(BusinessActionContext context, String accountId,
BigDecimal amount);
+ *
+ * @CompensationBusinessAction(name = "deductAmount", compensationMethod =
"refundAmount")
+ * boolean deductAmount(BusinessActionContext context, String accountId,
BigDecimal amount);
+ *
+ * boolean releaseAmount(BusinessActionContext context);
+ * boolean refundAmount(BusinessActionContext context);
+ * }
+ *
+ * Annotation Placement:
+ * - Interface level: Recommended for service contracts
+ * - Implementation class level: Alternative for concrete classes
+ * - Inheritance: Annotations are inherited from parent classes/interfaces
+ *
+ * Integration with Spring Framework:
+ * This annotation works seamlessly with Spring's component scanning and proxy
mechanisms.
+ * Services annotated with @SagaTransactional will be automatically detected
and enhanced
+ * by Seata's runtime infrastructure.
+ *
+ * Backward Compatibility:
+ * - Existing @LocalTCC annotations continue to work unchanged
+ * - Both annotations can coexist in the same application
+ * - No migration is required for existing TCC implementations
+ * - New Saga implementations should prefer @SagaTransactional
+ *
+ * Performance Characteristics:
+ * - Zero runtime overhead compared to @LocalTCC
+ * - Efficient annotation scanning with caching
+ * - Optimized for high-throughput Saga scenarios
+ *
+ * @see org.apache.seata.saga.rm.api.CompensationBusinessAction Primary
annotation for defining compensation actions
+ * @see org.apache.seata.rm.tcc.api.LocalTCC Legacy TCC-specific annotation
(still supported)
+ * @see org.apache.seata.rm.tcc.api.BusinessActionContext Context parameter
passed to compensation methods
+ * @see
org.apache.seata.saga.rm.remoting.parser.SagaTransactionalRemotingParser Parser
that handles this annotation
+ * @since 2.5.0
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+@Inherited
+public @interface SagaTransactional {}
diff --git
a/saga/seata-saga-annotation/src/main/java/org/apache/seata/saga/rm/remoting/parser/SagaTransactionalRemotingParser.java
b/saga/seata-saga-annotation/src/main/java/org/apache/seata/saga/rm/remoting/parser/SagaTransactionalRemotingParser.java
new file mode 100644
index 0000000000..c261a8141a
--- /dev/null
+++
b/saga/seata-saga-annotation/src/main/java/org/apache/seata/saga/rm/remoting/parser/SagaTransactionalRemotingParser.java
@@ -0,0 +1,155 @@
+/*
+ * 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.rm.remoting.parser;
+
+import org.apache.seata.common.exception.FrameworkException;
+import org.apache.seata.common.util.ReflectionUtil;
+import org.apache.seata.integration.tx.api.remoting.Protocols;
+import org.apache.seata.integration.tx.api.remoting.RemotingDesc;
+import
org.apache.seata.integration.tx.api.remoting.parser.AbstractedRemotingParser;
+import org.apache.seata.saga.rm.api.SagaTransactional;
+
+import java.util.Set;
+
+/**
+ * Remoting parser for Saga transaction participant beans with
@SagaTransactional annotation
+ *
+ * This parser is specifically designed for Saga transaction mode and handles
beans annotated
+ * with @SagaTransactional annotation. It provides proper service detection
and proxy enhancement
+ * for Saga compensation scenarios.
+ *
+ * Key Features:
+ * - Dedicated support for @SagaTransactional annotation
+ * - Optimized for Saga compensation patterns
+ * - Clear semantic separation from TCC mode
+ * - Proper integration with CompensationBusinessAction
+ *
+ * Usage Pattern:
+ * @SagaTransactional
+ * public interface PaymentSagaService {
+ * @CompensationBusinessAction(compensationMethod = "compensatePayment")
+ * boolean processPayment(BusinessActionContext context, String orderId,
double amount);
+ *
+ * boolean compensatePayment(BusinessActionContext context);
+ * }
+ *
+ * Detection Priority:
+ * 1. Implementation class annotations (higher priority)
+ * 2. Interface annotations (fallback)
+ *
+ * Performance Considerations:
+ * - Annotation detection is cached appropriately for high-throughput scenarios
+ * - Reflection-based scanning is optimized for typical usage patterns
+ *
+ * @see SagaTransactional The annotation this parser handles
+ * @see org.apache.seata.saga.rm.api.CompensationBusinessAction Commonly used
with @SagaTransactional
+ * @see
org.apache.seata.integration.tx.api.remoting.parser.AbstractedRemotingParser
Base class
+ * @since 2.5.0
+ */
+public class SagaTransactionalRemotingParser extends AbstractedRemotingParser {
+
+ @Override
+ public boolean isReference(Object bean, String beanName) {
+ return isSagaTransactional(bean);
+ }
+
+ @Override
+ public boolean isService(Object bean, String beanName) {
+ return isSagaTransactional(bean);
+ }
+
+ @Override
+ public boolean isService(Class<?> beanClass) throws FrameworkException {
+ return isSagaTransactional(beanClass);
+ }
+
+ @Override
+ public RemotingDesc getServiceDesc(Object bean, String beanName) throws
FrameworkException {
+ if (!this.isRemoting(bean, beanName)) {
+ return null;
+ }
+ RemotingDesc remotingDesc = new RemotingDesc();
+ remotingDesc.setReference(this.isReference(bean, beanName));
+ remotingDesc.setService(this.isService(bean, beanName));
+ remotingDesc.setProtocol(Protocols.IN_JVM);
+ Class<?> classType = bean.getClass();
+
+ // First priority: check if @SagaTransactional is present on the
implementation class itself
+ // Implementation class annotations take precedence over interface
annotations
+ if (hasSagaTransactionalAnnotation(classType)) {
+ remotingDesc.setServiceClass(classType);
+ remotingDesc.setServiceClassName(classType.getName());
+ remotingDesc.setTargetBean(bean);
+ return remotingDesc;
+ }
+
+ // Second priority: check if @SagaTransactional is present on any
implemented interfaces
+ // Fall back to interface annotations if no implementation class
annotations found
+ Set<Class<?>> interfaceClasses =
ReflectionUtil.getInterfaces(classType);
+ for (Class<?> interClass : interfaceClasses) {
+ if (hasSagaTransactionalAnnotation(interClass)) {
+ remotingDesc.setServiceClassName(interClass.getName());
+ remotingDesc.setServiceClass(interClass);
+ remotingDesc.setTargetBean(bean);
+ return remotingDesc;
+ }
+ }
+ throw new FrameworkException("Couldn't parse any Remoting info for
SagaTransactional bean");
+ }
+
+ @Override
+ public short getProtocol() {
+ return Protocols.IN_JVM;
+ }
+
+ /**
+ * Check if the given bean is annotated with @SagaTransactional annotation
+ *
+ * @param bean the bean to check
+ * @return true if the bean or its interfaces have @SagaTransactional
annotation
+ */
+ private boolean isSagaTransactional(Object bean) {
+ return isSagaTransactional(bean.getClass());
+ }
+
+ /**
+ * Check if the given class or its interfaces are annotated with
@SagaTransactional annotation
+ *
+ * @param classType the class type to check
+ * @return true if the class has @SagaTransactional annotation
+ */
+ private boolean isSagaTransactional(Class<?> classType) {
+ // Check the class itself first for better performance
+ if (hasSagaTransactionalAnnotation(classType)) {
+ return true;
+ }
+
+ // Check all interfaces
+ Set<Class<?>> interfaceClasses =
ReflectionUtil.getInterfaces(classType);
+ return
interfaceClasses.stream().anyMatch(this::hasSagaTransactionalAnnotation);
+ }
+
+ /**
+ * Check if a class has @SagaTransactional annotation
+ *
+ * @param clazz the class to check
+ * @return true if the class has @SagaTransactional annotation
+ */
+ private boolean hasSagaTransactionalAnnotation(Class<?> clazz) {
+ return clazz.isAnnotationPresent(SagaTransactional.class);
+ }
+}
diff --git
a/saga/seata-saga-annotation/src/main/resources/META-INF/services/org.apache.seata.integration.tx.api.remoting.RemotingParser
b/saga/seata-saga-annotation/src/main/resources/META-INF/services/org.apache.seata.integration.tx.api.remoting.RemotingParser
new file mode 100644
index 0000000000..5f47ddbcbe
--- /dev/null
+++
b/saga/seata-saga-annotation/src/main/resources/META-INF/services/org.apache.seata.integration.tx.api.remoting.RemotingParser
@@ -0,0 +1,17 @@
+#
+# 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.
+#
+org.apache.seata.saga.rm.remoting.parser.SagaTransactionalRemotingParser
\ No newline at end of file
diff --git
a/saga/seata-saga-annotation/src/test/java/org/apache/seata/saga/rm/api/SagaTransactionalTest.java
b/saga/seata-saga-annotation/src/test/java/org/apache/seata/saga/rm/api/SagaTransactionalTest.java
new file mode 100644
index 0000000000..eff8e3c2e0
--- /dev/null
+++
b/saga/seata-saga-annotation/src/test/java/org/apache/seata/saga/rm/api/SagaTransactionalTest.java
@@ -0,0 +1,233 @@
+/*
+ * 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.rm.api;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Unit tests for @SagaTransactional annotation
+ *
+ * This test suite validates the basic behavior and properties of the
@SagaTransactional annotation:
+ *
+ * Key test areas:
+ * 1. Annotation presence detection at runtime
+ * 2. Annotation inheritance behavior with @Inherited
+ * 3. Interface vs implementation annotation handling
+ * 4. Reflection-based annotation access
+ * 5. Service instantiation with annotated classes
+ * 6. Complex inheritance hierarchies
+ *
+ * These tests ensure that @SagaTransactional behaves correctly in runtime
environments
+ * and maintains compatibility with transaction processing frameworks.
+ */
+public class SagaTransactionalTest {
+
+ // Test classes for annotation testing
+ @SagaTransactional
+ public static class AccountService {
+ public boolean debit(String accountId, double amount) {
+ return amount > 0;
+ }
+
+ public boolean credit(String accountId, double amount) {
+ return amount > 0;
+ }
+ }
+
+ @SagaTransactional
+ public interface PaymentService {
+ boolean processPayment(String orderId, double amount);
+
+ boolean refundPayment(String orderId, double amount);
+ }
+
+ public static class PaymentServiceImpl implements PaymentService {
+ @Override
+ public boolean processPayment(String orderId, double amount) {
+ return orderId != null && amount > 0;
+ }
+
+ @Override
+ public boolean refundPayment(String orderId, double amount) {
+ return orderId != null && amount > 0;
+ }
+ }
+
+ public static class NoAnnotationService {
+ public boolean doSomething() {
+ return true;
+ }
+ }
+
+ @Test
+ public void testSagaTransactionalAnnotationExists() {
+ // Verify the annotation exists and can be used
+
assertTrue(AccountService.class.isAnnotationPresent(SagaTransactional.class));
+
assertTrue(PaymentService.class.isAnnotationPresent(SagaTransactional.class));
+
assertFalse(NoAnnotationService.class.isAnnotationPresent(SagaTransactional.class));
+ }
+
+ @Test
+ public void testSagaTransactionalAnnotationProperties() throws Exception {
+ SagaTransactional annotation =
AccountService.class.getAnnotation(SagaTransactional.class);
+ assertNotNull(annotation);
+
+ // Verify annotation type
+ assertEquals(SagaTransactional.class, annotation.annotationType());
+ }
+
+ @Test
+ public void testAnnotationInheritance() {
+ // Test that @Inherited works properly
+ class InheritedService extends AccountService {
+ public boolean additionalOperation() {
+ return true;
+ }
+ }
+
+ InheritedService inheritedService = new InheritedService();
+
+ // Should inherit the @SagaTransactional annotation
+
assertTrue(inheritedService.getClass().isAnnotationPresent(SagaTransactional.class));
+ }
+
+ @Test
+ public void testAnnotationOnInterface() {
+ PaymentServiceImpl implementation = new PaymentServiceImpl();
+
+ // Check that the interface has the annotation
+
assertTrue(PaymentService.class.isAnnotationPresent(SagaTransactional.class));
+
+ // Check that implementation can access interface annotation
+ for (Class<?> interfaceClass :
implementation.getClass().getInterfaces()) {
+ if (interfaceClass == PaymentService.class) {
+
assertTrue(interfaceClass.isAnnotationPresent(SagaTransactional.class));
+ }
+ }
+ }
+
+ @Test
+ public void testAnnotationRetentionPolicy() throws Exception {
+ // Verify annotation is retained at runtime
+ SagaTransactional annotation =
AccountService.class.getAnnotation(SagaTransactional.class);
+ assertNotNull(annotation);
+
+ // Verify it's available through reflection
+ boolean foundAnnotation = false;
+ for (java.lang.annotation.Annotation ann :
AccountService.class.getAnnotations()) {
+ if (ann instanceof SagaTransactional) {
+ foundAnnotation = true;
+ break;
+ }
+ }
+ assertTrue(foundAnnotation);
+ }
+
+ @Test
+ public void testAnnotationTarget() {
+ // Verify annotation can be applied to types (classes and interfaces)
+
assertTrue(AccountService.class.isAnnotationPresent(SagaTransactional.class));
+
assertTrue(PaymentService.class.isAnnotationPresent(SagaTransactional.class));
+ }
+
+ @Test
+ public void testMultipleServicesWithAnnotation() {
+ List<Class<?>> annotatedClasses = new ArrayList<>();
+ List<Class<?>> testClasses = new ArrayList<>();
+ testClasses.add(AccountService.class);
+ testClasses.add(PaymentService.class);
+ testClasses.add(PaymentServiceImpl.class);
+ testClasses.add(NoAnnotationService.class);
+
+ for (Class<?> clazz : testClasses) {
+ if (clazz.isAnnotationPresent(SagaTransactional.class)) {
+ annotatedClasses.add(clazz);
+ }
+
+ // Also check interfaces
+ for (Class<?> interfaceClass : clazz.getInterfaces()) {
+ if
(interfaceClass.isAnnotationPresent(SagaTransactional.class)) {
+ annotatedClasses.add(interfaceClass);
+ }
+ }
+ }
+
+ // Should find AccountService and PaymentService
+ assertTrue(annotatedClasses.size() >= 2);
+ assertTrue(annotatedClasses.contains(AccountService.class));
+ assertTrue(annotatedClasses.contains(PaymentService.class));
+ }
+
+ @Test
+ public void testServiceInstantiation() {
+ // Verify services with annotation can be instantiated normally
+ AccountService accountService = new AccountService();
+ assertNotNull(accountService);
+ assertTrue(accountService.debit("123", 100.0));
+
+ PaymentServiceImpl paymentService = new PaymentServiceImpl();
+ assertNotNull(paymentService);
+ assertTrue(paymentService.processPayment("order123", 50.0));
+ }
+
+ @Test
+ public void testReflectionAccess() throws Exception {
+ AccountService service = new AccountService();
+ Class<?> serviceClass = service.getClass();
+
+ // Verify we can access methods through reflection
+ assertNotNull(serviceClass.getMethod("debit", String.class,
double.class));
+ assertNotNull(serviceClass.getMethod("credit", String.class,
double.class));
+
+ // Verify annotation is accessible through reflection
+ assertTrue(serviceClass.isAnnotationPresent(SagaTransactional.class));
+ }
+
+ @Test
+ public void testClassHierarchyAnnotationDetection() {
+ // Test complex inheritance scenario
+ @SagaTransactional
+ class BaseService {
+ public void baseMethod() {}
+ }
+
+ class MiddleService extends BaseService {
+ public void middleMethod() {}
+ }
+
+ class FinalService extends MiddleService {
+ public void finalMethod() {}
+ }
+
+ // Test inheritance chain
+
assertTrue(BaseService.class.isAnnotationPresent(SagaTransactional.class));
+
assertTrue(MiddleService.class.isAnnotationPresent(SagaTransactional.class));
// inherited
+
assertTrue(FinalService.class.isAnnotationPresent(SagaTransactional.class)); //
inherited
+
+ // Test instances
+ FinalService finalService = new FinalService();
+
assertTrue(finalService.getClass().isAnnotationPresent(SagaTransactional.class));
+ }
+}
diff --git
a/saga/seata-saga-annotation/src/test/java/org/apache/seata/saga/rm/remoting/parser/SagaTransactionalRemotingParserTest.java
b/saga/seata-saga-annotation/src/test/java/org/apache/seata/saga/rm/remoting/parser/SagaTransactionalRemotingParserTest.java
new file mode 100644
index 0000000000..bcb48433c1
--- /dev/null
+++
b/saga/seata-saga-annotation/src/test/java/org/apache/seata/saga/rm/remoting/parser/SagaTransactionalRemotingParserTest.java
@@ -0,0 +1,256 @@
+/*
+ * 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.rm.remoting.parser;
+
+import org.apache.seata.common.exception.FrameworkException;
+import org.apache.seata.integration.tx.api.remoting.Protocols;
+import org.apache.seata.integration.tx.api.remoting.RemotingDesc;
+import org.apache.seata.saga.rm.api.SagaTransactional;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Unit tests for SagaTransactionalRemotingParser
+ * Tests the @SagaTransactional annotation support for Saga transaction
scenarios
+ *
+ * Test scenarios covered:
+ * 1. @SagaTransactional annotation on implementation class
+ * 2. @SagaTransactional annotation on interface with implementation
+ * 3. No annotations (negative test cases)
+ * 4. Multiple interfaces with different annotations
+ * 5. Inheritance scenarios
+ *
+ * The tests verify:
+ * - Annotation detection accuracy
+ * - Service/reference identification
+ * - RemotingDesc generation correctness
+ * - Proper separation from LocalTCC functionality
+ */
+public class SagaTransactionalRemotingParserTest {
+
+ private SagaTransactionalRemotingParser parser;
+
+ // Test classes with different annotation patterns
+ @SagaTransactional
+ public static class SagaTransactionalService {
+ public String doSomething() {
+ return "saga-transactional";
+ }
+ }
+
+ public static class NoAnnotationService {
+ public String doSomething() {
+ return "none";
+ }
+ }
+
+ @SagaTransactional
+ public interface SagaTransactionalInterface {
+ String doSomething();
+ }
+
+ public interface NoAnnotationInterface {
+ String doSomething();
+ }
+
+ public static class SagaTransactionalInterfaceImpl implements
SagaTransactionalInterface {
+ @Override
+ public String doSomething() {
+ return "impl-saga-transactional";
+ }
+ }
+
+ public static class NoAnnotationInterfaceImpl implements
NoAnnotationInterface {
+ @Override
+ public String doSomething() {
+ return "impl-none";
+ }
+ }
+
+ @SagaTransactional
+ public static class InheritedSagaService extends SagaTransactionalService {
+ @Override
+ public String doSomething() {
+ return "inherited-saga";
+ }
+ }
+
+ @BeforeEach
+ public void setUp() {
+ parser = new SagaTransactionalRemotingParser();
+ }
+
+ @Test
+ public void testIsReference_SagaTransactional() {
+ SagaTransactionalService service = new SagaTransactionalService();
+ assertTrue(parser.isReference(service, "sagaTransactionalService"));
+ }
+
+ @Test
+ public void testIsReference_NoAnnotations() {
+ NoAnnotationService service = new NoAnnotationService();
+ assertFalse(parser.isReference(service, "noAnnotationService"));
+ }
+
+ @Test
+ public void testIsService_SagaTransactional() {
+ SagaTransactionalService service = new SagaTransactionalService();
+ assertTrue(parser.isService(service, "sagaTransactionalService"));
+ }
+
+ @Test
+ public void testIsService_NoAnnotations() {
+ NoAnnotationService service = new NoAnnotationService();
+ assertFalse(parser.isService(service, "noAnnotationService"));
+ }
+
+ @Test
+ public void testIsService_Class_SagaTransactional() throws
FrameworkException {
+ assertTrue(parser.isService(SagaTransactionalService.class));
+ }
+
+ @Test
+ public void testIsService_Class_NoAnnotations() throws FrameworkException {
+ assertFalse(parser.isService(NoAnnotationService.class));
+ }
+
+ @Test
+ public void testGetServiceDesc_SagaTransactionalOnImplementation() throws
FrameworkException {
+ SagaTransactionalService service = new SagaTransactionalService();
+ RemotingDesc desc = parser.getServiceDesc(service,
"sagaTransactionalService");
+
+ assertRemotingDescWithServiceClass(desc, service,
SagaTransactionalService.class);
+ }
+
+ @Test
+ public void testGetServiceDesc_SagaTransactionalOnInterface() throws
FrameworkException {
+ SagaTransactionalInterfaceImpl service = new
SagaTransactionalInterfaceImpl();
+ RemotingDesc desc = parser.getServiceDesc(service,
"sagaTransactionalInterfaceImpl");
+
+ assertRemotingDescWithServiceClass(desc, service,
SagaTransactionalInterface.class);
+ }
+
+ @Test
+ public void testGetServiceDesc_NoAnnotations_ReturnsNull() {
+ NoAnnotationService service = new NoAnnotationService();
+ assertNull(parser.getServiceDesc(service, "noAnnotationService"));
+ }
+
+ @Test
+ public void testGetServiceDesc_NoAnnotationsOnInterface_ReturnsNull() {
+ NoAnnotationInterfaceImpl service = new NoAnnotationInterfaceImpl();
+ assertNull(parser.getServiceDesc(service,
"noAnnotationInterfaceImpl"));
+ }
+
+ @Test
+ public void testGetProtocol() {
+ assertEquals(Protocols.IN_JVM, parser.getProtocol());
+ }
+
+ @Test
+ public void testInheritedSagaTransactional() throws FrameworkException {
+ InheritedSagaService service = new InheritedSagaService();
+
+ assertTrue(parser.isService(service, "inheritedSagaService"));
+
+ RemotingDesc desc = parser.getServiceDesc(service,
"inheritedSagaService");
+ assertRemotingDescWithServiceClass(desc, service,
InheritedSagaService.class);
+ }
+
+ @Test
+ public void testAnnotationPrecedence_ImplementationOverInterface() throws
FrameworkException {
+ // When both implementation and interface have @SagaTransactional,
+ // implementation should take precedence
+
+ @SagaTransactional
+ class TestImpl implements SagaTransactionalInterface {
+ @Override
+ public String doSomething() {
+ return "test-impl";
+ }
+ }
+
+ TestImpl service = new TestImpl();
+ RemotingDesc desc = parser.getServiceDesc(service, "testImpl");
+
+ assertNotNull(desc);
+ // Implementation class should be used, not the interface
+ assertEquals(TestImpl.class, desc.getServiceClass());
+ }
+
+ @Test
+ public void testThrowsFrameworkExceptionWhenNoAnnotationFound() {
+ NoAnnotationService service = new NoAnnotationService();
+
+ // When a bean has no @SagaTransactional annotation, getServiceDesc
should return null
+ // because isRemoting() check will fail before reaching the exception
throwing code
+ RemotingDesc desc = parser.getServiceDesc(service,
"noAnnotationService");
+ assertNull(desc);
+ }
+
+ // Test complex inheritance scenario
+ @Test
+ public void testComplexInheritanceHierarchy() throws FrameworkException {
+ @SagaTransactional
+ class Level1 {
+ public boolean level1Method() {
+ return true;
+ }
+ }
+
+ class Level2 extends Level1 {
+ public boolean level2Method() {
+ return true;
+ }
+ }
+
+ @SagaTransactional
+ class Level3 extends Level2 {
+ public boolean level3Method() {
+ return true;
+ }
+ }
+
+ Level3 service = new Level3();
+
+ // Should be recognized (has its own @SagaTransactional)
+ assertTrue(parser.isService(service, "level3Service"));
+
+ RemotingDesc desc = parser.getServiceDesc(service, "level3Service");
+ assertNotNull(desc);
+ assertEquals(Level3.class, desc.getServiceClass());
+ }
+
+ private void assertValidRemotingDesc(RemotingDesc desc, Object
expectedTargetBean) {
+ assertNotNull(desc);
+ assertTrue(desc.isReference());
+ assertTrue(desc.isService());
+ assertEquals(Protocols.IN_JVM, desc.getProtocol());
+ assertEquals(expectedTargetBean, desc.getTargetBean());
+ assertNotNull(desc.getServiceClass());
+ assertNotNull(desc.getServiceClassName());
+ }
+
+ private void assertRemotingDescWithServiceClass(
+ RemotingDesc desc, Object targetBean, Class<?>
expectedServiceClass) {
+ assertValidRemotingDesc(desc, targetBean);
+ assertEquals(expectedServiceClass, desc.getServiceClass());
+ assertEquals(expectedServiceClass.getName(),
desc.getServiceClassName());
+ }
+}
diff --git a/tcc/src/main/java/org/apache/seata/rm/tcc/api/LocalTCC.java
b/tcc/src/main/java/org/apache/seata/rm/tcc/api/LocalTCC.java
index ba288e3bc6..f3bb601b49 100644
--- a/tcc/src/main/java/org/apache/seata/rm/tcc/api/LocalTCC.java
+++ b/tcc/src/main/java/org/apache/seata/rm/tcc/api/LocalTCC.java
@@ -27,8 +27,42 @@ import java.lang.annotation.Target;
/**
* Local TCC bean annotation, add on the TCC interface
*
- * @see
org.apache.seata.spring.annotation.GlobalTransactionScanner#wrapIfNecessary(Object,
String, Object) // the scanner for TM, GlobalLock, and TCC mode
- * @see LocalTCCRemotingParser // the RemotingParser impl for LocalTCC
+ * This annotation is specifically designed for TCC (Try-Confirm-Cancel)
transaction mode.
+ * For Saga scenarios, consider using @SagaTransactional annotation instead to
avoid confusion.
+ *
+ * When to Use @LocalTCC:
+ * - Pure TCC scenarios with Try-Confirm-Cancel semantics
+ * - Existing stable implementations (backward compatibility)
+ * - Services explicitly designed for TCC mode
+ * - When you want to explicitly indicate TCC transaction mode
+ *
+ * When to Consider @SagaTransactional Instead:
+ * - Saga scenarios with compensation actions
+ * - Generic transaction participants (non-TCC specific)
+ * - Services that might be used in multiple transaction modes
+ *
+ * Example Usage:
+ *
+ * @LocalTCC
+ * public interface PaymentTccService {
+ * @TwoPhaseBusinessAction(name = "payment", commitMethod =
"confirmPayment", rollbackMethod = "cancelPayment")
+ * boolean tryPayment(BusinessActionContext context, String orderId,
double amount);
+ *
+ * boolean confirmPayment(BusinessActionContext context);
+ *
+ * boolean cancelPayment(BusinessActionContext context);
+ * }
+ *
+ * Annotation Separation:
+ * Starting from version 2.5.0, @LocalTCC and @SagaTransactional have
dedicated parsers:
+ * - LocalTCCRemotingParser handles @LocalTCC (TCC mode)
+ * - SagaTransactionalRemotingParser handles @SagaTransactional (Saga mode)
+ * This ensures clear separation of concerns and better maintainability.
+ *
+ * @see
org.apache.seata.spring.annotation.GlobalTransactionScanner#wrapIfNecessary(Object,
String, Object) the scanner for TM, GlobalLock, and TCC mode
+ * @see LocalTCCRemotingParser the RemotingParser impl for LocalTCC
+ * @see org.apache.seata.saga.rm.api.SagaTransactional the dedicated
annotation for Saga transaction participants
+ * @see org.apache.seata.rm.tcc.api.TwoPhaseBusinessAction commonly used with
this annotation
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
diff --git
a/tcc/src/main/java/org/apache/seata/rm/tcc/api/TwoPhaseBusinessAction.java
b/tcc/src/main/java/org/apache/seata/rm/tcc/api/TwoPhaseBusinessAction.java
index 7172f1a9c3..257dfbc73c 100644
--- a/tcc/src/main/java/org/apache/seata/rm/tcc/api/TwoPhaseBusinessAction.java
+++ b/tcc/src/main/java/org/apache/seata/rm/tcc/api/TwoPhaseBusinessAction.java
@@ -27,9 +27,14 @@ import java.lang.annotation.Target;
/**
* TCC annotation.
* Define a TCC interface, which added on the try method.
- * Must be used with `@LocalTCC`.
+ * Must be used with `@LocalTCC` annotation on the interface or implementation
class.
+ * This annotation is specifically designed for TCC (Try-Confirm-Cancel)
transaction mode.
*
- * @see org.apache.seata.rm.tcc.api.LocalTCC // TCC annotation, which added on
the TCC interface. It can't be left out.
+ * For Saga scenarios with compensation patterns, use
`@CompensationBusinessAction` on methods within classes or interfaces marked
with `@SagaTransactional`.
+ *
+ * @see org.apache.seata.rm.tcc.api.LocalTCC TCC annotation, which added on
the TCC interface. It can't be left out.
+ * @see org.apache.seata.saga.rm.api.SagaTransactional Generic transaction
participant annotation for Saga scenarios
+ * @see org.apache.seata.saga.rm.api.CompensationBusinessAction Saga-specific
business action annotation
* @see
org.apache.seata.spring.annotation.GlobalTransactionScanner#wrapIfNecessary(Object,
String, Object) // the scanner for TM, GlobalLock, and TCC mode
* @see TccActionInterceptorHandler // the interceptor of TCC mode
* @see BusinessActionContext
diff --git
a/tcc/src/main/java/org/apache/seata/rm/tcc/remoting/parser/LocalTCCRemotingParser.java
b/tcc/src/main/java/org/apache/seata/rm/tcc/remoting/parser/LocalTCCRemotingParser.java
index d1906e3936..8bbed44efc 100644
---
a/tcc/src/main/java/org/apache/seata/rm/tcc/remoting/parser/LocalTCCRemotingParser.java
+++
b/tcc/src/main/java/org/apache/seata/rm/tcc/remoting/parser/LocalTCCRemotingParser.java
@@ -27,8 +27,38 @@ import org.springframework.aop.framework.AopProxyUtils;
import java.util.Set;
/**
- * local tcc bean parsing
+ * Remoting parser for TCC transaction participant beans with @LocalTCC
annotation
*
+ * This parser is specifically designed for TCC (Try-Confirm-Cancel)
transaction mode
+ * and handles beans annotated with @LocalTCC annotation. It provides proper
service
+ * detection and proxy enhancement for TCC scenarios.
+ *
+ * Key Features:
+ * - Dedicated support for @LocalTCC annotation
+ * - Optimized for TCC Try-Confirm-Cancel patterns
+ * - Proper integration with TwoPhaseBusinessAction
+ * - High-performance annotation detection
+ *
+ * Usage Pattern:
+ * @LocalTCC
+ * public interface PaymentTccService {
+ * @TwoPhaseBusinessAction(name = "payment", commitMethod =
"confirmPayment", rollbackMethod = "cancelPayment")
+ * boolean tryPayment(BusinessActionContext context, String orderId,
double amount);
+ *
+ * boolean confirmPayment(BusinessActionContext context);
+ * boolean cancelPayment(BusinessActionContext context);
+ * }
+ *
+ * Detection Priority:
+ * 1. Implementation class annotations (higher priority)
+ * 2. Interface annotations (fallback)
+ *
+ * Note: For Saga scenarios, use @SagaTransactional with
SagaTransactionalRemotingParser instead.
+ *
+ * @see LocalTCC The TCC-specific annotation this parser handles
+ * @see org.apache.seata.rm.tcc.api.TwoPhaseBusinessAction Commonly used with
@LocalTCC
+ * @see
org.apache.seata.integration.tx.api.remoting.parser.AbstractedRemotingParser
Base class
+ * @since 1.0.0
*/
public class LocalTCCRemotingParser extends AbstractedRemotingParser {
@@ -57,24 +87,28 @@ public class LocalTCCRemotingParser extends
AbstractedRemotingParser {
remotingDesc.setService(this.isService(bean, beanName));
remotingDesc.setProtocol(Protocols.IN_JVM);
Class<?> classType = bean.getClass();
- // check if LocalTCC annotation is marked on the implementation class
- if (classType.isAnnotationPresent(LocalTCC.class)) {
+
+ // First priority: check if @LocalTCC is present on the implementation
class itself
+ // Implementation class annotations take precedence over interface
annotations
+ if (hasLocalTCCAnnotation(classType)) {
remotingDesc.setServiceClass(AopProxyUtils.ultimateTargetClass(bean));
remotingDesc.setServiceClassName(remotingDesc.getServiceClass().getName());
remotingDesc.setTargetBean(bean);
return remotingDesc;
}
- // check if LocalTCC annotation is marked on the interface
+
+ // Second priority: check if @LocalTCC is present on any implemented
interfaces
+ // Fall back to interface annotations if no implementation class
annotations found
Set<Class<?>> interfaceClasses =
ReflectionUtil.getInterfaces(classType);
for (Class<?> interClass : interfaceClasses) {
- if (interClass.isAnnotationPresent(LocalTCC.class)) {
+ if (hasLocalTCCAnnotation(interClass)) {
remotingDesc.setServiceClassName(interClass.getName());
remotingDesc.setServiceClass(interClass);
remotingDesc.setTargetBean(bean);
return remotingDesc;
}
}
- throw new FrameworkException("Couldn't parser any Remoting info");
+ throw new FrameworkException("Couldn't parse any Remoting info for
LocalTCC bean");
}
@Override
@@ -83,22 +117,39 @@ public class LocalTCCRemotingParser extends
AbstractedRemotingParser {
}
/**
- * Determine whether there is an annotation on interface or impl {@link
LocalTCC}
- * @param bean the bean
- * @return boolean
+ * Check if the given bean is annotated with @LocalTCC annotation
+ *
+ * @param bean the bean to check
+ * @return true if the bean or its interfaces have @LocalTCC annotation
*/
private boolean isLocalTCC(Object bean) {
- Class<?> classType = bean.getClass();
- return isLocalTCC(classType);
+ return isLocalTCC(bean.getClass());
}
+ /**
+ * Check if the given class or its interfaces are annotated with @LocalTCC
annotation
+ *
+ * @param classType the class type to check
+ * @return true if the class has @LocalTCC annotation
+ */
private boolean isLocalTCC(Class<?> classType) {
- Set<Class<?>> interfaceClasses =
ReflectionUtil.getInterfaces(classType);
- for (Class<?> interClass : interfaceClasses) {
- if (interClass.isAnnotationPresent(LocalTCC.class)) {
- return true;
- }
+ // Check the class itself first for better performance
+ if (hasLocalTCCAnnotation(classType)) {
+ return true;
}
- return classType.isAnnotationPresent(LocalTCC.class);
+
+ // Check all interfaces
+ Set<Class<?>> interfaceClasses =
ReflectionUtil.getInterfaces(classType);
+ return interfaceClasses.stream().anyMatch(this::hasLocalTCCAnnotation);
+ }
+
+ /**
+ * Check if a class has @LocalTCC annotation
+ *
+ * @param clazz the class to check
+ * @return true if the class has @LocalTCC annotation
+ */
+ private boolean hasLocalTCCAnnotation(Class<?> clazz) {
+ return clazz.isAnnotationPresent(LocalTCC.class);
}
}
diff --git
a/tcc/src/test/java/org/apache/seata/rm/tcc/remoting/parser/LocalTCCRemotingParserTest.java
b/tcc/src/test/java/org/apache/seata/rm/tcc/remoting/parser/LocalTCCRemotingParserTest.java
index b25ad556fb..084408a5ce 100644
---
a/tcc/src/test/java/org/apache/seata/rm/tcc/remoting/parser/LocalTCCRemotingParserTest.java
+++
b/tcc/src/test/java/org/apache/seata/rm/tcc/remoting/parser/LocalTCCRemotingParserTest.java
@@ -16,57 +16,208 @@
*/
package org.apache.seata.rm.tcc.remoting.parser;
+import org.apache.seata.common.exception.FrameworkException;
+import org.apache.seata.integration.tx.api.remoting.Protocols;
import org.apache.seata.integration.tx.api.remoting.RemotingDesc;
-import org.apache.seata.rm.tcc.TccAction;
-import org.apache.seata.rm.tcc.TccActionImpl;
-import org.junit.jupiter.api.Assertions;
+import org.apache.seata.rm.tcc.api.LocalTCC;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
/**
- * The type Local tcc remoting parser test.
+ * Unit tests for LocalTCCRemotingParser
+ * Tests the @LocalTCC annotation support for TCC transaction mode
+ *
+ * Test scenarios covered:
+ * 1. @LocalTCC annotation on implementation class
+ * 2. @LocalTCC annotation on interface with implementation
+ * 3. No annotations (negative test cases)
+ * 4. Multiple interfaces with different annotation patterns
*
+ * The tests verify:
+ * - Annotation detection accuracy for @LocalTCC
+ * - Service/reference identification
+ * - RemotingDesc generation correctness
+ * - TCC-specific functionality
*/
public class LocalTCCRemotingParserTest {
- /**
- * The Local tcc remoting parser.
- */
- LocalTCCRemotingParser localTCCRemotingParser = new
LocalTCCRemotingParser();
+ private LocalTCCRemotingParser parser;
+
+ // Test classes with different annotation patterns
+ @LocalTCC
+ public static class LocalTCCService {
+ public String doSomething() {
+ return "local-tcc";
+ }
+ }
+
+ public static class NoAnnotationService {
+ public String doSomething() {
+ return "none";
+ }
+ }
+
+ @LocalTCC
+ public interface LocalTCCInterface {
+ String doSomething();
+ }
+
+ public interface NoAnnotationInterface {
+ String doSomething();
+ }
+
+ public static class LocalTCCInterfaceImpl implements LocalTCCInterface {
+ @Override
+ public String doSomething() {
+ return "impl-local-tcc";
+ }
+ }
+
+ public static class NoAnnotationInterfaceImpl implements
NoAnnotationInterface {
+ @Override
+ public String doSomething() {
+ return "impl-none";
+ }
+ }
+
+ // Test class with multiple interfaces
+ @LocalTCC
+ public interface AnotherLocalTCCInterface {
+ String doAnotherThing();
+ }
+
+ public static class MultipleInterfaceImpl implements LocalTCCInterface,
NoAnnotationInterface {
+ @Override
+ public String doSomething() {
+ return "multiple-interfaces";
+ }
+ }
+
+ @BeforeEach
+ public void setUp() {
+ parser = new LocalTCCRemotingParser();
+ }
- /**
- * Test service parser.
- */
@Test
- public void testServiceParser() {
- TccActionImpl tccAction = new TccActionImpl();
+ public void testIsReference_LocalTCC() {
+ LocalTCCService service = new LocalTCCService();
+ assertTrue(parser.isReference(service, "localTCCService"));
+ }
- boolean result = localTCCRemotingParser.isService(tccAction, "a");
- Assertions.assertTrue(result);
+ @Test
+ public void testIsReference_NoAnnotations() {
+ NoAnnotationService service = new NoAnnotationService();
+ assertFalse(parser.isReference(service, "noAnnotationService"));
}
- /**
- * Test reference parser.
- */
@Test
- public void testReferenceParser() {
- TccActionImpl tccAction = new TccActionImpl();
+ public void testIsService_LocalTCC() {
+ LocalTCCService service = new LocalTCCService();
+ assertTrue(parser.isService(service, "localTCCService"));
+ }
- boolean result = localTCCRemotingParser.isReference(tccAction, "b");
- Assertions.assertTrue(result);
+ @Test
+ public void testIsService_NoAnnotations() {
+ NoAnnotationService service = new NoAnnotationService();
+ assertFalse(parser.isService(service, "noAnnotationService"));
}
- /**
- * Test service desc.
- */
@Test
- public void testServiceDesc() {
- TccActionImpl tccAction = new TccActionImpl();
+ public void testIsService_Class_LocalTCC() throws FrameworkException {
+ assertTrue(parser.isService(LocalTCCService.class));
+ }
+
+ @Test
+ public void testIsService_Class_NoAnnotations() throws FrameworkException {
+ assertFalse(parser.isService(NoAnnotationService.class));
+ }
- RemotingDesc remotingDesc =
localTCCRemotingParser.getServiceDesc(tccAction, "c");
- Assertions.assertNotNull(remotingDesc);
+ @Test
+ public void testGetServiceDesc_LocalTCCOnImplementation() throws
FrameworkException {
+ LocalTCCService service = new LocalTCCService();
+ RemotingDesc desc = parser.getServiceDesc(service, "localTCCService");
+
+ assertRemotingDescWithServiceClass(desc, service,
LocalTCCService.class);
+ }
+
+ @Test
+ public void testGetServiceDesc_LocalTCCOnInterface() throws
FrameworkException {
+ LocalTCCInterfaceImpl service = new LocalTCCInterfaceImpl();
+ RemotingDesc desc = parser.getServiceDesc(service,
"localTCCInterfaceImpl");
+
+ assertRemotingDescWithServiceClass(desc, service,
LocalTCCInterface.class);
+ }
+
+ @Test
+ public void testGetServiceDesc_NoAnnotations_ReturnsNull() {
+ NoAnnotationService service = new NoAnnotationService();
+ RemotingDesc desc = parser.getServiceDesc(service,
"noAnnotationService");
+
+ assertNull(desc);
+ }
+
+ @Test
+ public void testGetServiceDesc_NoAnnotationsOnInterface_ReturnsNull() {
+ NoAnnotationInterfaceImpl service = new NoAnnotationInterfaceImpl();
+ RemotingDesc desc = parser.getServiceDesc(service,
"noAnnotationInterfaceImpl");
+
+ assertNull(desc);
+ }
+
+ @Test
+ public void testGetProtocol() {
+ assertEquals(Protocols.IN_JVM, parser.getProtocol());
+ }
+
+ @Test
+ public void testMultipleInterfacesWithLocalTCC() throws FrameworkException
{
+ MultipleInterfaceImpl service = new MultipleInterfaceImpl();
+ RemotingDesc desc = parser.getServiceDesc(service,
"multipleInterfaceImpl");
+
+ assertNotNull(desc);
+ assertValidRemotingDesc(desc, service);
+
+ // Should detect the @LocalTCC annotation on LocalTCCInterface
+ assertTrue(isAnnotatedInterface(desc.getServiceClass()));
+ assertEquals(LocalTCCInterface.class, desc.getServiceClass());
+ }
+
+ @Test
+ public void testIsRemoting_LocalTCC() {
+ LocalTCCService service = new LocalTCCService();
+ assertTrue(parser.isRemoting(service, "localTCCService"));
+ }
+
+ @Test
+ public void testIsRemoting_NoAnnotations() {
+ NoAnnotationService service = new NoAnnotationService();
+ assertFalse(parser.isRemoting(service, "noAnnotationService"));
+ }
+
+ private boolean isAnnotatedInterface(Class<?> clazz) {
+ return clazz.isAnnotationPresent(LocalTCC.class);
+ }
+
+ private void assertValidRemotingDesc(RemotingDesc desc, Object
expectedTargetBean) {
+ assertNotNull(desc);
+ assertTrue(desc.isReference());
+ assertTrue(desc.isService());
+ assertEquals(Protocols.IN_JVM, desc.getProtocol());
+ assertEquals(expectedTargetBean, desc.getTargetBean());
+ assertNotNull(desc.getServiceClass());
+ assertNotNull(desc.getServiceClassName());
+ }
- Assertions.assertEquals("org.apache.seata.rm.tcc.TccAction",
remotingDesc.getServiceClassName());
- Assertions.assertEquals(remotingDesc.getServiceClass(),
TccAction.class);
- Assertions.assertEquals(remotingDesc.getTargetBean(), tccAction);
+ private void assertRemotingDescWithServiceClass(
+ RemotingDesc desc, Object targetBean, Class<?>
expectedServiceClass) {
+ assertValidRemotingDesc(desc, targetBean);
+ assertEquals(expectedServiceClass, desc.getServiceClass());
+ assertEquals(expectedServiceClass.getName(),
desc.getServiceClassName());
}
}
diff --git
a/test/src/test/java/org/apache/seata/saga/annotation/AnnotationConflictTest.java
b/test/src/test/java/org/apache/seata/saga/annotation/AnnotationConflictTest.java
new file mode 100644
index 0000000000..43737e3a2d
--- /dev/null
+++
b/test/src/test/java/org/apache/seata/saga/annotation/AnnotationConflictTest.java
@@ -0,0 +1,364 @@
+/*
+ * 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.integration.tx.api.remoting.RemotingDesc;
+import org.apache.seata.rm.tcc.api.LocalTCC;
+import org.apache.seata.rm.tcc.remoting.parser.LocalTCCRemotingParser;
+import org.apache.seata.saga.rm.api.SagaTransactional;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Tests for LocalTCCRemotingParser behavior with different annotation
scenarios
+ *
+ * This test suite validates that LocalTCCRemotingParser:
+ * 1. ONLY recognizes @LocalTCC annotations (its primary responsibility)
+ * 2. IGNORES @SagaTransactional annotations (not its responsibility)
+ * 3. Handles inheritance correctly for @LocalTCC
+ * 4. Handles interface vs implementation scenarios for @LocalTCC
+ * 5. Properly handles edge cases and null scenarios
+ *
+ * Note: @SagaTransactional annotations should be handled by
SagaTransactionalRemotingParser,
+ * not by LocalTCCRemotingParser. This test verifies proper separation of
concerns.
+ */
+public class AnnotationConflictTest {
+
+ private LocalTCCRemotingParser parser;
+
+ // Valid @LocalTCC scenarios
+ @LocalTCC
+ public static class LocalTCCService {
+ public boolean doSomething() {
+ return true;
+ }
+ }
+
+ @LocalTCC
+ public interface LocalTCCInterface {
+ boolean doSomething();
+ }
+
+ public static class LocalTCCInterfaceImpl implements LocalTCCInterface {
+ @Override
+ public boolean doSomething() {
+ return true;
+ }
+ }
+
+ // @SagaTransactional scenarios (should be IGNORED by
LocalTCCRemotingParser)
+ @SagaTransactional
+ public static class SagaTransactionalService {
+ public boolean doSomething() {
+ return true;
+ }
+ }
+
+ @SagaTransactional
+ public interface SagaTransactionalInterface {
+ boolean doSomething();
+ }
+
+ public static class SagaTransactionalInterfaceImpl implements
SagaTransactionalInterface {
+ @Override
+ public boolean doSomething() {
+ return true;
+ }
+ }
+
+ // Mixed scenarios - implementation vs interface
+ @LocalTCC
+ public static class LocalTCCImpl implements SagaTransactionalInterface {
+ @Override
+ public boolean doSomething() {
+ return true;
+ }
+ }
+
+ @SagaTransactional
+ public static class SagaImpl implements LocalTCCInterface {
+ @Override
+ public boolean doSomething() {
+ return true;
+ }
+ }
+
+ // Inheritance scenarios
+ @LocalTCC
+ public static class BaseLocalTCCService {
+ public boolean baseMethod() {
+ return true;
+ }
+ }
+
+ public static class ExtendedLocalTCCService extends BaseLocalTCCService {
+ public boolean extendedMethod() {
+ return true;
+ }
+ }
+
+ @SagaTransactional
+ public static class BaseSagaService {
+ public boolean baseMethod() {
+ return true;
+ }
+ }
+
+ public static class ExtendedSagaService extends BaseSagaService {
+ public boolean extendedMethod() {
+ return true;
+ }
+ }
+
+ // No annotation scenarios
+ public static class NoAnnotationService {
+ public boolean doSomething() {
+ return true;
+ }
+ }
+
+ public interface NoAnnotationInterface {
+ boolean doSomething();
+ }
+
+ public static class NoAnnotationInterfaceImpl implements
NoAnnotationInterface {
+ @Override
+ public boolean doSomething() {
+ return true;
+ }
+ }
+
+ @BeforeEach
+ public void setUp() {
+ parser = new LocalTCCRemotingParser();
+ }
+
+ // Tests for @LocalTCC recognition (should work)
+ @Test
+ public void testLocalTCCOnClass_ShouldBeRecognized() {
+ LocalTCCService service = new LocalTCCService();
+
+ assertTrue(parser.isService(service, "localTCCService"));
+ assertTrue(parser.isReference(service, "localTCCService"));
+
+ RemotingDesc desc = parser.getServiceDesc(service, "localTCCService");
+ assertNotNull(desc);
+ assertEquals(LocalTCCService.class, desc.getServiceClass());
+ }
+
+ @Test
+ public void testLocalTCCOnInterface_ShouldBeRecognized() {
+ LocalTCCInterfaceImpl service = new LocalTCCInterfaceImpl();
+
+ assertTrue(parser.isService(service, "localTCCInterfaceImpl"));
+
+ RemotingDesc desc = parser.getServiceDesc(service,
"localTCCInterfaceImpl");
+ assertNotNull(desc);
+ assertEquals(LocalTCCInterface.class, desc.getServiceClass());
+ }
+
+ @Test
+ public void testLocalTCCInheritance_ShouldBeRecognized() {
+ ExtendedLocalTCCService service = new ExtendedLocalTCCService();
+
+ // Should inherit @LocalTCC from parent
+ assertTrue(parser.isService(service, "extendedLocalTCCService"));
+
+ RemotingDesc desc = parser.getServiceDesc(service,
"extendedLocalTCCService");
+ assertNotNull(desc);
+ assertEquals(ExtendedLocalTCCService.class, desc.getServiceClass());
+ }
+
+ // Tests for @SagaTransactional scenarios (should be IGNORED)
+ @Test
+ public void testSagaTransactionalOnClass_ShouldBeIgnored() {
+ SagaTransactionalService service = new SagaTransactionalService();
+
+ // LocalTCCRemotingParser should NOT recognize @SagaTransactional
+ assertFalse(parser.isService(service, "sagaTransactionalService"));
+ assertFalse(parser.isReference(service, "sagaTransactionalService"));
+
+ RemotingDesc desc = parser.getServiceDesc(service,
"sagaTransactionalService");
+ assertNull(desc);
+ }
+
+ @Test
+ public void testSagaTransactionalOnInterface_ShouldBeIgnored() {
+ SagaTransactionalInterfaceImpl service = new
SagaTransactionalInterfaceImpl();
+
+ // LocalTCCRemotingParser should NOT recognize @SagaTransactional
+ assertFalse(parser.isService(service,
"sagaTransactionalInterfaceImpl"));
+
+ RemotingDesc desc = parser.getServiceDesc(service,
"sagaTransactionalInterfaceImpl");
+ assertNull(desc);
+ }
+
+ @Test
+ public void testSagaTransactionalInheritance_ShouldBeIgnored() {
+ ExtendedSagaService service = new ExtendedSagaService();
+
+ // LocalTCCRemotingParser should NOT recognize inherited
@SagaTransactional
+ assertFalse(parser.isService(service, "extendedSagaService"));
+
+ RemotingDesc desc = parser.getServiceDesc(service,
"extendedSagaService");
+ assertNull(desc);
+ }
+
+ // Tests for mixed scenarios
+ @Test
+ public void
testLocalTCCImplWithSagaTransactionalInterface_ShouldRecognizeLocalTCC() {
+ LocalTCCImpl service = new LocalTCCImpl();
+
+ // Should recognize @LocalTCC on implementation, ignore
@SagaTransactional on interface
+ assertTrue(parser.isService(service, "localTCCImpl"));
+
+ RemotingDesc desc = parser.getServiceDesc(service, "localTCCImpl");
+ assertNotNull(desc);
+ // Implementation class should be used (has @LocalTCC)
+ assertEquals(LocalTCCImpl.class, desc.getServiceClass());
+ }
+
+ @Test
+ public void
testSagaImplWithLocalTCCInterface_ShouldRecognizeInterfaceLocalTCC() {
+ SagaImpl service = new SagaImpl();
+
+ // Should recognize @LocalTCC on interface, ignore @SagaTransactional
on implementation
+ assertTrue(parser.isService(service, "sagaImpl"));
+
+ RemotingDesc desc = parser.getServiceDesc(service, "sagaImpl");
+ assertNotNull(desc);
+ // Interface should be used (has @LocalTCC)
+ assertEquals(LocalTCCInterface.class, desc.getServiceClass());
+ }
+
+ // Tests for no annotation scenarios
+ @Test
+ public void testNoAnnotations_ShouldNotBeRecognized() {
+ NoAnnotationService service = new NoAnnotationService();
+
+ assertFalse(parser.isService(service, "noAnnotationService"));
+ assertFalse(parser.isReference(service, "noAnnotationService"));
+
+ RemotingDesc desc = parser.getServiceDesc(service,
"noAnnotationService");
+ assertNull(desc);
+ }
+
+ @Test
+ public void testNoAnnotationInterface_ShouldNotBeRecognized() {
+ NoAnnotationInterfaceImpl service = new NoAnnotationInterfaceImpl();
+
+ assertFalse(parser.isService(service, "noAnnotationInterfaceImpl"));
+
+ RemotingDesc desc = parser.getServiceDesc(service,
"noAnnotationInterfaceImpl");
+ assertNull(desc);
+ }
+
+ // Edge case tests
+ @Test
+ public void testNullService_ShouldThrowException() {
+ assertThrows(RuntimeException.class, () -> {
+ parser.isService(null, "nullService");
+ });
+
+ assertThrows(RuntimeException.class, () -> {
+ parser.getServiceDesc(null, "nullService");
+ });
+ }
+
+ @Test
+ public void testNullBeanName_ShouldNotThrowException() {
+ LocalTCCService service = new LocalTCCService();
+
+ assertDoesNotThrow(() -> {
+ boolean result = parser.isService(service, null);
+ assertTrue(result);
+ });
+
+ assertDoesNotThrow(() -> {
+ RemotingDesc desc = parser.getServiceDesc(service, null);
+ assertNotNull(desc);
+ });
+ }
+
+ @Test
+ public void testEmptyBeanName_ShouldNotThrowException() {
+ LocalTCCService service = new LocalTCCService();
+
+ assertDoesNotThrow(() -> {
+ boolean result = parser.isService(service, "");
+ assertTrue(result);
+ });
+
+ assertDoesNotThrow(() -> {
+ RemotingDesc desc = parser.getServiceDesc(service, "");
+ assertNotNull(desc);
+ });
+ }
+
+ @Test
+ public void testAnnotationPrecedence_ImplementationOverInterface() {
+ // When implementation has @LocalTCC and interface has
@SagaTransactional,
+ // implementation should take precedence
+
+ LocalTCCImpl service = new LocalTCCImpl();
+ RemotingDesc desc = parser.getServiceDesc(service, "testImpl");
+
+ assertNotNull(desc);
+ // Implementation class should be used (has @LocalTCC)
+ assertEquals(LocalTCCImpl.class, desc.getServiceClass());
+ }
+
+ @Test
+ public void testClassHierarchyWithLocalTCC() {
+ // Test inheritance chain with @LocalTCC
+ @LocalTCC
+ class Level1 {
+ public boolean level1Method() {
+ return true;
+ }
+ }
+
+ class Level2 extends Level1 {
+ public boolean level2Method() {
+ return true;
+ }
+ }
+
+ class Level3 extends Level2 {
+ public boolean level3Method() {
+ return true;
+ }
+ }
+
+ Level3 service = new Level3();
+
+ // Should be recognized (inherits @LocalTCC)
+ assertTrue(parser.isService(service, "level3Service"));
+
+ RemotingDesc desc = parser.getServiceDesc(service, "level3Service");
+ assertNotNull(desc);
+ assertEquals(Level3.class, desc.getServiceClass());
+ }
+}
diff --git
a/test/src/test/java/org/apache/seata/saga/annotation/DualParserIntegrationTest.java
b/test/src/test/java/org/apache/seata/saga/annotation/DualParserIntegrationTest.java
new file mode 100644
index 0000000000..b9a02e9831
--- /dev/null
+++
b/test/src/test/java/org/apache/seata/saga/annotation/DualParserIntegrationTest.java
@@ -0,0 +1,308 @@
+/*
+ * 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.integration.tx.api.remoting.RemotingDesc;
+import org.apache.seata.rm.tcc.api.LocalTCC;
+import org.apache.seata.rm.tcc.remoting.parser.LocalTCCRemotingParser;
+import org.apache.seata.saga.rm.api.SagaTransactional;
+import
org.apache.seata.saga.rm.remoting.parser.SagaTransactionalRemotingParser;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Integration tests for dual parser system (LocalTCCRemotingParser +
SagaTransactionalRemotingParser)
+ *
+ * This test suite validates that both parsers can work together correctly in
the same application:
+ *
+ * Key scenarios covered:
+ * 1. LocalTCCRemotingParser correctly handles @LocalTCC and ignores
@SagaTransactional
+ * 2. SagaTransactionalRemotingParser correctly handles @SagaTransactional and
ignores @LocalTCC
+ * 3. Both parsers can work on the same service beans without conflicts
+ * 4. Parser precedence and service detection work correctly
+ * 5. Mixed annotation scenarios are handled properly by each parser
+ * 6. No cross-contamination between parsers
+ *
+ * This ensures that the new @SagaTransactional annotation system works
seamlessly
+ * with existing @LocalTCC infrastructure without breaking changes.
+ */
+public class DualParserIntegrationTest {
+
+ private LocalTCCRemotingParser localTCCParser;
+ private SagaTransactionalRemotingParser sagaTransactionalParser;
+
+ // Pure @LocalTCC scenarios
+ @LocalTCC
+ public static class PureLocalTCCService {
+ public boolean doTccOperation() {
+ return true;
+ }
+ }
+
+ @LocalTCC
+ public interface PureLocalTCCInterface {
+ boolean doTccOperation();
+ }
+
+ public static class PureLocalTCCInterfaceImpl implements
PureLocalTCCInterface {
+ @Override
+ public boolean doTccOperation() {
+ return true;
+ }
+ }
+
+ // Pure @SagaTransactional scenarios
+ @SagaTransactional
+ public static class PureSagaTransactionalService {
+ public boolean doSagaOperation() {
+ return true;
+ }
+ }
+
+ @SagaTransactional
+ public interface PureSagaTransactionalInterface {
+ boolean doSagaOperation();
+ }
+
+ public static class PureSagaTransactionalInterfaceImpl implements
PureSagaTransactionalInterface {
+ @Override
+ public boolean doSagaOperation() {
+ return true;
+ }
+ }
+
+ // Mixed scenarios - for comprehensive parser separation testing
+ @LocalTCC
+ public static class LocalTCCImplWithSagaInterface implements
PureSagaTransactionalInterface {
+ @Override
+ public boolean doSagaOperation() {
+ return true;
+ }
+ }
+
+ @SagaTransactional
+ public static class SagaImplWithLocalTCCInterface implements
PureLocalTCCInterface {
+ @Override
+ public boolean doTccOperation() {
+ return true;
+ }
+ }
+
+ // No annotation control groups
+ public static class NoAnnotationService {
+ public boolean doSomething() {
+ return true;
+ }
+ }
+
+ @BeforeEach
+ public void setUp() {
+ localTCCParser = new LocalTCCRemotingParser();
+ sagaTransactionalParser = new SagaTransactionalRemotingParser();
+ }
+
+ // Test LocalTCCRemotingParser behavior
+ @Test
+ public void testLocalTCCParser_ShouldOnlyRecognizeLocalTCC() {
+ // Should recognize @LocalTCC
+ PureLocalTCCService localTCCService = new PureLocalTCCService();
+ assertTrue(localTCCParser.isService(localTCCService,
"localTCCService"));
+ assertTrue(localTCCParser.isReference(localTCCService,
"localTCCService"));
+
+ RemotingDesc localTCCDesc =
localTCCParser.getServiceDesc(localTCCService, "localTCCService");
+ assertNotNull(localTCCDesc);
+ assertEquals(PureLocalTCCService.class,
localTCCDesc.getServiceClass());
+
+ // Should NOT recognize @SagaTransactional
+ PureSagaTransactionalService sagaService = new
PureSagaTransactionalService();
+ assertFalse(localTCCParser.isService(sagaService, "sagaService"));
+ assertFalse(localTCCParser.isReference(sagaService, "sagaService"));
+
+ RemotingDesc sagaDesc = localTCCParser.getServiceDesc(sagaService,
"sagaService");
+ assertNull(sagaDesc);
+
+ // Should NOT recognize no annotation
+ NoAnnotationService noAnnotationService = new NoAnnotationService();
+ assertFalse(localTCCParser.isService(noAnnotationService,
"noAnnotationService"));
+ assertNull(localTCCParser.getServiceDesc(noAnnotationService,
"noAnnotationService"));
+ }
+
+ @Test
+ public void
testSagaTransactionalParser_ShouldOnlyRecognizeSagaTransactional() {
+ // Should recognize @SagaTransactional
+ PureSagaTransactionalService sagaService = new
PureSagaTransactionalService();
+ assertTrue(sagaTransactionalParser.isService(sagaService,
"sagaService"));
+ assertTrue(sagaTransactionalParser.isReference(sagaService,
"sagaService"));
+
+ RemotingDesc sagaDesc =
sagaTransactionalParser.getServiceDesc(sagaService, "sagaService");
+ assertNotNull(sagaDesc);
+ assertEquals(PureSagaTransactionalService.class,
sagaDesc.getServiceClass());
+
+ // Should NOT recognize @LocalTCC
+ PureLocalTCCService localTCCService = new PureLocalTCCService();
+ assertFalse(sagaTransactionalParser.isService(localTCCService,
"localTCCService"));
+ assertFalse(sagaTransactionalParser.isReference(localTCCService,
"localTCCService"));
+
+ RemotingDesc localTCCDesc =
sagaTransactionalParser.getServiceDesc(localTCCService, "localTCCService");
+ assertNull(localTCCDesc);
+
+ // Should NOT recognize no annotation
+ NoAnnotationService noAnnotationService = new NoAnnotationService();
+ assertFalse(sagaTransactionalParser.isService(noAnnotationService,
"noAnnotationService"));
+ assertNull(sagaTransactionalParser.getServiceDesc(noAnnotationService,
"noAnnotationService"));
+ }
+
+ @Test
+ public void testInterfaceAnnotationHandling_BothParsers() {
+ // LocalTCC interface implementation
+ PureLocalTCCInterfaceImpl localTCCImpl = new
PureLocalTCCInterfaceImpl();
+
+ // LocalTCCParser should recognize it
+ assertTrue(localTCCParser.isService(localTCCImpl, "localTCCImpl"));
+ RemotingDesc localTCCDesc =
localTCCParser.getServiceDesc(localTCCImpl, "localTCCImpl");
+ assertNotNull(localTCCDesc);
+ assertEquals(PureLocalTCCInterface.class,
localTCCDesc.getServiceClass());
+
+ // SagaTransactionalParser should NOT recognize it
+ assertFalse(sagaTransactionalParser.isService(localTCCImpl,
"localTCCImpl"));
+ assertNull(sagaTransactionalParser.getServiceDesc(localTCCImpl,
"localTCCImpl"));
+
+ // SagaTransactional interface implementation
+ PureSagaTransactionalInterfaceImpl sagaImpl = new
PureSagaTransactionalInterfaceImpl();
+
+ // SagaTransactionalParser should recognize it
+ assertTrue(sagaTransactionalParser.isService(sagaImpl, "sagaImpl"));
+ RemotingDesc sagaDesc =
sagaTransactionalParser.getServiceDesc(sagaImpl, "sagaImpl");
+ assertNotNull(sagaDesc);
+ assertEquals(PureSagaTransactionalInterface.class,
sagaDesc.getServiceClass());
+
+ // LocalTCCParser should NOT recognize it
+ assertFalse(localTCCParser.isService(sagaImpl, "sagaImpl"));
+ assertNull(localTCCParser.getServiceDesc(sagaImpl, "sagaImpl"));
+ }
+
+ @Test
+ public void testMixedAnnotationScenarios_ParserPriority() {
+ // @LocalTCC implementation with @SagaTransactional interface
+ LocalTCCImplWithSagaInterface mixedService1 = new
LocalTCCImplWithSagaInterface();
+
+ // LocalTCCParser should recognize the @LocalTCC on implementation
+ assertTrue(localTCCParser.isService(mixedService1, "mixedService1"));
+ RemotingDesc desc1 = localTCCParser.getServiceDesc(mixedService1,
"mixedService1");
+ assertNotNull(desc1);
+ assertEquals(LocalTCCImplWithSagaInterface.class,
desc1.getServiceClass());
+
+ // SagaTransactionalParser should recognize the @SagaTransactional on
interface
+ assertTrue(sagaTransactionalParser.isService(mixedService1,
"mixedService1"));
+ RemotingDesc desc2 =
sagaTransactionalParser.getServiceDesc(mixedService1, "mixedService1");
+ assertNotNull(desc2);
+ assertEquals(PureSagaTransactionalInterface.class,
desc2.getServiceClass());
+
+ // @SagaTransactional implementation with @LocalTCC interface
+ SagaImplWithLocalTCCInterface mixedService2 = new
SagaImplWithLocalTCCInterface();
+
+ // LocalTCCParser should recognize the @LocalTCC on interface
+ assertTrue(localTCCParser.isService(mixedService2, "mixedService2"));
+ RemotingDesc desc3 = localTCCParser.getServiceDesc(mixedService2,
"mixedService2");
+ assertNotNull(desc3);
+ assertEquals(PureLocalTCCInterface.class, desc3.getServiceClass());
+
+ // SagaTransactionalParser should recognize the @SagaTransactional on
implementation
+ assertTrue(sagaTransactionalParser.isService(mixedService2,
"mixedService2"));
+ RemotingDesc desc4 =
sagaTransactionalParser.getServiceDesc(mixedService2, "mixedService2");
+ assertNotNull(desc4);
+ assertEquals(SagaImplWithLocalTCCInterface.class,
desc4.getServiceClass());
+ }
+
+ @Test
+ public void testBothParsersWorkIndependently() {
+ // Create services for both parsers
+ PureLocalTCCService localTCCService = new PureLocalTCCService();
+ PureSagaTransactionalService sagaService = new
PureSagaTransactionalService();
+ NoAnnotationService noAnnotationService = new NoAnnotationService();
+
+ // Test all combinations to ensure no cross-contamination
+
+ // LocalTCC service
+ assertTrue(localTCCParser.isService(localTCCService, "test"));
+ assertFalse(sagaTransactionalParser.isService(localTCCService,
"test"));
+
+ // Saga service
+ assertFalse(localTCCParser.isService(sagaService, "test"));
+ assertTrue(sagaTransactionalParser.isService(sagaService, "test"));
+
+ // No annotation service
+ assertFalse(localTCCParser.isService(noAnnotationService, "test"));
+ assertFalse(sagaTransactionalParser.isService(noAnnotationService,
"test"));
+ }
+
+ @Test
+ public void testParserProtocolConsistency() {
+ // Both parsers should use the same protocol
+ assertEquals(localTCCParser.getProtocol(),
sagaTransactionalParser.getProtocol());
+ }
+
+ @Test
+ public void testRemotingDescConsistency_BothParsers() {
+ PureLocalTCCService localTCCService = new PureLocalTCCService();
+ PureSagaTransactionalService sagaService = new
PureSagaTransactionalService();
+
+ RemotingDesc localTCCDesc =
localTCCParser.getServiceDesc(localTCCService, "test");
+ RemotingDesc sagaDesc =
sagaTransactionalParser.getServiceDesc(sagaService, "test");
+
+ // Both should have consistent RemotingDesc structure
+ assertNotNull(localTCCDesc);
+ assertNotNull(sagaDesc);
+
+ // Both should indicate service and reference
+ assertTrue(localTCCDesc.isService());
+ assertTrue(localTCCDesc.isReference());
+ assertTrue(sagaDesc.isService());
+ assertTrue(sagaDesc.isReference());
+
+ // Both should use same protocol
+ assertEquals(localTCCDesc.getProtocol(), sagaDesc.getProtocol());
+
+ // Both should have proper target bean references
+ assertEquals(localTCCService, localTCCDesc.getTargetBean());
+ assertEquals(sagaService, sagaDesc.getTargetBean());
+ }
+
+ @Test
+ public void testNullBeanHandling_BothParsers() {
+ // Both parsers should handle null beans consistently
+ try {
+ localTCCParser.isService(null, "test");
+ assertTrue(false, "Should throw exception for null bean");
+ } catch (Exception e) {
+ // Expected
+ }
+
+ try {
+ sagaTransactionalParser.isService(null, "test");
+ assertTrue(false, "Should throw exception for null bean");
+ } catch (Exception e) {
+ // Expected
+ }
+ }
+}
diff --git
a/test/src/test/java/org/apache/seata/saga/annotation/SagaTransactionalAnnotationAction.java
b/test/src/test/java/org/apache/seata/saga/annotation/SagaTransactionalAnnotationAction.java
new file mode 100644
index 0000000000..c1761f5894
--- /dev/null
+++
b/test/src/test/java/org/apache/seata/saga/annotation/SagaTransactionalAnnotationAction.java
@@ -0,0 +1,50 @@
+/*
+ * 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.saga.rm.api.SagaTransactional;
+
+import java.util.List;
+
+/**
+ * The interface Saga action using the new @SagaTransactional annotation.
+ * This demonstrates the new way to avoid using @LocalTCC in Saga scenarios.
+ */
+@SagaTransactional
+public interface SagaTransactionalAnnotationAction {
+
+ /**
+ * Commit transaction
+ *
+ * @param actionContext the action context
+ * @param a the a
+ * @param b the b
+ * @param sagaParam the saga param
+ * @return the boolean
+ */
+ boolean commit(BusinessActionContext actionContext, int a, List b,
SagaParam sagaParam);
+
+ /**
+ * Compensation transaction
+ *
+ * @param actionContext the action context
+ * @param param the param
+ * @return the boolean
+ */
+ boolean compensation(BusinessActionContext actionContext, SagaParam param);
+}
diff --git
a/test/src/test/java/org/apache/seata/saga/annotation/SagaTransactionalAnnotationActionImpl.java
b/test/src/test/java/org/apache/seata/saga/annotation/SagaTransactionalAnnotationActionImpl.java
new file mode 100644
index 0000000000..a8ef96419c
--- /dev/null
+++
b/test/src/test/java/org/apache/seata/saga/annotation/SagaTransactionalAnnotationActionImpl.java
@@ -0,0 +1,57 @@
+/*
+ * 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.saga.rm.api.CompensationBusinessAction;
+
+import java.util.List;
+
+/**
+ * Implementation that uses @SagaTransactional instead of @LocalTCC
+ * This demonstrates the recommended approach for Saga scenarios to avoid
confusion
+ */
+public class SagaTransactionalAnnotationActionImpl implements
SagaTransactionalAnnotationAction {
+
+ private boolean isCommit;
+
+ @Override
+ @CompensationBusinessAction(
+ name = "sagaActionWithLocalTransactional",
+ compensationMethod = "compensation",
+ compensationArgsClasses = {BusinessActionContext.class,
SagaParam.class})
+ public boolean commit(
+ BusinessActionContext actionContext,
+ @BusinessActionContextParameter("a") int a,
+ @BusinessActionContextParameter(paramName = "b", index = 0) List b,
+ @BusinessActionContextParameter(isParamInProperty = true)
SagaParam sagaParam) {
+ isCommit = true;
+ return a > 1;
+ }
+
+ @Override
+ public boolean compensation(
+ BusinessActionContext actionContext,
@BusinessActionContextParameter("sagaParam") SagaParam param) {
+ isCommit = false;
+ return true;
+ }
+
+ public boolean isCommit() {
+ return isCommit;
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]