Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

springCloud服务使用@LocalTcc未生效或报错显示 #7047

Open
wxrqforever opened this issue Dec 4, 2024 · 13 comments
Open

springCloud服务使用@LocalTcc未生效或报错显示 #7047

wxrqforever opened this issue Dec 4, 2024 · 13 comments
Labels
type: bug Category issues or prs related to bug.

Comments

@wxrqforever
Copy link

wxrqforever commented Dec 4, 2024

一、相关组件版本

        <dependency>
            <groupId>org.apache.seata</groupId>
            <artifactId>seata-spring-boot-starter</artifactId>
            <version>>2.1.0</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba.nacos</groupId>
            <artifactId>nacos-client</artifactId>
            <version>2.4.2</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
            <version>2021.0.6.1</version>
            <exclusions>
                <exclusion>
                    <groupId>io.seata</groupId>
                    <artifactId>seata-spring-boot-starter</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
seata-server:2.1 

二、问题描述
业务想接入改造成TCC,所以先用一个简单的例子验证一下特性,发现一直达不到效果。

1.tcc相关注解在接口

代码如下:

public class GlobalTransactionTestController {
   @GetMapping("/testTcc")
    @GlobalTransactional
    public ResultGeneralModel<Boolean> testTcc() {
        log.info("测试tcc事务务");
        tccTestService.prepare(null,123);
        int i = 1/0;
        return ResultGeneralModel.newSuccess(Boolean.TRUE);
    }
}
@LocalTCC
public interface TccTestService {
    @TwoPhaseBusinessAction(name = "TccTestBean", commitMethod = "commit", rollbackMethod = "rollback")
    void prepare(BusinessActionContext actionContext,@BusinessActionContextParameter(paramName = "a") int a);;

    void commit(BusinessActionContext actionContext);


    void rollback(BusinessActionContext actionContext);
}
@Component
@Slf4j
public class TccTestServiceImpl implements TccTestService {

    @Override
    public void prepare(BusinessActionContext actionContext, int a) {
        //一些访问db的动作,省略了,非常简单
        log.info("prepare 被调用");
    }

    @Override
    public void commit(BusinessActionContext actionContext) {
        log.info("commit 被调用");

    }

    @Override
    public void rollback(BusinessActionContext actionContext) {
        log.info("rollback 被调用");

    }
}

启动正常,tcc resource注册成功:

INFO org.apache.seata.core.rpc.netty.RmNettyRemotingClient [main] [] [] will register resourceId:TccTestBean

代码执行后报错,报错日志:

2024-12-04 20:56:12.615 ERROR[http-nio-8080-exec-1] [] [] java.lang.NoSuchMethodException: org.springframework.aop.SpringProxy.prepare(org.apache.seata.rm.tcc.api.BusinessActionContext, int)
java.lang.RuntimeException: java.lang.NoSuchMethodException: org.springframework.aop.SpringProxy.prepare(org.apache.seata.rm.tcc.api.BusinessActionContext, int)
	at org.apache.seata.rm.tcc.interceptor.TccActionInterceptorHandler.lambda$parseAnnotation$0(TccActionInterceptorHandler.java:118)
	at java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1660)
	at org.apache.seata.rm.tcc.interceptor.TccActionInterceptorHandler.parseAnnotation(TccActionInterceptorHandler.java:103)
	at org.apache.seata.rm.tcc.interceptor.TccActionInterceptorHandler.doInvoke(TccActionInterceptorHandler.java:71)
	at org.apache.seata.integration.tx.api.interceptor.handler.AbstractProxyInvocationHandler.invoke(AbstractProxyInvocationHandler.java:43)
	at org.apache.seata.spring.annotation.AdapterSpringSeataInterceptor.invoke(AdapterSpringSeataInterceptor.java:44)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763)
	at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97)
	at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
	at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763)
	at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:708)
	at xxx.TccTestServiceImpl$$EnhancerBySpringCGLIB$$953891f3.prepare(<generated>)
	at xxxxx.seata.GlobalTransactionTestController.testTcc(GlobalTransactionTestController.java:77)
	at xxxxx.seata.GlobalTransactionTestController$$FastClassBySpringCGLIB$$14f5a059.invoke(<generated>)

debug定位了一下报错的地方,这里拿到的接口是SpringProxy,八成是spring代理的,这个接口里肯定是没有的preapre
org.apache.seata.rm.tcc.interceptor.TccActionInterceptorHandler#parseAnnotation

image

2.tcc相关注解在实现类
看了和tcc使用的相关issue,怀疑是注解的位置导致的,因此我将注解移动到了实现类中,

public interface TccTestService {
    void prepare(BusinessActionContext actionContext, int a);;

    void commit(BusinessActionContext actionContext);


    void rollback(BusinessActionContext actionContext);
}
@Component
@Slf4j
@LocalTCC
public class TccTestServiceImpl implements TccTestService {

    @Override
    @TwoPhaseBusinessAction(name = "TccTestBean", commitMethod = "commit", rollbackMethod = "rollback")
    public void prepare(BusinessActionContext actionContext, @BusinessActionContextParameter(paramName = "a") int a) {
        //一些访问db的动作,省略了,非常简单
        log.info("prepare 被调用");
    }

    @Override
    public void commit(BusinessActionContext actionContext) {
        log.info("commit 被调用");

    }

    @Override
    public void rollback(BusinessActionContext actionContext) {
        log.info("rollback 被调用");

    }
}

启动后,虽然tm 和rm注册成功. 但是并没有看到tcc resource注册成功的这条日志:

INFO org.apache.seata.core.rpc.netty.RmNettyRemotingClient [main] [] [] will register resourceId:TccTestBean

代码执行后,确实返回了/ by zero异常,但是cacnel方法并没有被回调。

甚至isLocalTCC都没有命中:

   private boolean isLocalTCC(Class<?> classType) {
        Set<Class<?>> interfaceClasses = ReflectionUtil.getInterfaces(classType);
        for (Class<?> interClass : interfaceClasses) {
            if (interClass.isAnnotationPresent(LocalTCC.class)) {
                return true;
            }
        }
        return classType.isAnnotationPresent(LocalTCC.class);
    }

以上,希望能得到帮助,多谢!

@slievrly
Copy link
Member

slievrly commented Dec 8, 2024

thanks for your feedback.

@slievrly slievrly added the type: bug Category issues or prs related to bug. label Dec 8, 2024
@wxrqforever
Copy link
Author

请问这个问题计划会在哪个版本的到修复

@wxrqforever
Copy link
Author

@funky-eyes 老哥 这个有计划修复的时间吗?是否有其他使用方式可以规避,或者有修复的大体方案,我这边也可以尝试修复,我这的业务需要使用localtcc的模式的需求,多谢帮忙看看。

@funky-eyes
Copy link
Contributor

你可以提供一个稳定复现的最小demo吗?
Can you provide a stable reproduced minimum demo?

@wxrqforever
Copy link
Author

你可以提供一个稳定复现的最小demo吗?
Can you provide a stable reproduced minimum demo?

明白 我尽快提供 多谢

@wxrqforever
Copy link
Author

你可以提供一个稳定复现的最小demo吗?
Can you provide a stable reproduced minimum demo?

@funky-eyes 最小可复现的demo已提交 https://github.com/wxrqforever/localTccDemo

我这里本周有些关于这个问题进展,同步一下。

我在完成最小可复现demo时,即一个纯spring-cloud项目后发现无法复现上面提到的报错"java.lang.RuntimeException: java.lang.NoSuchMethodException: org.springframework.aop.SpringProxy.prepare(org.apache.seata.rm.tcc.api.BusinessActionContext, int)
"
然后我debug对比这个demo和业务代码,发现导致这个异常的原因是,业务中有一个自定义的aop生效了,这个我也提交到demo里了
image

在这种情况下,debug可以看到:
断点1: org.apache.seata.rm.tcc.interceptor.TccActionInterceptorHandler
这里的targetBean是动态代理增强后的类
image

断点2:org.apache.seata.rm.tcc.interceptor.TccActionInterceptorHandler#parseAnnotation
这里拿到的interfaceClasses就包含有动态代理相关的接口,如果没有这个aop这里实际只有interface org.example.biz.LocalTccService,那么就是符合预期。
image

这里这段判断代码我有个疑问,在for循环里发现后NoSuchMethodException就抛出异常中断,那这里为什么要是一个循环,是存在什么场景能够到下一次循环而不触发这个异常吗?这里是否可以考虑,改成只要找到目标method的就可以,而不是必须都存在。

以上,期待您的建议

@funky-eyes
Copy link
Contributor

这个问题我认为有2个原因
1.parseAnnotation中应该对方法不存在进行跳过,不应该直接抛异常,因为其中可能存在代理类,并且其中还存在第二个问题,当实现类与接口并存tcc注解时,会出现问题,所以这里的逻辑我认为最好改为子类优先,父类注解的值应该被子类替换(这也是官网里为什么建议大家放到实现类中,而不是接口上的原因),以及这个遍历实际上只需要运行一次,只要initCommonFenceCleanTask执行完成后,就不应该再次进行,即businessAction.useTCCFence() 为true时,外层的for也是没意义的,而且会影响启动应用的耗时
2.GlobalTransactionScanner的优先级低于aop,导致aop先行代理后,得到了一个代理bean,而不是真正的targetbean
我认为修复该问题,可以从第一点下手,也是你所提到的问题,你可以提交一个pr来帮助社区修复该问题吗?

@wxrqforever
Copy link
Author

wxrqforever commented Jan 3, 2025

这个问题我认为有2个原因
1.parseAnnotation中应该对方法不存在进行跳过,不应该直接抛异常,因为其中可能存在代理类,并且其中还存在第二个问题,当实现类与接口并存tcc注解时,会出现问题,所以这里的逻辑我认为最好改为子类优先,父类注解的值应该被子类替换(这也是官网里为什么建议大家放到实现类中,而不是接口上的原因),以及这个遍历实际上只需要运行一次,只要initCommonFenceCleanTask执行完成后,就不应该再次进行,即businessAction.useTCCFence() 为true时,外层的for也是没意义的,而且会影响启动应用的耗时
2.GlobalTransactionScanner的优先级低于aop,导致aop先行代理后,得到了一个代理bean,而不是真正的targetbean
我认为修复该问题,可以从第一点下手,也是你所提到的问题,你可以提交一个pr来帮助社区修复该问题吗?

没问题的哥,我理解就是对parseAnnotation进行优化,去掉非必要的循环以及最后再抛出异常。

但是我还有个疑问,针对提出的原因1,官网建议的是将@LocalTcc和@TwoPhaseBusinessAction的两个注解放到实现类中吗?这个我也尝试过,我发现如果将这两个注解都放到实现类中的话,目前代码(seata-2.1)是无法识别@ TwoPhaseBusinessAction这个注解,这会导致目标业务对象不会被TccActionInterceptorHandler代理,最终导致tcc实际不会生效,这在我提交的demo也可以复现,原因也是因为代理类导致的。这样的话即使我对parseAnnotation进行了优化,也无法解决上面的问题。

导致不生效的代码,我定位认为是在这里:
org.apache.seata.rm.tcc.interceptor.parser.TccActionInterceptorParser#tccProxyTargetMethod
image
这里代码里拿到的method实际是aop代理对象的,这时候的method已丢失注解信息,导致了误判。这里我个人认为是还需要获取原对象method信息,才能够同时兼容@LocalTcc和@TwoPhaseBusinessAction放在实现类上的场景。

@funky-eyes
Copy link
Contributor

@localtcc@TwoPhaseBusinessAction 就应该被放在实现类了,社区的例子应该没有将@localtcc这个注解放到过接口上

@wxrqforever
Copy link
Author

@localtcc@TwoPhaseBusinessAction 就应该被放在实现类了,社区的例子应该没有将@localtcc这个注解放到过接口上

明白,但是如我前面所述,在存在aop的场景下,@TwoPhaseBusinessAction在实现类上时这个注解会失效,即使修复了parseAnnotation方法的问题,也仍然存在。定位了一下问题出在org.apache.seata.rm.tcc.interceptor.parser.TccActionInterceptorParser#tccProxyTargetMethod
这个地方,这里应该还需要获取代理的原对象。

@wxrqforever
Copy link
Author

@funky-eyes 哥 这个再帮忙看看

@funky-eyes
Copy link
Contributor

是的应该要获取非代理的原始bean,GlobalTransactionScanner order调低升高其优先级应该也能拿到原始对象,不过先看下直接不调整order如何获取到原始bean吧

@wxrqforever
Copy link
Author

是的应该要获取非代理的原始bean,GlobalTransactionScanner order调低升高其优先级应该也能拿到原始对象,不过先看下直接不调整order如何获取到原始bean吧

好的,我来修复这个问题

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: bug Category issues or prs related to bug.
Projects
None yet
Development

No branches or pull requests

3 participants