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 1843f4f5746..e72c9c481a6 100644 --- a/changes/en-us/2.x.md +++ b/changes/en-us/2.x.md @@ -9,6 +9,7 @@ Add changes here for all PR submitted to the 2.x branch. - [[#6864](https://github.com/apache/incubator-seata/pull/6864)] support shentong database - [[#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 +- [[#6973](https://github.com/apache/incubator-seata/pull/6973)] support saga annotation - [[#6926](https://github.com/apache/incubator-seata/pull/6926)] support ssl communication for raft nodes diff --git a/changes/zh-cn/2.x.md b/changes/zh-cn/2.x.md index 635241f97db..25cb1cd7fe1 100644 --- a/changes/zh-cn/2.x.md +++ b/changes/zh-cn/2.x.md @@ -10,6 +10,7 @@ - [[#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 依赖 +- [[#6973](https://github.com/apache/incubator-seata/pull/6973)] 支持saga注解化 - [[#6926](https://github.com/apache/incubator-seata/pull/6926)] 支持Raft节点间的SSL通信 ### bugfix: 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 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 list = (List)paramValue; + List list = (List) paramValue; if (list.isEmpty()) { return null; } @@ -145,7 +146,7 @@ public static Object getByIndex(@Nonnull ParamType paramType, @Nonnull String pa paramValue = list.get(index); } else { LOGGER.warn("the {} named '{}' is not a `List`, so the 'index' field of '@{}' cannot be used on it", - paramType.getCode(), paramName, BusinessActionContextParameter.class.getSimpleName()); + paramType.getCode(), paramName, BusinessActionContextParameter.class.getSimpleName()); } return paramValue; @@ -271,12 +272,12 @@ public static T convertActionContext(String key, @Nullable Object value, @No // Same class or super class, can cast directly if (targetClazz.isAssignableFrom(value.getClass())) { - return (T)value; + return (T) value; } // String class if (String.class.equals(targetClazz)) { - return (T)value.toString(); + return (T) value.toString(); } // JSON to Object @@ -292,4 +293,30 @@ public static T convertActionContext(String key, @Nullable Object value, @No throw new FrameworkException(e, errorMsg); } } + + public static 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/integration-tx-api/src/main/java/org/apache/seata/integration/tx/api/interceptor/InvocationHandlerType.java b/integration-tx-api/src/main/java/org/apache/seata/integration/tx/api/interceptor/InvocationHandlerType.java index 3825f45e4ce..d9ac494c4cd 100644 --- a/integration-tx-api/src/main/java/org/apache/seata/integration/tx/api/interceptor/InvocationHandlerType.java +++ b/integration-tx-api/src/main/java/org/apache/seata/integration/tx/api/interceptor/InvocationHandlerType.java @@ -29,6 +29,10 @@ public enum InvocationHandlerType { /** * TwoPhase InvocationHandler */ - TwoPhaseAnnotation + TwoPhaseAnnotation, + /** + * SagaAnnotation InvocationHandler + */ + SagaAnnotation } diff --git a/integration-tx-api/src/main/java/org/apache/seata/integration/tx/api/interceptor/parser/DefaultResourceRegisterParser.java b/integration-tx-api/src/main/java/org/apache/seata/integration/tx/api/interceptor/parser/DefaultResourceRegisterParser.java deleted file mode 100644 index deead0b6e20..00000000000 --- a/integration-tx-api/src/main/java/org/apache/seata/integration/tx/api/interceptor/parser/DefaultResourceRegisterParser.java +++ /dev/null @@ -1,59 +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 org.apache.seata.integration.tx.api.interceptor.parser; - -import org.apache.seata.common.loader.EnhancedServiceLoader; -import org.apache.seata.common.util.CollectionUtils; - -import java.util.ArrayList; -import java.util.List; - - -public class DefaultResourceRegisterParser { - - protected static List allRegisterResourceParsers = new ArrayList<>(); - - public void registerResource(Object target, String beanName) { - for (RegisterResourceParser registerResourceParser : allRegisterResourceParsers) { - registerResourceParser.registerResource(target, beanName); - } - } - - private static class SingletonHolder { - private static final DefaultResourceRegisterParser INSTANCE = new DefaultResourceRegisterParser(); - } - - public static DefaultResourceRegisterParser get() { - return DefaultResourceRegisterParser.SingletonHolder.INSTANCE; - } - - protected DefaultResourceRegisterParser() { - initResourceRegisterParser(); - } - - /** - * init parsers - */ - protected void initResourceRegisterParser() { - List registerResourceParsers = EnhancedServiceLoader.loadAll(RegisterResourceParser.class); - if (CollectionUtils.isNotEmpty(registerResourceParsers)) { - allRegisterResourceParsers.addAll(registerResourceParsers); - } - } - - -} diff --git a/saga/pom.xml b/saga/pom.xml index 68185fed444..0d05dbb5fa3 100644 --- a/saga/pom.xml +++ b/saga/pom.xml @@ -38,6 +38,7 @@ seata-saga-rm seata-saga-engine-store seata-saga-spring + seata-saga-annotation diff --git a/saga/seata-saga-annotation/pom.xml b/saga/seata-saga-annotation/pom.xml new file mode 100644 index 00000000000..ee9881c6825 --- /dev/null +++ b/saga/seata-saga-annotation/pom.xml @@ -0,0 +1,51 @@ + + + + + seata-saga + org.apache.seata + ${revision} + + 4.0.0 + seata-saga-annotation + seata-saga-annotation ${project.version} + saga annotation module for Seata built with Maven + + + + ${project.groupId} + seata-core + ${project.version} + + + ${project.groupId} + seata-rm + ${project.version} + + + ${project.groupId} + seata-integration-tx-api + ${project.version} + + + + diff --git a/saga/seata-saga-annotation/src/main/java/org/apache/seata/saga/rm/RMHandlerSagaAnnotation.java b/saga/seata-saga-annotation/src/main/java/org/apache/seata/saga/rm/RMHandlerSagaAnnotation.java new file mode 100644 index 00000000000..21dad9e5b47 --- /dev/null +++ b/saga/seata-saga-annotation/src/main/java/org/apache/seata/saga/rm/RMHandlerSagaAnnotation.java @@ -0,0 +1,39 @@ +/* + * 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; + +import org.apache.seata.core.model.BranchType; +import org.apache.seata.core.model.ResourceManager; +import org.apache.seata.rm.AbstractRMHandler; +import org.apache.seata.rm.DefaultResourceManager; + +/** + * The type Rm handler SagaAnnotation. + */ +public class RMHandlerSagaAnnotation extends AbstractRMHandler { + + @Override + protected ResourceManager getResourceManager() { + return DefaultResourceManager.get().getResourceManager(BranchType.SAGA_ANNOTATION); + } + + @Override + public BranchType getBranchType() { + return BranchType.SAGA_ANNOTATION; + } + +} diff --git a/saga/seata-saga-annotation/src/main/java/org/apache/seata/saga/rm/SagaAnnotationResource.java b/saga/seata-saga-annotation/src/main/java/org/apache/seata/saga/rm/SagaAnnotationResource.java new file mode 100644 index 00000000000..f5d1ba04355 --- /dev/null +++ b/saga/seata-saga-annotation/src/main/java/org/apache/seata/saga/rm/SagaAnnotationResource.java @@ -0,0 +1,208 @@ +/* + * 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; + +import java.lang.reflect.Method; + +import org.apache.seata.core.model.BranchType; +import org.apache.seata.core.model.Resource; + +/** + * The type Saga annotation resource. + */ +public class SagaAnnotationResource implements Resource { + + private String resourceGroupId = "DEFAULT"; + + private String appName; + + private String actionName; + + private Object targetBean; + + private String compensationMethodName; + + private Method compensationMethod; + + private Class[] compensationArgsClasses; + + private String[] phaseTwoCompensationKeys; + + @Override + public String getResourceGroupId() { + return resourceGroupId; + } + + /** + * Sets resource group id. + * + * @param resourceGroupId the resource group id + */ + public void setResourceGroupId(String resourceGroupId) { + this.resourceGroupId = resourceGroupId; + } + + @Override + public String getResourceId() { + return actionName; + } + + @Override + public BranchType getBranchType() { + return BranchType.SAGA_ANNOTATION; + } + + /** + * Gets app name. + * + * @return the app name + */ + public String getAppName() { + return appName; + } + + /** + * Sets app name. + * + * @param appName the app name + */ + public void setAppName(String appName) { + this.appName = appName; + } + + /** + * Gets action name. + * + * @return the action name + */ + public String getActionName() { + return actionName; + } + + /** + * Sets action name. + * + * @param actionName the action name + */ + public void setActionName(String actionName) { + this.actionName = actionName; + } + + /** + * Gets target bean. + * + * @return the target bean + */ + public Object getTargetBean() { + return targetBean; + } + + /** + * Sets target bean. + * + * @param targetBean the target bean + */ + public void setTargetBean(Object targetBean) { + this.targetBean = targetBean; + } + + /** + * Gets compensation method. + * + * @return the rollback method + */ + public Method getCompensationMethod() { + return compensationMethod; + } + + /** + * Sets compensation method. + * + * @param compensationMethod the rollback method + */ + public void setCompensationMethod(Method compensationMethod) { + this.compensationMethod = compensationMethod; + } + + /** + * Gets compensation method name. + * + * @return the rollback method name + */ + public String getCompensationMethodName() { + return compensationMethodName; + } + + /** + * Sets compensation method name. + * + * @param compensationMethodName the rollback method name + */ + public void setCompensationMethodName(String compensationMethodName) { + this.compensationMethodName = compensationMethodName; + } + + /** + * get compensation method args + * + * @return class array + */ + public Class[] getCompensationArgsClasses() { + return compensationArgsClasses; + } + + /** + * set compensation method args + * + * @param compensationArgsClasses rollbackArgsClasses + */ + public void setCompensationArgsClasses(Class[] compensationArgsClasses) { + this.compensationArgsClasses = compensationArgsClasses; + } + + /** + * get compensation method args keys + * + * @return keys array + */ + public String[] getPhaseTwoCompensationKeys() { + return phaseTwoCompensationKeys; + } + + /** + * set compensation method args key + * + * @param phaseTwoCompensationKeys phaseTwoCompensationKeys + */ + public void setPhaseTwoCompensationKeys(String[] phaseTwoCompensationKeys) { + this.phaseTwoCompensationKeys = phaseTwoCompensationKeys; + } + + @Override + public int hashCode() { + return actionName.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof SagaAnnotationResource)) { + return false; + } + return this.actionName.equals(((SagaAnnotationResource) obj).actionName); + } + +} \ No newline at end of file diff --git a/saga/seata-saga-annotation/src/main/java/org/apache/seata/saga/rm/SagaAnnotationResourceManager.java b/saga/seata-saga-annotation/src/main/java/org/apache/seata/saga/rm/SagaAnnotationResourceManager.java new file mode 100644 index 00000000000..ea802b16781 --- /dev/null +++ b/saga/seata-saga-annotation/src/main/java/org/apache/seata/saga/rm/SagaAnnotationResourceManager.java @@ -0,0 +1,153 @@ +/* + * 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; + +import org.apache.seata.common.exception.ExceptionUtil; +import org.apache.seata.common.exception.RepeatRegistrationException; +import org.apache.seata.common.exception.ShouldNeverHappenException; +import org.apache.seata.core.exception.TransactionException; +import org.apache.seata.core.model.BranchStatus; +import org.apache.seata.core.model.BranchType; +import org.apache.seata.core.model.Resource; +import org.apache.seata.integration.tx.api.remoting.TwoPhaseResult; +import org.apache.seata.rm.AbstractResourceManager; +import org.apache.seata.rm.tcc.api.BusinessActionContext; +import org.apache.seata.rm.tcc.api.BusinessActionContextUtil; + +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Saga annotation resource manager + */ +public class SagaAnnotationResourceManager extends AbstractResourceManager { + + /** + * TCC resource cache + */ + private final Map resourceCache = new ConcurrentHashMap<>(); + + @Override + public void registerResource(Resource resource) { + String resourceId = resource.getResourceId(); + SagaAnnotationResource newResource = (SagaAnnotationResource) resource; + SagaAnnotationResource oldResource = (SagaAnnotationResource) resourceCache.get(resourceId); + + if (oldResource != null) { + Object newResourceBean = newResource.getTargetBean(); + Object oldResourceBean = oldResource.getTargetBean(); + if (newResourceBean != oldResourceBean) { + throw new RepeatRegistrationException(String.format("Same SagaAnnotation resource name <%s> between method1 <%s> of class1 <%s> and method2 <%s> of class2 <%s>, should be unique", + resourceId, + newResource.getActionName(), + newResourceBean.getClass().getName(), + oldResource.getActionName(), + oldResourceBean.getClass().getName())); + } + } + + resourceCache.put(resourceId, newResource); + super.registerResource(newResource); + } + + /** + * saga branch commit + * + * @param branchType + * @param xid Transaction id. + * @param branchId Branch id. + * @param resourceId Resource id. + * @param applicationData Application data bind with this branch. + * @return BranchStatus + */ + @Override + public BranchStatus branchCommit(BranchType branchType, String xid, long branchId, String resourceId, String applicationData) { + //impossible to reach here + return BranchStatus.PhaseTwo_Committed; + } + + @Override + public BranchStatus branchRollback(BranchType branchType, String xid, long branchId, String resourceId, String applicationData) throws TransactionException { + SagaAnnotationResource resource = (SagaAnnotationResource) resourceCache.get(resourceId); + if (resource == null) { + throw new ShouldNeverHappenException(String.format("SagaAnnotation resource is not exist, resourceId: %s", resourceId)); + } + + Object targetBean = resource.getTargetBean(); + Method compensationMethod = resource.getCompensationMethod(); + if (targetBean == null || compensationMethod == null) { + throw new ShouldNeverHappenException(String.format("SagaAnnotation resource is not available, resourceId: %s", resourceId)); + } + + try { + BusinessActionContext businessActionContext = BusinessActionContextUtil.getBusinessActionContext(xid, branchId, resourceId, + applicationData); + Object[] args = this.getTwoPhaseRollbackArgs(resource, businessActionContext); + BusinessActionContextUtil.setContext(businessActionContext); + + boolean result; + Object ret = compensationMethod.invoke(targetBean, args); + if (ret != null) { + if (ret instanceof TwoPhaseResult) { + result = ((TwoPhaseResult) ret).isSuccess(); + } else { + result = (boolean) ret; + } + } else { + result = true; + } + + LOGGER.info("SagaAnnotation resource rollback result : {}, xid: {}, branchId: {}, resourceId: {}", result, xid, branchId, resourceId); + return result ? BranchStatus.PhaseTwo_Rollbacked : BranchStatus.PhaseTwo_RollbackFailed_Retryable; + } catch (Throwable t) { + String msg = String.format("rollback SagaAnnotation resource error, resourceId: %s, xid: %s.", resourceId, xid); + LOGGER.error(msg, ExceptionUtil.unwrap(t)); + return BranchStatus.PhaseTwo_RollbackFailed_Retryable; + } finally { + BusinessActionContextUtil.clear(); + } + } + + @Override + public Map getManagedResources() { + return resourceCache; + } + + @Override + public BranchType getBranchType() { + return BranchType.SAGA_ANNOTATION; + } + + private Object[] getTwoPhaseRollbackArgs(SagaAnnotationResource resource, BusinessActionContext businessActionContext) { + String[] keys = resource.getPhaseTwoCompensationKeys(); + Class[] argsRollbackClasses = resource.getCompensationArgsClasses(); + return getTwoPhaseMethodParams(keys, argsRollbackClasses, businessActionContext); + } + + protected Object[] getTwoPhaseMethodParams(String[] keys, Class[] argsClasses, BusinessActionContext businessActionContext) { + Object[] args = new Object[argsClasses.length]; + for (int i = 0; i < argsClasses.length; i++) { + if (argsClasses[i].equals(BusinessActionContext.class)) { + args[i] = businessActionContext; + } else { + args[i] = businessActionContext.getActionContext(keys[i], argsClasses[i]); + } + } + return args; + } +} \ No newline at end of file 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 new file mode 100644 index 00000000000..fe47e18fe73 --- /dev/null +++ b/saga/seata-saga-annotation/src/main/java/org/apache/seata/saga/rm/api/CompensationBusinessAction.java @@ -0,0 +1,71 @@ +/* + * 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; + +import org.apache.seata.rm.tcc.api.BusinessActionContext; + + +/** + * Saga annotation. + * Define a saga interface, which added on the commit method, if occurs rollback, compensation will be called. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +@Inherited +public @interface CompensationBusinessAction { + + /** + * saga bean name, must be unique + * + * @return the string + */ + String name(); + + /** + * compensation method name + * + * @return the string + */ + String compensationMethod() default "compensation"; + + /** + * delay branch report while sharing params to phase 2 to enhance performance + * + * @return isDelayReport + */ + boolean isDelayReport() default false; + + /** + * whether use fence (idempotent,non_rollback,suspend) + * + * @return the boolean + */ + boolean useFence() default false; + + /** + * compensation method's args + * + * @return the Class[] + */ + Class[] compensationArgsClasses() default {BusinessActionContext.class}; +} \ No newline at end of file diff --git a/saga/seata-saga-annotation/src/main/java/org/apache/seata/saga/rm/interceptor/SagaAnnotationActionInterceptorHandler.java b/saga/seata-saga-annotation/src/main/java/org/apache/seata/saga/rm/interceptor/SagaAnnotationActionInterceptorHandler.java new file mode 100644 index 00000000000..3685f249b51 --- /dev/null +++ b/saga/seata-saga-annotation/src/main/java/org/apache/seata/saga/rm/interceptor/SagaAnnotationActionInterceptorHandler.java @@ -0,0 +1,160 @@ +/* + * 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.interceptor; + +import org.apache.seata.common.Constants; +import org.apache.seata.common.util.ReflectionUtil; +import org.apache.seata.core.context.RootContext; +import org.apache.seata.core.model.BranchType; +import org.apache.seata.integration.tx.api.interceptor.ActionInterceptorHandler; +import org.apache.seata.integration.tx.api.interceptor.InvocationHandlerType; +import org.apache.seata.integration.tx.api.interceptor.InvocationWrapper; +import org.apache.seata.integration.tx.api.interceptor.SeataInterceptorPosition; +import org.apache.seata.integration.tx.api.interceptor.TwoPhaseBusinessActionParam; +import org.apache.seata.integration.tx.api.interceptor.handler.AbstractProxyInvocationHandler; +import org.apache.seata.saga.rm.api.CompensationBusinessAction; +import org.slf4j.MDC; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * saga-annotation invocationHandler + */ +public class SagaAnnotationActionInterceptorHandler extends AbstractProxyInvocationHandler { + + private Set methodsToProxy; + + protected Object targetBean; + + protected ActionInterceptorHandler actionInterceptorHandler = new ActionInterceptorHandler(); + + protected Map parseAnnotationCache = new ConcurrentHashMap<>(); + + public SagaAnnotationActionInterceptorHandler(Object targetBean, Set methodsToProxy) { + this.targetBean = targetBean; + this.methodsToProxy = methodsToProxy; + } + + + @Override + protected Object doInvoke(InvocationWrapper invocation) throws Throwable { + if (!RootContext.inGlobalTransaction() || RootContext.inSagaBranch()) { + //not in transaction, or this interceptor is disabled + return invocation.proceed(); + } + Method method = invocation.getMethod(); + Annotation businessAction = parseAnnotation(method); + + //try method + if (businessAction != null) { + //save the xid + String xid = RootContext.getXID(); + //save the previous branchType + BranchType previousBranchType = RootContext.getBranchType(); + //if not TCC, bind TCC branchType + if (getBranchType() != previousBranchType) { + RootContext.bindBranchType(getBranchType()); + } + try { + TwoPhaseBusinessActionParam businessActionParam = createTwoPhaseBusinessActionParam(businessAction); + return actionInterceptorHandler.proceed(method, invocation.getArguments(), xid, businessActionParam, + invocation::proceed); + } finally { + //if not TCC, unbind branchType + if (getBranchType() != previousBranchType) { + RootContext.unbindBranchType(); + } + //MDC remove branchId + MDC.remove(RootContext.MDC_KEY_BRANCH_ID); + } + } + + //not TCC try method + return invocation.proceed(); + } + + + @Override + public Set getMethodsToProxy() { + return methodsToProxy; + } + + @Override + public SeataInterceptorPosition getPosition() { + return SeataInterceptorPosition.Any; + } + + @Override + public String type() { + return InvocationHandlerType.SagaAnnotation.name(); + } + + @Override + public int order() { + return 2; + } + + private TwoPhaseBusinessActionParam createTwoPhaseBusinessActionParam(Annotation annotation) { + CompensationBusinessAction businessAction = (CompensationBusinessAction) annotation; + + TwoPhaseBusinessActionParam businessActionParam = new TwoPhaseBusinessActionParam(); + businessActionParam.setActionName(businessAction.name()); + businessActionParam.setDelayReport(businessAction.isDelayReport()); + businessActionParam.setUseCommonFence(businessAction.useFence()); + businessActionParam.setBranchType(BranchType.SAGA_ANNOTATION); + + Map businessActionContextMap = new HashMap<>(4); + businessActionContextMap.put(Constants.ROLLBACK_METHOD, businessAction.compensationMethod()); + businessActionContextMap.put(Constants.ACTION_NAME, businessAction.name()); + businessActionContextMap.put(Constants.USE_COMMON_FENCE, businessAction.useFence()); + businessActionParam.setBusinessActionContext(businessActionContextMap); + + return businessActionParam; + } + + private Annotation parseAnnotation(Method methodKey) { + return parseAnnotationCache.computeIfAbsent(methodKey, method -> { + Annotation twoPhaseBusinessAction = method.getAnnotation(getAnnotationClass()); + if (twoPhaseBusinessAction == null) { + Set> interfaceClasses = ReflectionUtil.getInterfaces(targetBean.getClass()); + for (Class interClass : interfaceClasses) { + try { + Method m = interClass.getMethod(method.getName(), method.getParameterTypes()); + twoPhaseBusinessAction = m.getAnnotation(getAnnotationClass()); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + } + return twoPhaseBusinessAction; + }); + } + + private BranchType getBranchType() { + return BranchType.SAGA_ANNOTATION; + } + + private Class getAnnotationClass() { + return CompensationBusinessAction.class; + } + +} \ No newline at end of file diff --git a/saga/seata-saga-annotation/src/main/java/org/apache/seata/saga/rm/interceptor/parser/SagaAnnotationActionInterceptorParser.java b/saga/seata-saga-annotation/src/main/java/org/apache/seata/saga/rm/interceptor/parser/SagaAnnotationActionInterceptorParser.java new file mode 100644 index 00000000000..84ba16c9c58 --- /dev/null +++ b/saga/seata-saga-annotation/src/main/java/org/apache/seata/saga/rm/interceptor/parser/SagaAnnotationActionInterceptorParser.java @@ -0,0 +1,104 @@ +/* + * 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.interceptor.parser; + +import org.apache.seata.common.exception.FrameworkException; +import org.apache.seata.common.util.ReflectionUtil; +import org.apache.seata.core.model.Resource; +import org.apache.seata.integration.tx.api.interceptor.ActionContextUtil; +import org.apache.seata.integration.tx.api.interceptor.handler.ProxyInvocationHandler; +import org.apache.seata.integration.tx.api.interceptor.parser.IfNeedEnhanceBean; +import org.apache.seata.integration.tx.api.interceptor.parser.InterfaceParser; +import org.apache.seata.integration.tx.api.interceptor.parser.NeedEnhanceEnum; +import org.apache.seata.integration.tx.api.remoting.parser.DefaultRemotingParser; +import org.apache.seata.rm.DefaultResourceManager; +import org.apache.seata.saga.rm.SagaAnnotationResource; +import org.apache.seata.saga.rm.api.CompensationBusinessAction; +import org.apache.seata.saga.rm.interceptor.SagaAnnotationActionInterceptorHandler; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * saga-annotation proxyInvocationHandler parser + * used to identify the saga annotation @CompensationBusinessAction and return the proxy handler. + */ +public class SagaAnnotationActionInterceptorParser implements InterfaceParser { + + @Override + public ProxyInvocationHandler parserInterfaceToProxy(Object target, String objectName) { + 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 + registerResource(target, methodClassMap); + + return new SagaAnnotationActionInterceptorHandler(target, methodsToProxy.stream().map(Method::getName).collect(Collectors.toSet())); + } + + private void registerResource(Object target, Map> methodClassMap) { + try { + for (Map.Entry> methodClassEntry : methodClassMap.entrySet()) { + Method method = methodClassEntry.getKey(); + Annotation annotation = method.getAnnotation(getAnnotationClass()); + if (annotation != null) { + Resource resource = createResource(target, methodClassEntry.getValue(), annotation); + //registry resource + DefaultResourceManager.get().registerResource(resource); + } + } + } catch (Throwable t) { + throw new FrameworkException(t, "register SagaAnnotation resource error"); + } + } + + + @Override + public IfNeedEnhanceBean parseIfNeedEnhancement(Class beanClass) { + IfNeedEnhanceBean ifNeedEnhanceBean = new IfNeedEnhanceBean(); + //current support remote service enhance + if (DefaultRemotingParser.get().isService(beanClass)) { + ifNeedEnhanceBean.setIfNeed(true); + ifNeedEnhanceBean.setNeedEnhanceEnum(NeedEnhanceEnum.SERVICE_BEAN); + } + return ifNeedEnhanceBean; + } + + protected Class getAnnotationClass() { + return CompensationBusinessAction.class; + } + + protected Resource createResource(Object targetBean, Class serviceClass, Annotation annotation) throws NoSuchMethodException { + CompensationBusinessAction compensationBusinessAction = (CompensationBusinessAction) annotation; + SagaAnnotationResource sagaAnnotationResource = new SagaAnnotationResource(); + sagaAnnotationResource.setActionName(compensationBusinessAction.name()); + sagaAnnotationResource.setTargetBean(targetBean); + sagaAnnotationResource.setCompensationMethodName(compensationBusinessAction.compensationMethod()); + Method compensationMethod = serviceClass.getMethod(compensationBusinessAction.compensationMethod(), compensationBusinessAction.compensationArgsClasses()); + sagaAnnotationResource.setCompensationMethod(compensationMethod); + sagaAnnotationResource.setCompensationArgsClasses(compensationBusinessAction.compensationArgsClasses()); + sagaAnnotationResource.setPhaseTwoCompensationKeys(ActionContextUtil.getTwoPhaseArgs(sagaAnnotationResource.getCompensationMethod(), compensationBusinessAction.compensationArgsClasses())); + + return sagaAnnotationResource; + } +} diff --git a/compatible/src/main/resources/META-INF/services/org.apache.seata.integration.tx.api.interceptor.parser.RegisterResourceParser b/saga/seata-saga-annotation/src/main/resources/META-INF/services/org.apache.seata.core.model.ResourceManager similarity index 93% rename from compatible/src/main/resources/META-INF/services/org.apache.seata.integration.tx.api.interceptor.parser.RegisterResourceParser rename to saga/seata-saga-annotation/src/main/resources/META-INF/services/org.apache.seata.core.model.ResourceManager index 82c08236e87..fb0ebe6018a 100644 --- a/compatible/src/main/resources/META-INF/services/org.apache.seata.integration.tx.api.interceptor.parser.RegisterResourceParser +++ b/saga/seata-saga-annotation/src/main/resources/META-INF/services/org.apache.seata.core.model.ResourceManager @@ -14,4 +14,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -io.seata.rm.tcc.resource.parser.TccRegisterResourceParser \ No newline at end of file +org.apache.seata.saga.rm.SagaAnnotationResourceManager diff --git a/saga/seata-saga-annotation/src/main/resources/META-INF/services/org.apache.seata.integration.tx.api.interceptor.parser.InterfaceParser b/saga/seata-saga-annotation/src/main/resources/META-INF/services/org.apache.seata.integration.tx.api.interceptor.parser.InterfaceParser new file mode 100644 index 00000000000..099adeee7a7 --- /dev/null +++ b/saga/seata-saga-annotation/src/main/resources/META-INF/services/org.apache.seata.integration.tx.api.interceptor.parser.InterfaceParser @@ -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.interceptor.parser.SagaAnnotationActionInterceptorParser \ No newline at end of file diff --git a/tcc/src/main/resources/META-INF/services/org.apache.seata.integration.tx.api.interceptor.parser.RegisterResourceParser b/saga/seata-saga-annotation/src/main/resources/META-INF/services/org.apache.seata.rm.AbstractRMHandler similarity index 92% rename from tcc/src/main/resources/META-INF/services/org.apache.seata.integration.tx.api.interceptor.parser.RegisterResourceParser rename to saga/seata-saga-annotation/src/main/resources/META-INF/services/org.apache.seata.rm.AbstractRMHandler index 0df6a52001a..042138ccffd 100644 --- a/tcc/src/main/resources/META-INF/services/org.apache.seata.integration.tx.api.interceptor.parser.RegisterResourceParser +++ b/saga/seata-saga-annotation/src/main/resources/META-INF/services/org.apache.seata.rm.AbstractRMHandler @@ -14,4 +14,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -org.apache.seata.rm.tcc.resource.parser.TccRegisterResourceParser \ No newline at end of file +org.apache.seata.saga.rm.RMHandlerSagaAnnotation \ No newline at end of file diff --git a/server/src/main/java/org/apache/seata/server/transaction/saga/SagaAnnotationCore.java b/server/src/main/java/org/apache/seata/server/transaction/saga/SagaAnnotationCore.java new file mode 100644 index 00000000000..03a2709967c --- /dev/null +++ b/server/src/main/java/org/apache/seata/server/transaction/saga/SagaAnnotationCore.java @@ -0,0 +1,46 @@ +/* + * 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.server.transaction.saga; + +import org.apache.seata.core.exception.TransactionException; +import org.apache.seata.core.model.BranchStatus; +import org.apache.seata.core.model.BranchType; +import org.apache.seata.core.rpc.RemotingServer; +import org.apache.seata.server.coordinator.AbstractCore; +import org.apache.seata.server.session.BranchSession; +import org.apache.seata.server.session.GlobalSession; + +/** + * The type saga annotation core. + */ +public class SagaAnnotationCore extends AbstractCore { + + public SagaAnnotationCore(RemotingServer remotingServer) { + super(remotingServer); + } + + @Override + public BranchStatus branchCommit(GlobalSession globalSession, BranchSession branchSession) throws TransactionException { + //SAGA_ANNOTATION branch type, just mock commit + return BranchStatus.PhaseTwo_Committed; + } + + @Override + public BranchType getHandleBranchType() { + return BranchType.SAGA_ANNOTATION; + } +} diff --git a/server/src/main/resources/META-INF/services/org.apache.seata.server.coordinator.AbstractCore b/server/src/main/resources/META-INF/services/org.apache.seata.server.coordinator.AbstractCore index 538e5a4cae0..462a3cefc99 100644 --- a/server/src/main/resources/META-INF/services/org.apache.seata.server.coordinator.AbstractCore +++ b/server/src/main/resources/META-INF/services/org.apache.seata.server.coordinator.AbstractCore @@ -17,4 +17,5 @@ org.apache.seata.server.transaction.at.ATCore org.apache.seata.server.transaction.tcc.TccCore org.apache.seata.server.transaction.saga.SagaCore -org.apache.seata.server.transaction.xa.XACore \ No newline at end of file +org.apache.seata.server.transaction.xa.XACore +org.apache.seata.server.transaction.saga.SagaAnnotationCore \ No newline at end of file diff --git a/tcc/src/main/java/org/apache/seata/rm/tcc/interceptor/parser/TccActionInterceptorParser.java b/tcc/src/main/java/org/apache/seata/rm/tcc/interceptor/parser/TccActionInterceptorParser.java index 740690b19db..9d506919aaa 100644 --- a/tcc/src/main/java/org/apache/seata/rm/tcc/interceptor/parser/TccActionInterceptorParser.java +++ b/tcc/src/main/java/org/apache/seata/rm/tcc/interceptor/parser/TccActionInterceptorParser.java @@ -16,37 +16,41 @@ */ package org.apache.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 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.ActionContextUtil; import org.apache.seata.integration.tx.api.interceptor.handler.ProxyInvocationHandler; -import org.apache.seata.integration.tx.api.interceptor.parser.DefaultResourceRegisterParser; import org.apache.seata.integration.tx.api.interceptor.parser.IfNeedEnhanceBean; import org.apache.seata.integration.tx.api.interceptor.parser.InterfaceParser; import org.apache.seata.integration.tx.api.interceptor.parser.NeedEnhanceEnum; import org.apache.seata.integration.tx.api.remoting.parser.DefaultRemotingParser; +import org.apache.seata.rm.DefaultResourceManager; +import org.apache.seata.rm.tcc.TCCResource; import org.apache.seata.rm.tcc.api.TwoPhaseBusinessAction; import org.apache.seata.rm.tcc.interceptor.TccActionInterceptorHandler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -public class TccActionInterceptorParser implements InterfaceParser { +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; - private static final Logger LOGGER = LoggerFactory.getLogger(TccActionInterceptorParser.class); +public class TccActionInterceptorParser implements InterfaceParser { @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())); } @Override @@ -59,37 +63,50 @@ public IfNeedEnhanceBean parseIfNeedEnhancement(Class beanClass) { return ifNeedEnhanceBean; } - /** - * is TCC proxy-bean/target-bean: LocalTCC , the proxy bean of sofa:reference/dubbo:reference - * - * @param target the remoting desc - * @return boolean boolean - */ - - 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())); + protected void registerResource(Object target, Map> methodClassMap) { + try { + for (Map.Entry> methodClassEntry : methodClassMap.entrySet()) { + Method method = methodClassEntry.getKey(); + Annotation annotation = method.getAnnotation(getAnnotationClass()); + if (annotation != null) { + Resource resource = createResource(target, methodClassEntry.getValue(), method, annotation); + //registry resource + DefaultResourceManager.get().registerResource(resource); + } } + } catch (Throwable t) { + throw new FrameworkException(t, "register tcc resource error"); } + } - TwoPhaseBusinessAction twoPhaseBusinessAction; - for (Method method : methods) { - twoPhaseBusinessAction = method.getAnnotation(TwoPhaseBusinessAction.class); - if (twoPhaseBusinessAction != null) { - methodsToProxy.add(method.getName()); - } - } - if (methodsToProxy.isEmpty()) { - return Collections.emptySet(); + protected Class getAnnotationClass() { + return TwoPhaseBusinessAction.class; + } + + 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"); } - // sofa:reference / dubbo:reference, AOP - return methodsToProxy; + 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(ActionContextUtil.getTwoPhaseArgs(tccResource.getCommitMethod(), + twoPhaseBusinessAction.commitArgsClasses())); + tccResource.setPhaseTwoRollbackKeys(ActionContextUtil.getTwoPhaseArgs(tccResource.getRollbackMethod(), + twoPhaseBusinessAction.rollbackArgsClasses())); + return tccResource; } } diff --git a/tcc/src/main/java/org/apache/seata/rm/tcc/resource/parser/TccRegisterResourceParser.java b/tcc/src/main/java/org/apache/seata/rm/tcc/resource/parser/TccRegisterResourceParser.java deleted file mode 100644 index 0d72824bf92..00000000000 --- a/tcc/src/main/java/org/apache/seata/rm/tcc/resource/parser/TccRegisterResourceParser.java +++ /dev/null @@ -1,111 +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 org.apache.seata.rm.tcc.resource.parser; - -import java.lang.annotation.Annotation; -import java.lang.reflect.Method; -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; - -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.integration.tx.api.interceptor.ActionContextUtil; -import org.apache.seata.integration.tx.api.interceptor.parser.RegisterResourceParser; -import org.apache.seata.rm.DefaultResourceManager; -import org.apache.seata.rm.tcc.TCCResource; -import org.apache.seata.rm.tcc.api.BusinessActionContext; -import org.apache.seata.rm.tcc.api.BusinessActionContextParameter; -import org.apache.seata.rm.tcc.api.TwoPhaseBusinessAction; - -public class TccRegisterResourceParser implements RegisterResourceParser { - - @Override - public void registerResource(Object target, String beanName) { - try { - //service bean, registry resource - Class serviceClass = target.getClass(); - executeRegisterResource(target, new HashSet<>(Arrays.asList(serviceClass.getMethods())), target.getClass()); - Set> interfaceClasses = ReflectionUtil.getInterfaces(serviceClass); - for (Class interClass : interfaceClasses) { - executeRegisterResource(target, new HashSet<>(Arrays.asList(interClass.getMethods())), interClass); - } - } catch (Throwable t) { - throw new FrameworkException(t, "parser remoting service error"); - } - } - - - 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); - } - } - } - - 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/tcc/src/test/java/org/apache/seata/rm/tcc/resource/parser/TccRegisterResourceParserTest.java b/tcc/src/test/java/org/apache/seata/rm/tcc/resource/parser/TccRegisterResourceParserTest.java deleted file mode 100644 index b7f651afae9..00000000000 --- a/tcc/src/test/java/org/apache/seata/rm/tcc/resource/parser/TccRegisterResourceParserTest.java +++ /dev/null @@ -1,45 +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 org.apache.seata.rm.tcc.resource.parser; - -import java.lang.reflect.Method; - -import org.apache.seata.rm.tcc.TccAction; -import org.apache.seata.rm.tcc.TccParam; -import org.apache.seata.rm.tcc.api.BusinessActionContext; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - - -class TccRegisterResourceParserTest { - - TccRegisterResourceParser tccRegisterResourceParser = new TccRegisterResourceParser(); - - @Test - public void testGetTwoPhaseArgs() throws Exception { - Class[] argsCommitClasses = new Class[]{BusinessActionContext.class, TccParam.class, Integer.class}; - Method commitMethod = TccAction.class.getMethod("commitWithArg", argsCommitClasses); - Assertions.assertThrows(IllegalArgumentException.class, () -> { - tccRegisterResourceParser.getTwoPhaseArgs(commitMethod, argsCommitClasses); - }); - Class[] argsRollbackClasses = new Class[]{BusinessActionContext.class, TccParam.class}; - Method rollbackMethod = TccAction.class.getMethod("rollbackWithArg", argsRollbackClasses); - String[] keys = tccRegisterResourceParser.getTwoPhaseArgs(rollbackMethod, argsRollbackClasses); - Assertions.assertNull(keys[0]); - Assertions.assertEquals("tccParam", keys[1]); - } -} diff --git a/test/pom.xml b/test/pom.xml index fa7ec063945..3c43350502f 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -113,7 +113,6 @@ seata-spring ${project.version} - 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/integration-tx-api/src/main/java/org/apache/seata/integration/tx/api/interceptor/parser/RegisterResourceParser.java b/test/src/test/java/org/apache/seata/saga/annotation/NormalSagaAnnotationAction.java similarity index 61% rename from integration-tx-api/src/main/java/org/apache/seata/integration/tx/api/interceptor/parser/RegisterResourceParser.java rename to test/src/test/java/org/apache/seata/saga/annotation/NormalSagaAnnotationAction.java index 1300be74454..11aadcb88f7 100644 --- a/integration-tx-api/src/main/java/org/apache/seata/integration/tx/api/interceptor/parser/RegisterResourceParser.java +++ b/test/src/test/java/org/apache/seata/saga/annotation/NormalSagaAnnotationAction.java @@ -14,11 +14,25 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.seata.integration.tx.api.interceptor.parser; +package org.apache.seata.saga.annotation; +import org.apache.seata.rm.tcc.api.BusinessActionContext; -public interface RegisterResourceParser { +import java.util.List; - void registerResource(Object target, String beanName); +/** + * The interface saga action. + */ +public interface NormalSagaAnnotationAction { + + + boolean commit(BusinessActionContext actionContext, int a, List b, SagaParam sagaParam); -} + /** + * Rollback boolean. + * + * @param actionContext the action context + * @return the boolean + */ + boolean compensation(BusinessActionContext actionContext, 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..6062cce4597 --- /dev/null +++ b/test/src/test/java/org/apache/seata/saga/annotation/NormalSagaAnnotationActionImpl.java @@ -0,0 +1,49 @@ +/* + * 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; + +/** + * + */ +public class NormalSagaAnnotationActionImpl implements NormalSagaAnnotationAction { + + private boolean isCommit; + + + @Override + @CompensationBusinessAction(name = "sagaActionForTest", 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; + } +} \ 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