From 4f160c77a400527cac0f6cc2f190181095e9d5de Mon Sep 17 00:00:00 2001 From: "huanzu.chen" Date: Sun, 10 Apr 2022 21:25:07 +0800 Subject: [PATCH 1/3] add QSF(queue service framework) --- pom.xml | 5 + .../RocketMQTransactionConfiguration.java | 1 - rocketmq-spring-qsf/README.md | 84 ++++++ rocketmq-spring-qsf/pom.xml | 146 ++++++++++ rocketmq-spring-qsf/qsf_architecture_cn.jpg | Bin 0 -> 88938 bytes rocketmq-spring-qsf/qsf_architecture_en.jpg | Bin 0 -> 119110 bytes .../pom.xml | 77 +++++ .../DubboSyncQSFConsumerCallBackImpl.java | 90 ++++++ .../qsf/callback/SyncQSFConsumerCallBack.java | 43 +++ .../callback/domain/QSFCallBackObject.java | 60 ++++ ...cCallBackConsumerByDubboPostProcessor.java | 96 +++++++ ...cCallBackProviderByDubboPostProcessor.java | 65 +++++ .../rocketmq-spring-qsf-core/pom.xml | 103 +++++++ .../msgconsumer/DefaultQSFMsgConsumer.java | 122 ++++++++ .../msgconsumer/QSFMsgConsumer.java | 87 ++++++ .../msgconsumer/QSFServiceProvider.java | 94 ++++++ .../DefaultQSFRocketmqMsgSender.java | 65 +++++ .../msgproducer/QSFMethodInvokeSpecial.java | 59 ++++ .../msgproducer/QSFMsgProducer.java | 70 +++++ .../annotation/msgproducer/QSFMsgSender.java | 31 ++ .../msgproducer/QSFServiceConsumer.java | 65 +++++ .../qsf/beans/ApplicationContextHelper.java | 70 +++++ .../qsf/beans/ApplicationStartedListener.java | 60 ++++ .../spring/qsf/beans/QSFInfraBeans.java | 62 ++++ .../QSFConsumerBeanFactoryPostProcessor.java | 174 +++++++++++ .../QSFConsumerBeanPostProcessor.java | 269 ++++++++++++++++++ .../consumer/QSFConsumerMockBeanFactory.java | 58 ++++ ...QSFConsumerMockProxyInvocationHandler.java | 33 +++ .../qsf/consumer/QSFConsumerServiceProxy.java | 40 +++ ...ConsumerServiceProxyInvocationHandler.java | 155 ++++++++++ .../spring/qsf/model/MethodInvokeInfo.java | 122 ++++++++ .../QSFConsumerPostProcessor.java | 57 ++++ .../QSFProviderPostProcessor.java | 53 ++++ .../preprocessor/QSFConsumerPreProcessor.java | 56 ++++ .../preprocessor/QSFProviderPreProcessor.java | 53 ++++ .../spring/qsf/provider/QSFMsgListener.java | 21 ++ .../QSFProviderBeanPostProcessor.java | 122 ++++++++ .../qsf/provider/QSFRocketmqMsgListener.java | 73 +++++ .../ClearableAfterApplicationStarted.java | 22 ++ .../rocketmq/spring/qsf/util/IPUtils.java | 86 ++++++ .../rocketmq/spring/qsf/util/InvokeUtils.java | 117 ++++++++ .../rocketmq/spring/qsf/util/KeyUtils.java | 28 ++ .../spring/qsf/util/QSFStringUtils.java | 32 +++ .../qsf/util/ReflectionMethodInvoker.java | 149 ++++++++++ .../spring/qsf/util/ReflectionUtils.java | 73 +++++ .../rocketmq-spring-qsf-demo/pom.xml | 36 +++ .../pom.xml | 92 ++++++ .../demo/QSFCallbackDubboDemoApplication.java | 30 ++ .../QSFCallbackDubboDemoController.java | 67 +++++ .../QSFCallbackDubboDemoService.java | 38 +++ .../QSFCallbackDubboDemoServiceImpl.java | 43 +++ .../src/main/resources/application.properties | 24 ++ .../src/main/resources/application.yml | 40 +++ .../src/main/resources/logback-spring.xml | 37 +++ .../rocketmq-spring-qsf-demo-core/pom.xml | 75 +++++ .../qsf/demo/QSFCoreDemoApplication.java | 30 ++ .../controller/QSFCoreDemoController.java | 52 ++++ .../demo/qsfprovider/QSFCoreDemoService.java | 29 ++ .../qsfprovider/QSFCoreDemoServiceImpl.java | 36 +++ .../src/main/resources/application.properties | 24 ++ .../src/main/resources/application.yml | 20 ++ .../src/main/resources/logback-spring.xml | 37 +++ .../pom.xml | 75 +++++ .../demo/QSFIdemptencyDemoApplication.java | 30 ++ .../QSFIdemptencyDemoController.java | 63 ++++ .../qsfprovider/QSFIdemptencyDemoService.java | 37 +++ .../QSFIdemptencyDemoServiceImpl.java | 43 +++ .../src/main/resources/application.properties | 24 ++ .../src/main/resources/application.yml | 38 +++ .../src/main/resources/logback-spring.xml | 37 +++ .../rocketmq-spring-qsf-idempotency/pom.xml | 76 +++++ .../qsf/idempotency/IdempotencyLockUtils.java | 34 +++ .../qsf/idempotency/IdempotencyParams.java | 50 ++++ .../idempotency/IdempotencyParamsManager.java | 75 +++++ .../qsf/idempotency/QSFIdempotency.java | 44 +++ .../QSFIdempotencyProviderPostProcessor.java | 72 +++++ .../QSFIdempotencyProviderPreProcessor.java | 76 +++++ .../rocketmq-spring-qsf-state-store/pom.xml | 77 +++++ .../spring/qsf/store/QSFJedisClient.java | 35 +++ .../qsf/store/QSFJedisClusterClient.java | 65 +++++ .../spring/qsf/store/QSFJedisPoolClient.java | 79 +++++ .../spring/qsf/store/QSFStateStoreBeans.java | 88 ++++++ .../QSFStateStoreRedisConfigProperties.java | 62 ++++ 83 files changed, 5237 insertions(+), 1 deletion(-) create mode 100644 rocketmq-spring-qsf/README.md create mode 100644 rocketmq-spring-qsf/pom.xml create mode 100644 rocketmq-spring-qsf/qsf_architecture_cn.jpg create mode 100644 rocketmq-spring-qsf/qsf_architecture_en.jpg create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-callback-dubbo/pom.xml create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-callback-dubbo/src/main/java/org/apache/rocketmq/spring/qsf/callback/DubboSyncQSFConsumerCallBackImpl.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-callback-dubbo/src/main/java/org/apache/rocketmq/spring/qsf/callback/SyncQSFConsumerCallBack.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-callback-dubbo/src/main/java/org/apache/rocketmq/spring/qsf/callback/domain/QSFCallBackObject.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-callback-dubbo/src/main/java/org/apache/rocketmq/spring/qsf/callback/postprocessor/QSFSyncCallBackConsumerByDubboPostProcessor.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-callback-dubbo/src/main/java/org/apache/rocketmq/spring/qsf/callback/postprocessor/QSFSyncCallBackProviderByDubboPostProcessor.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-core/pom.xml create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/annotation/msgconsumer/DefaultQSFMsgConsumer.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/annotation/msgconsumer/QSFMsgConsumer.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/annotation/msgconsumer/QSFServiceProvider.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/annotation/msgproducer/DefaultQSFRocketmqMsgSender.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/annotation/msgproducer/QSFMethodInvokeSpecial.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/annotation/msgproducer/QSFMsgProducer.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/annotation/msgproducer/QSFMsgSender.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/annotation/msgproducer/QSFServiceConsumer.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/beans/ApplicationContextHelper.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/beans/ApplicationStartedListener.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/beans/QSFInfraBeans.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/consumer/QSFConsumerBeanFactoryPostProcessor.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/consumer/QSFConsumerBeanPostProcessor.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/consumer/QSFConsumerMockBeanFactory.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/consumer/QSFConsumerMockProxyInvocationHandler.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/consumer/QSFConsumerServiceProxy.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/consumer/QSFConsumerServiceProxyInvocationHandler.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/model/MethodInvokeInfo.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/postprocessor/QSFConsumerPostProcessor.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/postprocessor/QSFProviderPostProcessor.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/preprocessor/QSFConsumerPreProcessor.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/preprocessor/QSFProviderPreProcessor.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/provider/QSFMsgListener.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/provider/QSFProviderBeanPostProcessor.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/provider/QSFRocketmqMsgListener.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/util/ClearableAfterApplicationStarted.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/util/IPUtils.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/util/InvokeUtils.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/util/KeyUtils.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/util/QSFStringUtils.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/util/ReflectionMethodInvoker.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/util/ReflectionUtils.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-demo/pom.xml create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-callback-dubbo/pom.xml create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-callback-dubbo/src/main/java/org/apache/rocketmq/spring/qsf/demo/QSFCallbackDubboDemoApplication.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-callback-dubbo/src/main/java/org/apache/rocketmq/spring/qsf/demo/controller/QSFCallbackDubboDemoController.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-callback-dubbo/src/main/java/org/apache/rocketmq/spring/qsf/demo/qsfprovider/QSFCallbackDubboDemoService.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-callback-dubbo/src/main/java/org/apache/rocketmq/spring/qsf/demo/qsfprovider/QSFCallbackDubboDemoServiceImpl.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-callback-dubbo/src/main/resources/application.properties create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-callback-dubbo/src/main/resources/application.yml create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-callback-dubbo/src/main/resources/logback-spring.xml create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-core/pom.xml create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-core/src/main/java/org/apache/rocketmq/spring/qsf/demo/QSFCoreDemoApplication.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-core/src/main/java/org/apache/rocketmq/spring/qsf/demo/controller/QSFCoreDemoController.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-core/src/main/java/org/apache/rocketmq/spring/qsf/demo/qsfprovider/QSFCoreDemoService.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-core/src/main/java/org/apache/rocketmq/spring/qsf/demo/qsfprovider/QSFCoreDemoServiceImpl.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-core/src/main/resources/application.properties create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-core/src/main/resources/application.yml create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-core/src/main/resources/logback-spring.xml create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-idempotency/pom.xml create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-idempotency/src/main/java/org/apache/rocketmq/spring/qsf/demo/QSFIdemptencyDemoApplication.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-idempotency/src/main/java/org/apache/rocketmq/spring/qsf/demo/controller/QSFIdemptencyDemoController.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-idempotency/src/main/java/org/apache/rocketmq/spring/qsf/demo/qsfprovider/QSFIdemptencyDemoService.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-idempotency/src/main/java/org/apache/rocketmq/spring/qsf/demo/qsfprovider/QSFIdemptencyDemoServiceImpl.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-idempotency/src/main/resources/application.properties create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-idempotency/src/main/resources/application.yml create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-idempotency/src/main/resources/logback-spring.xml create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-idempotency/pom.xml create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-idempotency/src/main/java/org/apache/rocketmq/spring/qsf/idempotency/IdempotencyLockUtils.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-idempotency/src/main/java/org/apache/rocketmq/spring/qsf/idempotency/IdempotencyParams.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-idempotency/src/main/java/org/apache/rocketmq/spring/qsf/idempotency/IdempotencyParamsManager.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-idempotency/src/main/java/org/apache/rocketmq/spring/qsf/idempotency/QSFIdempotency.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-idempotency/src/main/java/org/apache/rocketmq/spring/qsf/idempotency/QSFIdempotencyProviderPostProcessor.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-idempotency/src/main/java/org/apache/rocketmq/spring/qsf/idempotency/QSFIdempotencyProviderPreProcessor.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-state-store/pom.xml create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-state-store/src/main/java/org/apache/rocketmq/spring/qsf/store/QSFJedisClient.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-state-store/src/main/java/org/apache/rocketmq/spring/qsf/store/QSFJedisClusterClient.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-state-store/src/main/java/org/apache/rocketmq/spring/qsf/store/QSFJedisPoolClient.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-state-store/src/main/java/org/apache/rocketmq/spring/qsf/store/QSFStateStoreBeans.java create mode 100644 rocketmq-spring-qsf/rocketmq-spring-qsf-state-store/src/main/java/org/apache/rocketmq/spring/qsf/store/QSFStateStoreRedisConfigProperties.java diff --git a/pom.xml b/pom.xml index 18b8548b..327cd194 100644 --- a/pom.xml +++ b/pom.xml @@ -218,5 +218,10 @@ + + + rocketmq-spring-qsf diff --git a/rocketmq-spring-boot/src/main/java/org/apache/rocketmq/spring/autoconfigure/RocketMQTransactionConfiguration.java b/rocketmq-spring-boot/src/main/java/org/apache/rocketmq/spring/autoconfigure/RocketMQTransactionConfiguration.java index e4712754..99c08f20 100644 --- a/rocketmq-spring-boot/src/main/java/org/apache/rocketmq/spring/autoconfigure/RocketMQTransactionConfiguration.java +++ b/rocketmq-spring-boot/src/main/java/org/apache/rocketmq/spring/autoconfigure/RocketMQTransactionConfiguration.java @@ -20,7 +20,6 @@ import java.util.Map; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import org.apache.rocketmq.client.producer.TransactionMQProducer; import org.apache.rocketmq.spring.annotation.RocketMQTransactionListener; diff --git a/rocketmq-spring-qsf/README.md b/rocketmq-spring-qsf/README.md new file mode 100644 index 00000000..aa79e783 --- /dev/null +++ b/rocketmq-spring-qsf/README.md @@ -0,0 +1,84 @@ +## QSF:queue service framework + +*** + +### QSF introduction ++ With QSF we can produce/consume rocket-mq messages non-intrusively, and base QSF we can implement standard MQ eventual consistency, idempotency, flow control and so on. + +*** + +### QSF usage & best practice +1. Import the qsf package. If you only need the basic capabilities of qsf, you only need to import rocketmq-spring-qsf-core. +2. The message sender defines the message protocol in the form of a service interface, and publishes a maven package. The service defining package here is preferably independent of the RPC service defining package, and the qsf-client keyword added to the package name is preferred to reduce the cost of communication and collaboration. +3. When a message needs to be sent, the message sender introduces the service interface defined in step 2 with @QSFMsgProducer(or @QSFServiceConsumer) and calls the service. +4. The message receiver introduces the QSF service defining package in step 2, and implement the service, then annotate the service implementation with @QSFMsgConsumer(or @QSFServiceProvider). +5. QSF has implemented callback extension qsf-callback, idempotent extension qsf-idempotency, please refer to the rocketmq-spring-qsf-demo module of the project for usage. + +*** + +### QSF demo ++ Demos module : rocketmq-spring-qsf/rocketmq-spring-qsf-demo ++ Before running the demos, an available rocketmq server suite required : + + Start rocketmq nameserver on the localhost with default port 127.0.0.1:9876, and start rocketmq broker on the localhost registered in the nameserver above + + or find an available rocketmq server suite and modify the demos' configuration file rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-*/src/main/resources/application.yml + + rocketmq-spring-qsf-core usage demo: + 1. Run rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-core/QSFCoreDemoApplication#main + 2. Visit http://localhost:7001/demo/qsf/basic + 3. Looking at the spring-boot console log of the demo, we can see that the http-io thread sent a service invoking message to rocketmq : + ``` +[http-nio-7001-exec-1] INFO o.a.r.s.q.a.c.DefaultQSFRocketmqMsgSender - sendMessage methodInvokeInfo:MethodInvokeInfo(invokeBeanType=org.apache.rocketmq.spring.qsf.demo.qsfprovider.QSFCoreDemoService, methodName=testQSFBasic, argsTypes=[long, class java.lang.String], args=[100, hello world], sourceIp=fe80:0:0:0:94d1:970f:f05d:8e49%eth1, sourceCallKey=fe80:0:0:0:94d1:970f:f05d:8e49%eth1:1649255150154:46, syncCall=null) result:SendResult [sendStatus=SEND_OK, msgId=7F000001C89818B4AAC21E8FF6500000, offsetMsgId=1E08501600002A9F000000000000D0B4, messageQueue=MessageQueue [topic=rocketmq_topic_qsf_demo, brokerName=broker-a, queueId=2], queueOffset=1] +``` + +and the rocketmq consumer thread consumes the message then executes the implementation of the service : +``` +[ConsumeMessageThread_1] INFO o.a.r.s.q.a.p.DefaultQSFMsgConsumer - consume message id:7F000001C89818B4AAC21E8FF6500000 key:org.apache.rocketmq.spring.qsf.demo.qsfprovider.QSFCoreDemoService.testQSFBasic:long#java.lang.String:100#hello world body:{"args":[100,"hello world"],"argsTypes":["long","java.lang.String"],"invokeBeanType":"org.apache.rocketmq.spring.qsf.demo.qsfprovider.QSFCoreDemoService","methodName":"testQSFBasic","sourceCallKey":"fe80:0:0:0:94d1:970f:f05d:8e49%eth1:1649255150154:46","sourceIp":"fe80:0:0:0:94d1:970f:f05d:8e49%eth1"} +[ConsumeMessageThread_1] INFO o.a.r.s.q.d.q.QSFCoreDemoServiceImpl - in service call: testQSFBasic id:100 name:hello world +``` + ++ rocketmq-spring-qsf-idempotency usage demo : +1. Start a redis server on the localhost with default port 127.0.0.1:2181, or find an available redis server and modify the configuration file rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-idempotency/src/main/resources/application.yml +2. Run rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-idempotency/QSFIdemptencyDemoApplication#main +3. Visit http://localhost:7002/demo/qsf/idem +4. Looking at the spring-boot console log of the demo, we can see that the http-io thread sent service invoking messages to rocketmq twice : +``` +[http-nio-7002-exec-1] INFO o.a.r.s.q.a.c.DefaultQSFRocketmqMsgSender - sendMessage methodInvokeInfo:MethodInvokeInfo(invokeBeanType=org.apache.rocketmq.spring.qsf.demo.qsfprovider.QSFIdemptencyDemoService, methodName=testQSFIdempotency, argsTypes=[long, class java.lang.String], args=[100, hello world], sourceIp=fe80:0:0:0:94d1:970f:f05d:8e49%eth1, sourceCallKey=fe80:0:0:0:94d1:970f:f05d:8e49%eth1:1649255892282:58, syncCall=null) result:SendResult [sendStatus=SEND_OK, msgId=7F000001C93C18B4AAC21E9B487A0000, offsetMsgId=1E08501600002A9F000000000000D30F, messageQueue=MessageQueue [topic=rocketmq_topic_qsf_demo, brokerName=broker-a, queueId=10], queueOffset=6] +[http-nio-7002-exec-1] INFO o.a.r.s.q.a.c.DefaultQSFRocketmqMsgSender - sendMessage methodInvokeInfo:MethodInvokeInfo(invokeBeanType=org.apache.rocketmq.spring.qsf.demo.qsfprovider.QSFIdemptencyDemoService, methodName=testQSFIdempotency, argsTypes=[long, class java.lang.String], args=[100, hello world], sourceIp=fe80:0:0:0:94d1:970f:f05d:8e49%eth1, sourceCallKey=fe80:0:0:0:94d1:970f:f05d:8e49%eth1:1649255892282:58, syncCall=null) result:SendResult [sendStatus=SEND_OK, msgId=7F000001C93C18B4AAC21E9B487A0000, offsetMsgId=1E08501600002A9F000000000000D30F, messageQueue=MessageQueue [topic=rocketmq_topic_qsf_demo, brokerName=broker-a, queueId=10], queueOffset=6] +``` + + and the rocketmq consumer thread 1 consumes the message then executes the implementation of the service normally : +``` +[ConsumeMessageThread_1] INFO o.a.r.s.q.a.p.DefaultQSFMsgConsumer - consume message id:7F000001C93C18B4AAC21E9B487A0000 key:org.apache.rocketmq.spring.qsf.demo.qsfprovider.QSFIdemptencyDemoService.testQSFIdempotency:long#java.lang.String:100#hello world body:{"args":[100,"hello world"],"argsTypes":["long","java.lang.String"],"invokeBeanType":"org.apache.rocketmq.spring.qsf.demo.qsfprovider.QSFIdemptencyDemoService","methodName":"testQSFIdempotency","sourceCallKey":"fe80:0:0:0:94d1:970f:f05d:8e49%eth1:1649255892282:58","sourceIp":"fe80:0:0:0:94d1:970f:f05d:8e49%eth1"} +[ConsumeMessageThread_1] INFO o.a.r.s.q.i.IdempotencyParamsManager - getAnnotation QSFIdempotency for method:org.apache.rocketmq.spring.qsf.demo.qsfprovider.QSFIdemptencyDemoService.testQSFIdempotency:long#java.lang.String result:@org.apache.rocketmq.spring.qsf.idempotency.QSFIdempotency(idempotentMethodExecuteTimeout=1000, idempotencyMillisecondsToExpire=3600000) +[ConsumeMessageThread_1] INFO o.a.r.s.q.d.q.QSFIdemptencyDemoServiceImpl - in service call: testQSFIdempotency id:100 name:hello world +``` + + and the rocketmq consumer thread 2 consumes the message but reject to execute the implementation of the service : +``` +[ConsumeMessageThread_2] INFO o.a.r.s.q.a.p.DefaultQSFMsgConsumer - consume message id:7F000001C93C18B4AAC21E9B48820001 key:org.apache.rocketmq.spring.qsf.demo.qsfprovider.QSFIdemptencyDemoService.testQSFIdempotency:long#java.lang.String:100#hello world body:{"args":[100,"hello world"],"argsTypes":["long","java.lang.String"],"invokeBeanType":"org.apache.rocketmq.spring.qsf.demo.qsfprovider.QSFIdemptencyDemoService","methodName":"testQSFIdempotency","sourceCallKey":"fe80:0:0:0:94d1:970f:f05d:8e49%eth1:1649255893122:58","sourceIp":"fe80:0:0:0:94d1:970f:f05d:8e49%eth1"} +[ConsumeMessageThread_2] INFO o.a.r.s.q.i.QSFIdempotencyProviderPreProcessor - method has been called elsewhere, ignored here, idempotencyKey:org.apache.rocketmq.spring.qsf.demo.qsfprovider.QSFIdemptencyDemoService.testQSFIdempotency:long#java.lang.String:100#hello world +[ConsumeMessageThread_2] INFO o.a.r.s.q.a.p.DefaultQSFMsgConsumer - invoke break because org.apache.rocketmq.spring.qsf.idempotency.QSFIdempotencyProviderPreProcessor@2c2a4417 returns false for invokeInfoJson:{"args":[100,"hello world"],"argsTypes":["long","java.lang.String"],"invokeBeanType":"org.apache.rocketmq.spring.qsf.demo.qsfprovider.QSFIdemptencyDemoService","methodName":"testQSFIdempotency","sourceCallKey":"fe80:0:0:0:94d1:970f:f05d:8e49%eth1:1649255893122:58","sourceIp":"fe80:0:0:0:94d1:970f:f05d:8e49%eth1"} +``` + ++ rocketmq-spring-qsf-callback-dubbo usage demo : +1. Start a zookeeper server on the localhost with default port 127.0.0.1:2181, or find an available zookeeper server and modify the configuration file rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-callback-dubbo/src/main/resources/application.yml +2. Run rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-callback-dubbo/QSFIdemptencyDemoApplication#main +3. Visit http://localhost:7003/demo/qsf/callback +4. Looking at the spring-boot console log of the demo, we can see that the http-io thread sent a service invoke message to rocketmq : +``` +[http-nio-7003-exec-1] INFO o.a.r.s.q.a.c.DefaultQSFRocketmqMsgSender - sendMessage methodInvokeInfo:MethodInvokeInfo(invokeBeanType=org.apache.rocketmq.spring.qsf.demo.qsfprovider.QSFCallbackDubboDemoService, methodName=testQSFCallback, argsTypes=[long, class java.lang.String], args=[100, hello world], sourceIp=fe80:0:0:0:94d1:970f:f05d:8e49%eth1, sourceCallKey=fe80:0:0:0:94d1:970f:f05d:8e49%eth1:1649257585092:81, syncCall=true) result:SendResult [sendStatus=SEND_OK, msgId=7F000001D3BC18B4AAC21EB51DA10000, offsetMsgId=1E08501600002A9F000000000000D7F5, messageQueue=MessageQueue [topic=rocketmq_topic_qsf_demo, brokerName=broker-a, queueId=15], queueOffset=3] +``` +and the rocketmq consumer thread consumes the message then executes the implementation of the service : +``` +[ConsumeMessageThread_1] INFO o.a.r.s.q.a.p.DefaultQSFMsgConsumer - consume message id:7F000001D3BC18B4AAC21EB51DA10000 key:org.apache.rocketmq.spring.qsf.demo.qsfprovider.QSFCallbackDubboDemoService.testQSFCallback:long#java.lang.String:100#hello world body:{"args":[100,"hello world"],"argsTypes":["long","java.lang.String"],"invokeBeanType":"org.apache.rocketmq.spring.qsf.demo.qsfprovider.QSFCallbackDubboDemoService","methodName":"testQSFCallback","sourceCallKey":"fe80:0:0:0:94d1:970f:f05d:8e49%eth1:1649257585092:81","sourceIp":"fe80:0:0:0:94d1:970f:f05d:8e49%eth1","syncCall":true} +[ConsumeMessageThread_1] INFO o.a.r.s.q.d.q.QSFCallbackDubboDemoServiceImpl - in service call: testQSFCallback id:100 name:hello world +``` + +and the dubbo implement of qsf consumer callback is invoked in the dubbo service thread to receive the return value and awake the http-io thread : +``` +[DubboServerHandler-30.8.80.22:20880-thread-2] INFO o.a.r.s.q.c.DubboSyncQSFConsumerCallBackImpl - syncValueCallBack called, sourceAoneApp:qsfdemo, callKey:fe80:0:0:0:94d1:970f:f05d:8e49%eth1:1649257585092:81, returnValue:syncEcho:hello world, callBackObject:QSFCallBackObject(callBackCountDownLatch=java.util.concurrent.CountDownLatch@43f22e8f[Count = 1], returnValue=null, validCallbackSourceApps=null, callBackReturnValueAppName=) +23:06:26.259 [DubboServerHandler-30.8.80.22:20880-thread-2] INFO o.a.r.s.q.c.DubboSyncQSFConsumerCallBackImpl - return value:syncEcho:hello world to thread:fe80:0:0:0:94d1:970f:f05d:8e49%eth1:1649257585092:81 done +23:06:26.259 [http-nio-7003-exec-1] INFO o.a.r.s.q.c.p.QSFSyncCallBackConsumerByDubboPostProcessor - caller thread notified when all callback called +23:06:26.259 [http-nio-7003-exec-1] INFO o.a.r.s.q.d.c.QSFCallbackDubboDemoController - syncEcho result:syncEcho:hello world +``` + +*** diff --git a/rocketmq-spring-qsf/pom.xml b/rocketmq-spring-qsf/pom.xml new file mode 100644 index 00000000..e62bd2f5 --- /dev/null +++ b/rocketmq-spring-qsf/pom.xml @@ -0,0 +1,146 @@ + + + + + + org.apache.rocketmq + rocketmq-spring-boot-parent + 2.2.2-SNAPSHOT + ../rocketmq-spring-boot-parent/pom.xml + + 4.0.0 + + rocketmq-spring-qsf + pom + 1.0.0-SNAPSHOT + + Apache RocketMQ Spring Boot QSF(queue service framework) ${project.version} + + + 8 + 8 + 8 + UTF-8 + UTF-8 + + + + rocketmq-spring-qsf-core + rocketmq-spring-qsf-callback-dubbo + rocketmq-spring-qsf-state-store + rocketmq-spring-qsf-idempotency + + rocketmq-spring-qsf-demo + + + + + + org.apache.rocketmq + rocketmq-spring-qsf-core + ${project.version} + + + org.apache.rocketmq + rocketmq-spring-qsf-callback-dubbo + ${project.version} + + + org.apache.rocketmq + rocketmq-spring-qsf-state-store + ${project.version} + + + org.apache.rocketmq + rocketmq-spring-qsf-idempotency + ${project.version} + + + + org.springframework.boot + spring-boot-dependencies + ${spring.boot.version} + pom + import + + + org.apache.rocketmq + rocketmq-client + 4.9.1 + + + org.apache.dubbo + dubbo-spring-boot-starter + 2.7.15 + + + org.springframework.boot + spring-boot-starter-web + ${spring.boot.version} + + + + org.apache.curator + curator-framework + 5.1.0 + + + org.apache.curator + curator-recipes + 5.1.0 + + + redis.clients + jedis + 3.8.0 + + + + org.slf4j + jcl-over-slf4j + 1.7.26 + + + + com.alibaba + fastjson + 1.2.68.noneautotype + + + + + junit + junit + 4.12 + test + + + + + org.projectlombok + lombok + 1.18.8 + provided + + + + + + \ No newline at end of file diff --git a/rocketmq-spring-qsf/qsf_architecture_cn.jpg b/rocketmq-spring-qsf/qsf_architecture_cn.jpg new file mode 100644 index 0000000000000000000000000000000000000000..02a419e6b3b6565651c2c927e27f1d76c2172153 GIT binary patch literal 88938 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 M6Y>91an}a_S3bS8+yDRo literal 0 HcmV?d00001 diff --git a/rocketmq-spring-qsf/qsf_architecture_en.jpg b/rocketmq-spring-qsf/qsf_architecture_en.jpg new file mode 100644 index 0000000000000000000000000000000000000000..a098a9e821892c61ff6dd9a6a22acbf673277ab2 GIT binary patch literal 119110 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

PG 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 literal 0 HcmV?d00001 diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-callback-dubbo/pom.xml b/rocketmq-spring-qsf/rocketmq-spring-qsf-callback-dubbo/pom.xml new file mode 100644 index 00000000..55d22801 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-callback-dubbo/pom.xml @@ -0,0 +1,77 @@ + + + + + + rocketmq-spring-qsf + org.apache.rocketmq + 1.0.0-SNAPSHOT + + 4.0.0 + + org.apache.rocketmq + rocketmq-spring-qsf-callback-dubbo + + + + org.apache.rocketmq + rocketmq-spring-qsf-core + + + + org.apache.dubbo + dubbo-spring-boot-starter + + + + org.projectlombok + lombok + provided + + + + + + + org.apache.maven.plugins + maven-resources-plugin + 3.1.0 + + UTF-8 + + + + org.apache.maven.plugins + maven-source-plugin + 3.1.0 + + + attach-sources + verify + + jar-no-fork + + + + + + + + \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-callback-dubbo/src/main/java/org/apache/rocketmq/spring/qsf/callback/DubboSyncQSFConsumerCallBackImpl.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-callback-dubbo/src/main/java/org/apache/rocketmq/spring/qsf/callback/DubboSyncQSFConsumerCallBackImpl.java new file mode 100644 index 00000000..6a4764a4 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-callback-dubbo/src/main/java/org/apache/rocketmq/spring/qsf/callback/DubboSyncQSFConsumerCallBackImpl.java @@ -0,0 +1,90 @@ +/* + * 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.rocketmq.spring.qsf.callback; + +import org.apache.rocketmq.spring.qsf.callback.domain.QSFCallBackObject; +import org.apache.rocketmq.spring.qsf.util.QSFStringUtils; +import lombok.extern.slf4j.Slf4j; +import org.apache.dubbo.config.annotation.DubboService; +import org.springframework.util.CollectionUtils; + +import java.util.Set; + +/** + * @desc After the mq lister processes the message, it calls back QSFProviderPostProcessor to notify the message processing completion and pass the return value; dubbo provider ip is specified in the actual call to ensure that the correct server is called back. + **/ +@DubboService(interfaceClass = SyncQSFConsumerCallBack.class) +@Slf4j +public class DubboSyncQSFConsumerCallBackImpl implements SyncQSFConsumerCallBack { + + @Override + public void syncValueCallBack(String sourceAoneApp, String callKey, Object returnValue) { + QSFCallBackObject callBackObject = CALLBACK_MANAGE_MAP.get(callKey); + log.info(" syncValueCallBack called, sourceAoneApp:{}, callKey:{}, returnValue:{}, callBackObject:{}", + sourceAoneApp, callKey, returnValue, callBackObject); + + if (callBackObject == null) { + return; + } + + setReturnValue(callBackObject, sourceAoneApp, returnValue); + + Set validCallbackSourceApps = callBackObject.getValidCallbackSourceApps(); + if (validCallbackSourceApps == null || validCallbackSourceApps.contains(sourceAoneApp)) { + callBackObject.getCallBackCountDownLatch().countDown(); + } + + log.info(" return value:{} to thread:{} done", returnValue, callKey); + } + + /** + * When there are multiple mq consumers, the selection strategy of the return value: + * 1. When QSF methodSpecial does not specify CallBackReturnValueAppName and ValidCallbackSourceApps, take the return of the last mq consumer + * 2. When CallBackReturnValueAppName is specified, take the return value of sourceAoneApp equals CallBackReturnValueAppName + * 3. When ValidCallbackSourceApps is specified, and ValidCallbackSourceApps.contains(sourceAoneApp), take the return of the last mq consumer in ValidCallbackSourceApps + * + * @param callBackObject + * @param sourceAoneApp + * @param returnValue + * @return + */ + private void setReturnValue(QSFCallBackObject callBackObject, String sourceAoneApp, Object returnValue) { + if (returnValue == null) { + return; + } + + if (QSFStringUtils.isTrimEmpty(callBackObject.getCallBackReturnValueAppName())) { + /** + * MethodSpecial does not specify CallBackReturnValueAppName , ValidCallbackSourceApps, + * or ValidCallbackSourceApps is specified and ValidCallbackSourceApps.contains(sourceAoneApp), + * take the return of the last mq consumer as the return value. + */ + if (CollectionUtils.isEmpty(callBackObject.getValidCallbackSourceApps()) + || callBackObject.getValidCallbackSourceApps().contains(sourceAoneApp)) { + callBackObject.setReturnValue(returnValue); + } + return; + } + + if (callBackObject.getCallBackReturnValueAppName().equals(sourceAoneApp)) { + //When CallBackReturnValueAppName is specified, take the return value of sourceAoneApp equals CallBackReturnValueAppName + callBackObject.setReturnValue(returnValue); + return; + } + } +} \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-callback-dubbo/src/main/java/org/apache/rocketmq/spring/qsf/callback/SyncQSFConsumerCallBack.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-callback-dubbo/src/main/java/org/apache/rocketmq/spring/qsf/callback/SyncQSFConsumerCallBack.java new file mode 100644 index 00000000..edb3ac33 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-callback-dubbo/src/main/java/org/apache/rocketmq/spring/qsf/callback/SyncQSFConsumerCallBack.java @@ -0,0 +1,43 @@ +/* + * 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.rocketmq.spring.qsf.callback; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.apache.rocketmq.spring.qsf.callback.domain.QSFCallBackObject; + +/** + * @desc After the mq lister processes the message, it calls back SyncQSFConsumerCallBack to notify the completion of the message processing and pass the return value + */ +public interface SyncQSFConsumerCallBack { + /** + * map, key={traceId}:{threadId} + * After the current thread sends a message, Condition.await waits, + * until the mq listener completes the message processing, call the SyncQSFConsumerCallBack provider and pass the return value back + */ + ConcurrentMap CALLBACK_MANAGE_MAP = new ConcurrentHashMap<>(96); + + /** + * Call the callback after mq processing, and pass the return value + * @param sourceAoneApp call source app + * @param callKey = {traceId}:{threadId} + * @param returnValue + */ + void syncValueCallBack(String sourceAoneApp, String callKey, Object returnValue); +} \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-callback-dubbo/src/main/java/org/apache/rocketmq/spring/qsf/callback/domain/QSFCallBackObject.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-callback-dubbo/src/main/java/org/apache/rocketmq/spring/qsf/callback/domain/QSFCallBackObject.java new file mode 100644 index 00000000..6616c8c7 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-callback-dubbo/src/main/java/org/apache/rocketmq/spring/qsf/callback/domain/QSFCallBackObject.java @@ -0,0 +1,60 @@ +/* + * 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.rocketmq.spring.qsf.callback.domain; + +import java.util.Set; +import java.util.concurrent.CountDownLatch; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * @desc QSF callback info + **/ + +@Data +@ToString +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class QSFCallBackObject { + /** + * After the thread that initiated the QSF call sends the message, CountDownLatch.await waits, + * until mq listener completes message processing, call SyncQSFConsumerCallBack.syncValueCallBack and pass the return value back + */ + private CountDownLatch callBackCountDownLatch; + + /** + * used to pass return value + */ + private Object returnValue; + + /** + * When mq listener calls SyncQSFConsumerCallBack.syncValueCallBack, + * only the mq listener which is in validCallbackSourceApps set is allowed to participate in waking up the thread that initiated the QSF call + */ + private Set validCallbackSourceApps; + + /** + * Specify the callback return value of the mq listener, if not specified, the return value will be the last callback + */ + private String callBackReturnValueAppName; +} diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-callback-dubbo/src/main/java/org/apache/rocketmq/spring/qsf/callback/postprocessor/QSFSyncCallBackConsumerByDubboPostProcessor.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-callback-dubbo/src/main/java/org/apache/rocketmq/spring/qsf/callback/postprocessor/QSFSyncCallBackConsumerByDubboPostProcessor.java new file mode 100644 index 00000000..0eba7c66 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-callback-dubbo/src/main/java/org/apache/rocketmq/spring/qsf/callback/postprocessor/QSFSyncCallBackConsumerByDubboPostProcessor.java @@ -0,0 +1,96 @@ +/* + * 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.rocketmq.spring.qsf.callback.postprocessor; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.apache.rocketmq.spring.qsf.annotation.msgproducer.QSFServiceConsumer; +import org.apache.rocketmq.spring.qsf.annotation.msgproducer.QSFMethodInvokeSpecial; +import org.apache.rocketmq.spring.qsf.callback.SyncQSFConsumerCallBack; +import org.apache.rocketmq.spring.qsf.callback.domain.QSFCallBackObject; +import org.apache.rocketmq.spring.qsf.model.MethodInvokeInfo; + +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.spring.qsf.postprocessor.QSFConsumerPostProcessor; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.stereotype.Component; + +/** + * @desc After mq producer sends a message, await to suspend the thread, until the mq listener wake it up + **/ +@Component +@Slf4j +public class QSFSyncCallBackConsumerByDubboPostProcessor extends QSFConsumerPostProcessor { + + @Override + public Object callAfterMessageSend(MethodInvokeInfo methodInvokeInfo, QSFServiceConsumer msgProducerConfig, + QSFMethodInvokeSpecial methodSpecial) { + if (methodSpecial == null || !Boolean.TRUE.equals(methodSpecial.syncCall())) { + // Asynchronous call, return directly + return null; + } + + if (methodSpecial.syncCallBackTimeoutMs() <= 0) { + throw new BeanCreationException("QSFConsumer syncCallBackTimeoutMs should>0 when syncCall=true for method:" + methodInvokeInfo.getMethodName()); + } + + if (methodSpecial.waitCallBackAppNames() != null && methodSpecial.waitCallBackAppNames().length > 0 + && methodSpecial.waitCallBackAppNames().length < methodSpecial.minCallBackTimes()) { + throw new BeanCreationException("QSFConsumer waitCallBackAppNames size should greater than minCallBackTimes when syncCall=true for method:" + methodInvokeInfo.getMethodName()); + } + + String callKey = methodInvokeInfo.getSourceCallKey(); + final CountDownLatch countDownLatch = new CountDownLatch(methodSpecial.minCallBackTimes()); + Set validCallbackSourceApps = methodSpecial.waitCallBackAppNames() == null || methodSpecial.waitCallBackAppNames().length == 0 ? + null : new HashSet<>(Arrays.asList(methodSpecial.waitCallBackAppNames())); + + QSFCallBackObject callBackManageBO = QSFCallBackObject.builder() + .callBackCountDownLatch(countDownLatch) + .callBackReturnValueAppName(methodSpecial.returnValueAppName() == null ? null : methodSpecial.returnValueAppName().trim()) + .validCallbackSourceApps(validCallbackSourceApps) + .build(); + + try { + SyncQSFConsumerCallBack.CALLBACK_MANAGE_MAP.put(callKey, callBackManageBO); + + /** + * Only need to wait for the callback if you need a return value/requires mq listener processing status (such as timeout). + * Synchronous call, wait for the listener to complete the processing and callback. + */ + countDownLatch.await(methodSpecial.syncCallBackTimeoutMs(), TimeUnit.MILLISECONDS); + + if (countDownLatch.getCount() > 0) { + log.info(" caller thread notified when timeout, methodSpecial:{}, current countDownLatch count:{}", + methodSpecial, countDownLatch.getCount()); + } else { + log.info(" caller thread notified when all callback called"); + } + + } catch (Throwable ex) { + throw new RuntimeException(ex); + } finally { + SyncQSFConsumerCallBack.CALLBACK_MANAGE_MAP.remove(callKey); + } + + return callBackManageBO.getReturnValue(); + } +} \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-callback-dubbo/src/main/java/org/apache/rocketmq/spring/qsf/callback/postprocessor/QSFSyncCallBackProviderByDubboPostProcessor.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-callback-dubbo/src/main/java/org/apache/rocketmq/spring/qsf/callback/postprocessor/QSFSyncCallBackProviderByDubboPostProcessor.java new file mode 100644 index 00000000..68117598 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-callback-dubbo/src/main/java/org/apache/rocketmq/spring/qsf/callback/postprocessor/QSFSyncCallBackProviderByDubboPostProcessor.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.rocketmq.spring.qsf.callback.postprocessor; + +import org.apache.rocketmq.spring.qsf.callback.SyncQSFConsumerCallBack; +import org.apache.rocketmq.spring.qsf.model.MethodInvokeInfo; +import org.apache.rocketmq.spring.qsf.postprocessor.QSFProviderPostProcessor; +import org.apache.rocketmq.spring.qsf.util.QSFStringUtils; + +import lombok.extern.slf4j.Slf4j; +import org.apache.dubbo.config.annotation.DubboReference; +import org.apache.dubbo.config.annotation.Method; +import org.apache.dubbo.rpc.RpcContext; +import org.apache.dubbo.rpc.cluster.router.address.Address; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +/** + * @desc After the mq lister processes the message, it will call back SyncQSFConsumerCallBack.syncValueCallBack to return the return value and wake up the mq producer thread + **/ +@Component +@Slf4j +public class QSFSyncCallBackProviderByDubboPostProcessor extends QSFProviderPostProcessor implements InitializingBean { + + @DubboReference(injvm = false, check = false, methods = {@Method(name = "syncValueCallBack", retries = 5)}) + private SyncQSFConsumerCallBack syncQSFConsumerCallBack; + + @Value("${qsf.project.name:}") + private String projectName; + + @Override + public void callAfterMessageProcess(MethodInvokeInfo methodInvokeInfo, Object returnValue) { + if (Boolean.TRUE.equals(methodInvokeInfo.getSyncCall()) + && QSFStringUtils.isNotTrimEmpty(methodInvokeInfo.getSourceCallKey()) + && QSFStringUtils.isNotTrimEmpty(methodInvokeInfo.getSourceIp())) { + /** + * specific dubbo provider server ip + */ + Address providerAddr = new Address(methodInvokeInfo.getSourceIp(), 20880); + RpcContext.getContext().setObjectAttachment("address", providerAddr); + + /** + * On the mq listener instance, call syncValueCallBack to return the return value and wake up the message thread + */ + syncQSFConsumerCallBack.syncValueCallBack(projectName, methodInvokeInfo.getSourceCallKey(), returnValue); + } + } + +} \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-core/pom.xml b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/pom.xml new file mode 100644 index 00000000..60add8ca --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/pom.xml @@ -0,0 +1,103 @@ + + + + + + rocketmq-spring-qsf + org.apache.rocketmq + 1.0.0-SNAPSHOT + + 4.0.0 + + org.apache.rocketmq + rocketmq-spring-qsf-core + + + + + org.apache.rocketmq + rocketmq-client + + + org.slf4j + slf4j-api + + + + + + org.slf4j + slf4j-api + + + + org.springframework + spring-context + + + org.springframework + spring-beans + + + org.springframework + spring-core + + + + + + com.alibaba + fastjson + + + org.projectlombok + lombok + provided + + + + + + + org.apache.maven.plugins + maven-resources-plugin + 3.1.0 + + UTF-8 + + + + org.apache.maven.plugins + maven-source-plugin + 3.1.0 + + + attach-sources + verify + + jar-no-fork + + + + + + + + \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/annotation/msgconsumer/DefaultQSFMsgConsumer.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/annotation/msgconsumer/DefaultQSFMsgConsumer.java new file mode 100644 index 00000000..d72fd8c0 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/annotation/msgconsumer/DefaultQSFMsgConsumer.java @@ -0,0 +1,122 @@ +/* + * 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.rocketmq.spring.qsf.annotation.msgconsumer; + +import java.util.List; + +import com.alibaba.fastjson.JSON; +import org.apache.rocketmq.spring.qsf.beans.ApplicationContextHelper; +import org.apache.rocketmq.spring.qsf.model.MethodInvokeInfo; +import org.apache.rocketmq.spring.qsf.postprocessor.QSFProviderPostProcessor; +import org.apache.rocketmq.spring.qsf.preprocessor.QSFProviderPreProcessor; +import org.apache.rocketmq.spring.qsf.util.ReflectionMethodInvoker; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.common.message.MessageExt; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +/** + * @desc Listen to the MethodInvokeInfo message, find the execution bean, and pass parameters to execute. + **/ + +@Component +@Slf4j +public class DefaultQSFMsgConsumer implements MessageListenerConcurrently { + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, + ConsumeConcurrentlyContext context) { + if (msgs == null) { + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + + List qsfProviderPostProcessorList = QSFProviderPostProcessor.getQsfProviderPostProcessorList(); + List qsfProviderPreProcessorList = QSFProviderPreProcessor.getQsfProviderPreProcessorList(); + try { + for (MessageExt msg : msgs) { + String invokeInfoJson = new String(msg.getBody()); + log.info(" consume message id:{} key:{} body:{}", msg.getMsgId(), msg.getKeys(), invokeInfoJson); + MethodInvokeInfo methodInvokeInfo = JSON.parseObject(invokeInfoJson, MethodInvokeInfo.class); + + boolean needProcessing = true; + QSFProviderPreProcessor breakProcessor = null; + for (QSFProviderPreProcessor preProcessor : qsfProviderPreProcessorList) { + // execute preprocessing, such as unified idempotency support, etc. + needProcessing = preProcessor.callBeforeMessageProcess(methodInvokeInfo); + if (!needProcessing) { + breakProcessor = preProcessor; + break; + } + } + if (!needProcessing) { + log.info(" invoke break because {} returns false for invokeInfoJson:{}", breakProcessor, invokeInfoJson); + continue; + } + + castArgsType(methodInvokeInfo); + + Object serviceBean = ApplicationContextHelper.getBeanByTypeName(methodInvokeInfo.getInvokeBeanType()); + Object returnValue = ReflectionMethodInvoker.invoke( + serviceBean, + methodInvokeInfo.getMethodName(), + methodInvokeInfo.getArgsTypes(), methodInvokeInfo.getArgs()); + + if (qsfProviderPostProcessorList.size() == 0) { + // no post processor exists + continue; + } + + for (QSFProviderPostProcessor qsfProviderPostProcessor : qsfProviderPostProcessorList) { + /** + * Perform post-processing, such as calling syncValueCallBack to notify that the message is processed and pass the return value + */ + qsfProviderPostProcessor.callAfterMessageProcess(methodInvokeInfo, returnValue); + } + } + } catch (Throwable e) { + log.info(" consume message fail, msgs:{}", msgs, e); + return ConsumeConcurrentlyStatus.RECONSUME_LATER; + } + + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + + /** + * Solve the problem of inaccurate subclass and superclass type after json serialization, and loss of numberic type (Long Byte, etc. are all changed to Integer). + * Do not use autotyped json serialization to maintain security & compatible with json without autotype. + * Because QSF's MethodInvokeInfo has all parameter type information, qsf does not need json autotype, just to do accurate type cast by itself, and does not need custom serialization. + * (Considering readability and compatibility issues, and the MethodInvokeInfo object is small and insensitive to performance, json is the most suitable solution for qsf to pass method parameter serialization) + */ + private void castArgsType(MethodInvokeInfo methodInvokeInfo) { + if (methodInvokeInfo.getArgs() == null) { + return; + } + + for (int i = 0; i < methodInvokeInfo.getArgs().length; i++) { + Object arg = methodInvokeInfo.getArgs()[i]; + Class argClass = methodInvokeInfo.getArgsTypes()[i]; + if (arg == null || argClass.isAssignableFrom(arg.getClass())) { + continue; + } + + methodInvokeInfo.getArgs()[i] = JSON.parseObject(JSON.toJSONString(arg), argClass); + } + } +} \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/annotation/msgconsumer/QSFMsgConsumer.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/annotation/msgconsumer/QSFMsgConsumer.java new file mode 100644 index 00000000..ce5ec69c --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/annotation/msgconsumer/QSFMsgConsumer.java @@ -0,0 +1,87 @@ +/* + * 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.rocketmq.spring.qsf.annotation.msgconsumer; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apache.rocketmq.spring.qsf.annotation.msgproducer.QSFServiceConsumer; +import org.apache.rocketmq.common.protocol.heartbeat.MessageModel; + +import org.springframework.core.annotation.AliasFor; +import org.springframework.stereotype.Component; + +/** + * @desc The executor of the annotation in the RPC call; the actual implementation is to receive and process mq messages, and the receiving part is wrapped by the qsf framework + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Component +@QSFServiceProvider(topic = "", consumerId = "", messageListenerBeanClass = DefaultQSFMsgConsumer.class) +public @interface QSFMsgConsumer { + + /** + * message topic + */ + @AliasFor(annotation = QSFServiceProvider.class) + String topic(); + + /** + * message consumerId + */ + @AliasFor(annotation = QSFServiceProvider.class) + String consumerId(); + + /** + * message tag + */ + @AliasFor(annotation = QSFServiceProvider.class) + String tags() default "*"; + + /** + * selectorSql expression, Rocketmq supported + */ + @AliasFor(annotation = QSFServiceProvider.class) + String selectorSql() default ""; + + /** + * Minimum number of consumer threads + */ + @AliasFor(annotation = QSFServiceProvider.class) + int minConsumeThreads() default 8; + + /** + * Maximum number of consumer threads + */ + @AliasFor(annotation = QSFServiceProvider.class) + int maxConsumeThreads() default 16; + + @AliasFor(annotation = QSFServiceProvider.class) + QSFServiceConsumer.QueueType queueType() default QSFServiceConsumer.QueueType.ROCKET_MQ; + + @AliasFor(annotation = QSFServiceProvider.class) + MessageModel messageModel() default MessageModel.CLUSTERING; + + /** + * Message consumption model, push or pull + */ + @AliasFor(annotation = QSFServiceProvider.class) + QSFServiceProvider.ConsumeModel consumeModel() default QSFServiceProvider.ConsumeModel.push; +} diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/annotation/msgconsumer/QSFServiceProvider.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/annotation/msgconsumer/QSFServiceProvider.java new file mode 100644 index 00000000..2b616d50 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/annotation/msgconsumer/QSFServiceProvider.java @@ -0,0 +1,94 @@ +/* + * 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.rocketmq.spring.qsf.annotation.msgconsumer; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apache.rocketmq.client.consumer.listener.MessageListener; +import org.apache.rocketmq.spring.qsf.annotation.msgproducer.QSFServiceConsumer; +import org.apache.rocketmq.common.protocol.heartbeat.MessageModel; + +import org.springframework.stereotype.Component; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Component +public @interface QSFServiceProvider { + + Class DEFAULT_MESSAGE_LISTENER_CLASS = MessageListener.class; + + /** + * message processing bean class + * messageListenerBeanClass needs to implement MessageListenerConcurrently or MessageListenerOrderly + * @return + */ + Class messageListenerBeanClass() default DefaultQSFMsgConsumer.class; + + /** + * message topic + * @return + */ + String topic(); + + /** + * message consumerId + * @return + */ + String consumerId(); + + /** + * message tag + * @return + */ + String tags() default "*"; + + /** + * selectorSql expression, Rocketmq supported + * @return + */ + String selectorSql() default ""; + + /** + * Minimum number of consumer threads + * @return + */ + int minConsumeThreads() default 8; + + /** + * Maximum number of consumer threads + * @return + */ + int maxConsumeThreads() default 16; + + QSFServiceConsumer.QueueType queueType() default QSFServiceConsumer.QueueType.ROCKET_MQ; + + MessageModel messageModel() default MessageModel.CLUSTERING; + + /** + * Message consumption model, push or pull + * @return + */ + ConsumeModel consumeModel() default ConsumeModel.push; + + enum ConsumeModel { + push; + } +} diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/annotation/msgproducer/DefaultQSFRocketmqMsgSender.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/annotation/msgproducer/DefaultQSFRocketmqMsgSender.java new file mode 100644 index 00000000..ef53796b --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/annotation/msgproducer/DefaultQSFRocketmqMsgSender.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.rocketmq.spring.qsf.annotation.msgproducer; + +import com.alibaba.fastjson.JSON; +import org.apache.rocketmq.spring.qsf.model.MethodInvokeInfo; +import org.apache.rocketmq.client.producer.MQProducer; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.common.message.Message; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Component; + +/** + * @desc qsf rocketmq message producer + */ +@Slf4j +@Component +public class DefaultQSFRocketmqMsgSender implements QSFMsgSender { + + @Autowired + @Qualifier("qsfRocketmqMsgProducer") + private MQProducer qsfRocketmqMsgProducer; + + @Override + public String sendInvokeMsg(String topic, String tag, QSFServiceConsumer.QueueType queueType, MethodInvokeInfo methodInvokeInfo) { + try { + String invokeInfoJson = JSON.toJSONString(methodInvokeInfo); + Message message = new Message(topic, tag, invokeInfoJson.getBytes("utf-8")); + message.setKeys(methodInvokeInfo.buildMethodInvokeInstanceSignature()); + if (queueType == null) { + queueType = QSFServiceConsumer.QueueType.ROCKET_MQ; + } + switch (queueType) { + case ROCKET_MQ: + default: + SendResult sendResult = qsfRocketmqMsgProducer.send(message); + log.info(" sendMessage methodInvokeInfo:{} result:{}", methodInvokeInfo, sendResult); + return sendResult.getMsgId(); + } + } catch (Throwable e) { + log.error(" sendInvokeMsg fail, topic:{},tag:{},methodInvokeInfo:{}", topic, tag, + methodInvokeInfo, e); + throw new RuntimeException("sendInvokeMsg fail " + methodInvokeInfo, e); + } + } + +} \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/annotation/msgproducer/QSFMethodInvokeSpecial.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/annotation/msgproducer/QSFMethodInvokeSpecial.java new file mode 100644 index 00000000..49ada1b0 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/annotation/msgproducer/QSFMethodInvokeSpecial.java @@ -0,0 +1,59 @@ +/* + * 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.rocketmq.spring.qsf.annotation.msgproducer; + +/** + * @desc Specify the method call configuration. If you need a return value, you must specify methodSpecials.syncCall=true + */ +public @interface QSFMethodInvokeSpecial { + /** + * true: synchronous call, block the current thread after sending the message, until the message listener processes the message, then notify the message sending thread, and then continue to execute + * false: asynchronous call, do not wait after sending a message, continue execution directly. + * + * Synchronous invocation expands the usage scenarios of mq; please do business evaluation and capacity planning when using it, and make sure that synchronous blocking will not cause business risks. + * + * To enable syncCall, you need to import dependency rocketmq-spring-qsf-callback-dubbo. + */ + boolean syncCall() default false; + + /** + * When called synchronously (syncCall=true), after sending a message, how many milliseconds will await. + */ + long syncCallBackTimeoutMs() default 3000L; + + /** + * The specified method is executed according to the ConsumerMethodSpecial configuration + */ + String methodName(); + + /** + * Which callbacks from mq listener to wait for to wake up the message sending thread, if not specified, any callback is allowed to wake up the message sending thread. + * The appName used in the mq producer is ${qsf.project.name} , or an empty string if the configuration does not exist. + */ + String[] waitCallBackAppNames() default {}; + + /** + * Specify the callback return value of the message producer, if not specified, the thread that send the message will take the last callback as return value. + */ + String returnValueAppName() default ""; + + /** + * The minimum number of callbacks to wake up the message sending thread. + */ + int minCallBackTimes() default 1; +} \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/annotation/msgproducer/QSFMsgProducer.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/annotation/msgproducer/QSFMsgProducer.java new file mode 100644 index 00000000..741cc2ff --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/annotation/msgproducer/QSFMsgProducer.java @@ -0,0 +1,70 @@ +/* + * 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.rocketmq.spring.qsf.annotation.msgproducer; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.annotation.AliasFor; + +/** + * @desc QSF call initiator(message producer) annotation + * + * QSF:queue service framework + * A framework that wraps the message queue as a standard method call (it cannot be regarded as an RPC framework based on a message queue. For most scenarios, asynchronous message calls cannot replace synchronous RPC calls). + * Annotation is on the initiator of the call, the actual implementation is to send mq messages, and the sending capability is wrapped by the QSF framework. + * Note that if a return value is required, methodSpecials.syncCall=true must be specified, otherwise an exception will be thrown. + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +@Autowired +@QSFServiceConsumer(topic = "", messageSenderBeanClass = DefaultQSFRocketmqMsgSender.class) +public @interface QSFMsgProducer { + /** + * message producer bean class + */ + @AliasFor(annotation = QSFServiceConsumer.class) + Class messageSenderBeanClass() default DefaultQSFRocketmqMsgSender.class; + + /** + * message topic + */ + @AliasFor(annotation = QSFServiceConsumer.class) + String topic(); + + /** + * message tag + */ + @AliasFor(annotation = QSFServiceConsumer.class) + String tag() default ""; + + /** + * message queue type + */ + @AliasFor(annotation = QSFServiceConsumer.class) + QSFServiceConsumer.QueueType queueType() default QSFServiceConsumer.QueueType.ROCKET_MQ; + + /** + * Specify the method call configuration. If you need a return value, you must specify methodSpecials.syncCall=true + */ + @AliasFor(annotation = QSFServiceConsumer.class) + QSFMethodInvokeSpecial[] methodSpecials() default {}; +} \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/annotation/msgproducer/QSFMsgSender.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/annotation/msgproducer/QSFMsgSender.java new file mode 100644 index 00000000..45a97bed --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/annotation/msgproducer/QSFMsgSender.java @@ -0,0 +1,31 @@ +/* + * 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.rocketmq.spring.qsf.annotation.msgproducer; + +import org.apache.rocketmq.spring.qsf.model.MethodInvokeInfo; + +public interface QSFMsgSender { + /** + * send MethodInvokeInfo message + * @param topic message topic + * @param tag message tag + * @param methodInvokeInfo method invoke information + * @return + */ + String sendInvokeMsg(String topic, String tag, QSFServiceConsumer.QueueType queueType, MethodInvokeInfo methodInvokeInfo); +} \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/annotation/msgproducer/QSFServiceConsumer.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/annotation/msgproducer/QSFServiceConsumer.java new file mode 100644 index 00000000..47544ccc --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/annotation/msgproducer/QSFServiceConsumer.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.rocketmq.spring.qsf.annotation.msgproducer; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.beans.factory.annotation.Autowired; + +/** + * @desc annotation on class members + * The type annotated (usually an interface, no local implementation is required), all methods under the type will be proxied, and the proxy behavior is to send message to the topic configured by MsgProducer. + * Message structure: MethodInvokeInfo serialized by json. + */ +@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Autowired +public @interface QSFServiceConsumer { + + /** + * message sending bean class + */ + Class messageSenderBeanClass(); + + /** + * message topic + */ + String topic(); + + /** + * message tag + */ + String tag() default ""; + + /** + * message queue type + */ + QueueType queueType() default QueueType.ROCKET_MQ; + + /** + * Specify the method call configuration. If you need a return value, you must specify methodSpecials.syncCall=true + */ + QSFMethodInvokeSpecial[] methodSpecials() default {}; + + enum QueueType { + ROCKET_MQ; + } +} diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/beans/ApplicationContextHelper.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/beans/ApplicationContextHelper.java new file mode 100644 index 00000000..59489a6a --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/beans/ApplicationContextHelper.java @@ -0,0 +1,70 @@ +/* + * 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.rocketmq.spring.qsf.beans; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.context.ApplicationContext; + +/** + * @desc ApplicationContextHelper + **/ + +public class ApplicationContextHelper { + private static ApplicationContext applicationContext; + + private static Map beanTypeName2BeanMap = new ConcurrentHashMap<>(96); + + public static T getBeanByTypeName(String beanTypeName) { + Object bean = beanTypeName2BeanMap.get(beanTypeName); + if (bean != null) { + return (T)bean; + } + + try { + Class beanType = Class.forName(beanTypeName); + bean = applicationContext.getBean(beanType); + beanTypeName2BeanMap.put(beanTypeName, bean); + + return (T)bean; + } catch (Throwable e) { + throw new RuntimeException("getBeanByTypeName fail for " + beanTypeName, e); + } + } + + public static T getBean(Class type) { + return applicationContext.getBean(type); + } + + public static T getBean(Class type, String beanName) { + return applicationContext.getBean(beanName, type); + } + + public static Map getBeansOfType(Class type) { + return applicationContext.getBeansOfType(type); + } + + public static ApplicationContext getApplicationContext() { + return applicationContext; + } + + public static void setApplicationContext(ApplicationContext appContext) { + applicationContext = appContext; + } +} \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/beans/ApplicationStartedListener.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/beans/ApplicationStartedListener.java new file mode 100644 index 00000000..3681def5 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/beans/ApplicationStartedListener.java @@ -0,0 +1,60 @@ +/* + * 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.rocketmq.spring.qsf.beans; + +import java.util.Map; + +import org.apache.rocketmq.spring.qsf.util.ClearableAfterApplicationStarted; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.stereotype.Component; + +/** + * @desc + **/ + +@Component +@Slf4j +public class ApplicationStartedListener implements ApplicationListener { + + @Override + public void onApplicationEvent(ContextRefreshedEvent event) { + log.info(" application context refreshed:{}", event); + + if (event == null || event.getApplicationContext() == null) { + return; + } + + Map clearableAfterApplicationStartedMap = event.getApplicationContext().getBeansOfType(ClearableAfterApplicationStarted.class); + if (clearableAfterApplicationStartedMap == null || clearableAfterApplicationStartedMap.size() == 0) { + log.info(" no ClearableAfterApplicationStarted bean in current application"); + return; + } + + for (ClearableAfterApplicationStarted bean : clearableAfterApplicationStartedMap.values()) { + try { + bean.clearAfterApplicationStart(); + log.info(" clearAfterApplicationStart success, bean:{}", bean); + } catch (Throwable e) { + log.info(" clearAfterApplicationStart fail, bean:{}", bean, e); + } + } + } +} diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/beans/QSFInfraBeans.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/beans/QSFInfraBeans.java new file mode 100644 index 00000000..0c01d197 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/beans/QSFInfraBeans.java @@ -0,0 +1,62 @@ +/* + * 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.rocketmq.spring.qsf.beans; + +import org.apache.rocketmq.client.exception.MQClientException; + +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.MQProducer; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @desc QSF infra beans + **/ + +@Configuration +@Slf4j +public class QSFInfraBeans { + @Value("${qsf.rocketmq.name-server}") + private String namesrvAddr; + + @Bean(name = "namesrvAddr") + public String namesrvAddr() { + return namesrvAddr; + } + + @Bean(name = "qsfRocketmqMsgProducer") + public MQProducer qsfRocketmqMsgProducer() { + DefaultMQProducer qsfRocketmqMsgProducer = new DefaultMQProducer(); + qsfRocketmqMsgProducer.setNamesrvAddr(namesrvAddr); + qsfRocketmqMsgProducer.setProducerGroup("qsf_rocketmq_producer_group"); + qsfRocketmqMsgProducer.setSendMsgTimeout(3000); + qsfRocketmqMsgProducer.setCompressMsgBodyOverHowmuch(4096); + qsfRocketmqMsgProducer.setRetryTimesWhenSendFailed(3); + + try { + qsfRocketmqMsgProducer.start(); + } catch (MQClientException e) { + log.error(" qsfRocketmqMsgProducer start fail", e); + throw new RuntimeException("qsfRocketmqMsgProducer start fail", e); + } + + return qsfRocketmqMsgProducer; + } +} diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/consumer/QSFConsumerBeanFactoryPostProcessor.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/consumer/QSFConsumerBeanFactoryPostProcessor.java new file mode 100644 index 00000000..45068319 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/consumer/QSFConsumerBeanFactoryPostProcessor.java @@ -0,0 +1,174 @@ +/* + * 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.rocketmq.spring.qsf.consumer; + +import java.lang.reflect.Field; +import java.util.List; +import java.util.ArrayList; +import java.util.Set; +import java.util.HashSet; +import java.util.Map; +import java.util.HashMap; +import java.util.LinkedHashMap; + +import org.apache.rocketmq.spring.qsf.annotation.msgproducer.QSFServiceConsumer; +import org.apache.rocketmq.spring.qsf.annotation.msgproducer.QSFMethodInvokeSpecial; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeansException; +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.stereotype.Component; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +/** + * @desc + **/ + +@Component +@Slf4j +public class QSFConsumerBeanFactoryPostProcessor implements BeanClassLoaderAware, BeanFactoryPostProcessor, + ApplicationContextAware { + private ClassLoader classLoader; + + private ApplicationContext context; + + private Map beanDefinitions = new LinkedHashMap<>(); + + private Map> beanIdentifierMap = new HashMap<>(); + + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.classLoader = classLoader; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.context = applicationContext; + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + final List beanClasses = new ArrayList<>(beanFactory.getBeanDefinitionNames().length); + for (String beanName : beanFactory.getBeanDefinitionNames()) { + BeanDefinition definition = beanFactory.getBeanDefinition(beanName); + if (definition.getBeanClassName() != null) { + beanClasses.add(definition.getBeanClassName()); + } + } + + for (String beanName : beanFactory.getBeanDefinitionNames()) { + BeanDefinition definition = beanFactory.getBeanDefinition(beanName); + + String beanClassName = definition.getBeanClassName(); + // beanClassName is null when the type returned with @Bean is Object + if (beanClassName != null) { + Class clazz = ClassUtils.resolveClassName(definition.getBeanClassName(), this.classLoader); + ReflectionUtils.doWithFields(clazz, new ReflectionUtils.FieldCallback() { + @Override + public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException { + parseElement(field, beanClasses); + } + }); + } + } + + BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory; + for (String beanName : beanDefinitions.keySet()) { + if (context.containsBean(beanName)) { + throw new IllegalArgumentException("[QSF Starter] Spring context already has a bean named " + beanName + + ", please change @QSFConsumer field name."); + } + registry.registerBeanDefinition(beanName, beanDefinitions.get(beanName)); + log.info(" registered QSFConsumerBean {} in spring context.", beanName); + } + } + + private void parseElement(Field field, List beanClasses) { + QSFServiceConsumer annotation = AnnotationUtils.getAnnotation(field, QSFServiceConsumer.class); + if (annotation == null) { + return; + } + + /** + * Check if there is a local QSF service bean definition, if not, create a local qsf service bean definition to prevent autowired fail. + * Regardless of whether there is a local bean or not, the actual qsf service bean used is the proxy qsf service created in QSFConsumerBeanPostProcessor. + */ + try { + // Prevent the classloader of fieldType beanClass from being inconsistent, so add an extra layer of class.forName + Class fieldType = Class.forName(field.getType().getName()); + for (String beanClassName : beanClasses) { + Class beanClass = Class.forName(beanClassName); + if (fieldType.isAssignableFrom(beanClass)) { + return; + } + } + } catch (ClassNotFoundException e) { + log.error(" check qsf local implement fail, field:" + field, e); + throw new RuntimeException("check qsf local implement fail, field:" + field, e); + } + + BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(QSFConsumerMockBeanFactory.class); + beanDefinitionBuilder.addPropertyValue("serviceInterface", field.getType()); + beanDefinitionBuilder.addPropertyValue("msgProducerConfig", annotation); + + QSFServiceConsumer msgProducerConfig = AnnotationUtils.getAnnotation(field, QSFServiceConsumer.class); + QSFMethodInvokeSpecial[] methodSpecials = msgProducerConfig == null ? null : msgProducerConfig.methodSpecials(); + beanDefinitionBuilder.addPropertyValue("methodSpecialsConfig", methodSpecials); + + BeanDefinition beanDefinition = beanDefinitionBuilder.getBeanDefinition(); + + if (checkFieldNameDuplicate4FirstTime(field.getName(), beanDefinition)) { + log.warn(" registered QSFConsumerBean with duplicate fieldName:{} in spring context.", field.getName()); + } + beanDefinitions.put(field.getName(), beanDefinition); + } + + private boolean checkFieldNameDuplicate4FirstTime(String fieldName, BeanDefinition beanDefinition) { + Set serviceIdentiferSet = beanIdentifierMap.get(fieldName); + String serviceIdentifier = getServiceIdentifier(beanDefinition); + if (serviceIdentiferSet == null) { + Set tmp = new HashSet(); + tmp.add(serviceIdentifier); + beanIdentifierMap.put(fieldName, tmp); + return false; + } + // if not first check + if (serviceIdentiferSet.contains(serviceIdentifier)) { + return false; + } else { + serviceIdentiferSet.add(serviceIdentifier); + return true; + } + } + + private String getServiceIdentifier(BeanDefinition beanDefinition) { + MutablePropertyValues mutablePropertyValues = beanDefinition.getPropertyValues(); + return mutablePropertyValues.get("serviceInterface") + "_" + mutablePropertyValues.get("msgProdcerConfig") + "_" + mutablePropertyValues.get("methodSpecialsConfig"); + } + +} diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/consumer/QSFConsumerBeanPostProcessor.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/consumer/QSFConsumerBeanPostProcessor.java new file mode 100644 index 00000000..3cffe66a --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/consumer/QSFConsumerBeanPostProcessor.java @@ -0,0 +1,269 @@ +/* + * 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.rocketmq.spring.qsf.consumer; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import javax.annotation.Resource; + +import org.apache.rocketmq.spring.qsf.annotation.msgproducer.QSFServiceConsumer; +import org.apache.rocketmq.spring.qsf.annotation.msgproducer.QSFMethodInvokeSpecial; +import org.apache.rocketmq.spring.qsf.beans.ApplicationContextHelper; +import org.apache.rocketmq.spring.qsf.util.ClearableAfterApplicationStarted; +import org.apache.rocketmq.spring.qsf.util.ReflectionUtils; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.stereotype.Component; + +/** + * @desc QSFConsumer processing: generate jdk dynamic proxy for interface, parse method call MethodInvokeInfo , send message in the proxy invoke method. + */ + +@Component +@Slf4j +public class QSFConsumerBeanPostProcessor implements BeanPostProcessor, ApplicationContextAware, + ClearableAfterApplicationStarted { + + private ApplicationContext applicationContext; + + /** + * Before the QSF consumer proxy bean is processed, record the map of the injected field and the bean to which the field belongs, so as to replace the field bean when the QSF consumer proxy bean is generated + */ + private Map injectedFieldBeanMap = new ConcurrentHashMap<>(768); + private Map> injectedTypeFieldsMap = new ConcurrentHashMap<>(256); + + // Record the qsf consumer proxy bean map to replace the field bean after the qsf consumer proxy bean is generated + private Map qsfConsumerBeansMap = new ConcurrentHashMap<>(96); + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (ApplicationContextHelper.getApplicationContext() == null) { + ApplicationContextHelper.setApplicationContext(applicationContext); + } + + List declaredFields = ReflectionUtils.getAllFields(bean.getClass()); + for (int i = 0; i < declaredFields.size(); i++) { + Field declaredField = declaredFields.get(i); + declaredField.setAccessible(true); + + Class declaredFieldType = declaredField.getType(); + + try { + proxyFieldAfterQSFBean(bean, declaredField, declaredFieldType); + } catch (IllegalAccessException e) { + throw new BeanCreationException("replace qsf proxy bean fail for field field:" + declaredField, e); + } + + QSFServiceConsumer msgProducerConfig = AnnotatedElementUtils.findMergedAnnotation(declaredField, QSFServiceConsumer.class); + if (msgProducerConfig != null) { + // MsgProducer must be annotated on the interface, if it is not an interface, an BeanCreationException will be thrown. + if (!declaredFieldType.isInterface()) { + throw new BeanCreationException("MsgProducer/QSFConsumer must be used at interface"); + } + + if (msgProducerConfig.topic() == null || msgProducerConfig.topic().trim().length() == 0) { + throw new BeanCreationException("MsgProducer/QSFConsumer topic should not be empty " + declaredField); + } + + QSFMethodInvokeSpecial[] methodSpecials = msgProducerConfig == null ? null : msgProducerConfig.methodSpecials(); + + // check ConsumerMethodSpecial + checkMethodSpecialConfig(declaredField, msgProducerConfig, methodSpecials); + + // inject proxy object + try { + injectProxyBean(bean, declaredField, declaredFieldType, msgProducerConfig, methodSpecials); + + } catch (Throwable e) { + throw new BeanCreationException("create QSFConsumer proxy fail for field:" + declaredField, e); + } + } + } + + return bean; + } + + /** + * Replace field bean when qsf consumer proxy bean is being processed + * + * @param bean + * @param declaredField + * @param declaredFieldType + * @param msgProducer + * @param methodSpecials + * @throws IllegalAccessException + */ + private void injectProxyBean(Object bean, Field declaredField, Class declaredFieldType, QSFServiceConsumer msgProducer, + QSFMethodInvokeSpecial[] methodSpecials) throws IllegalAccessException { + Object objectProxy = qsfConsumerBeansMap.get(declaredFieldType); + if (objectProxy == null) { + // The consumer service that is proxied and sends messages should be a singleton + objectProxy = QSFConsumerServiceProxy.createProxy(declaredFieldType, msgProducer, + methodSpecials); + qsfConsumerBeansMap.put(declaredFieldType, objectProxy); + } + + declaredField.set(bean, objectProxy); + + List fields = injectedTypeFieldsMap.get(declaredFieldType); + if (fields != null && fields.size() > 0) { + for (Field field : fields) { + Object fieldBean = injectedFieldBeanMap.get(field); + if (fieldBean != null) { + field.set(fieldBean, objectProxy); + + injectedFieldBeanMap.remove(field); + } + } + } + } + + /** + * Register QSF consumer proxy as spring bean + * + * @param declaredField + * @param objectProxy + */ + private void registerQSFProxy(Field declaredField, Object objectProxy) { + // get BeanFactory + DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory)applicationContext.getAutowireCapableBeanFactory(); + // dynamically register beans + Qualifier qualifier = AnnotatedElementUtils.findMergedAnnotation(declaredField, Qualifier.class); + String qsfConsumerBeanName = qualifier == null ? declaredField.getName() : qualifier.value(); + defaultListableBeanFactory.registerSingleton(qsfConsumerBeanName, objectProxy); + } + + /** + * After the qsf consumer proxy bean is processed, replace the field bean + * + * @param bean + * @param declaredField + * @param declaredFieldType + */ + private void proxyFieldAfterQSFBean(Object bean, Field declaredField, Class declaredFieldType) + throws IllegalAccessException { + if (AnnotatedElementUtils.hasAnnotation(declaredField, Autowired.class) + || AnnotatedElementUtils.hasMetaAnnotationTypes(declaredField, Autowired.class) + || AnnotatedElementUtils.hasAnnotation(declaredField, Resource.class) + || AnnotatedElementUtils.hasMetaAnnotationTypes(declaredField, Resource.class) + ) { + Object proxyBean = qsfConsumerBeansMap.get(declaredFieldType); + if (proxyBean != null) { + declaredField.set(bean, proxyBean); + } else { + injectedFieldBeanMap.put(declaredField, bean); + + List fields = injectedTypeFieldsMap.get(declaredFieldType); + if (fields == null) { + fields = new ArrayList<>(); + injectedTypeFieldsMap.put(declaredFieldType, fields); + } + fields.add(declaredField); + } + } + } + + /** + * Check ConsumerMethodSpecial. + * For methods whose return type is not void, ConsumerMethodSpecial.syncCall=true must be specified + * + * @param declaredField + * @param consumerConfig + * @param methodSpecials + */ + private void checkMethodSpecialConfig(Field declaredField, QSFServiceConsumer consumerConfig, + QSFMethodInvokeSpecial[] methodSpecials) { + List methodsWithReturn = getQSFMethodsWithReturn(declaredField, consumerConfig); + if (methodsWithReturn.size() > 0 && (methodSpecials == null || methodSpecials.length == 0)) { + /** + * The consumer has a return value, + * you must specify @ConsumerMethodSpecial.syncCall=true to pass the return value through the callback + */ + throw new BeanCreationException("methods with return value should add annotation ConsumerMethodSpecial.syncCall=true, or return void instead:" + declaredField + "#" + methodsWithReturn); + } + + if (methodSpecials != null && methodSpecials.length > 0) { + Set syncCallMethods = new HashSet<>(); + for (QSFMethodInvokeSpecial methodSpecial : methodSpecials) { + if (methodSpecial == null || !Boolean.TRUE.equals(methodSpecial.syncCall())) { + continue; + } + + syncCallMethods.add(methodSpecial.methodName()); + } + + for (String methodName : methodsWithReturn) { + if (!syncCallMethods.contains(methodName)) { + throw new BeanCreationException("method with return value should add annotation ConsumerMethodSpecial.syncCall=true, or return void instead:" + declaredField + "#" + methodName); + } + } + } + } + + private List getQSFMethodsWithReturn(Field declaredField, QSFServiceConsumer consumerConfig) { + List methodsWithReturn = new ArrayList<>(); + if (consumerConfig != null) { + Method[] methods = declaredField.getType().getMethods(); + if (methods != null) { + for (Method method : methods) { + if (!Void.TYPE.equals(method.getReturnType())) { + methodsWithReturn.add(method.getName()); + } + } + } + } + return methodsWithReturn; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + /** + * After the application context starts, clean up the data that is no longer used + */ + @Override + public void clearAfterApplicationStart() { + this.injectedFieldBeanMap = null; + this.injectedTypeFieldsMap = null; + + this.qsfConsumerBeansMap = null; + } +} \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/consumer/QSFConsumerMockBeanFactory.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/consumer/QSFConsumerMockBeanFactory.java new file mode 100644 index 00000000..61f405f1 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/consumer/QSFConsumerMockBeanFactory.java @@ -0,0 +1,58 @@ +/* + * 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.rocketmq.spring.qsf.consumer; + +import org.apache.rocketmq.spring.qsf.annotation.msgproducer.QSFServiceConsumer; +import org.apache.rocketmq.spring.qsf.annotation.msgproducer.QSFMethodInvokeSpecial; + +import org.springframework.beans.factory.FactoryBean; + +/** + * @desc + **/ + +public class QSFConsumerMockBeanFactory implements FactoryBean { + + private Class serviceInterface; + + private QSFServiceConsumer msgProducerConfig; + + private QSFMethodInvokeSpecial[] methodSpecialsConfig; + + @Override + public Object getObject() throws Exception { + return QSFConsumerServiceProxy.createProxy(serviceInterface, msgProducerConfig, methodSpecialsConfig); + } + + @Override + public Class getObjectType() { + return serviceInterface; + } + + public void setServiceInterface(Class serviceInterface) { + this.serviceInterface = serviceInterface; + } + + public void setMsgProducerConfig(QSFServiceConsumer msgProducerConfig) { + this.msgProducerConfig = msgProducerConfig; + } + + public void setMethodSpecialsConfig(QSFMethodInvokeSpecial[] methodSpecialsConfig) { + this.methodSpecialsConfig = methodSpecialsConfig; + } +} diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/consumer/QSFConsumerMockProxyInvocationHandler.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/consumer/QSFConsumerMockProxyInvocationHandler.java new file mode 100644 index 00000000..fd56c201 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/consumer/QSFConsumerMockProxyInvocationHandler.java @@ -0,0 +1,33 @@ +/* + * 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.rocketmq.spring.qsf.consumer; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; + +/** + * @desc + **/ + +public class QSFConsumerMockProxyInvocationHandler implements InvocationHandler { + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + return null; + } +} \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/consumer/QSFConsumerServiceProxy.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/consumer/QSFConsumerServiceProxy.java new file mode 100644 index 00000000..c10ab60c --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/consumer/QSFConsumerServiceProxy.java @@ -0,0 +1,40 @@ +/* + * 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.rocketmq.spring.qsf.consumer; + +import java.lang.reflect.Proxy; + +import org.apache.rocketmq.spring.qsf.annotation.msgproducer.QSFServiceConsumer; +import org.apache.rocketmq.spring.qsf.annotation.msgproducer.QSFMethodInvokeSpecial; + +/** + * @desc jdk proxy creator + */ + +public class QSFConsumerServiceProxy { + + public static T createProxy(Class interfaceCls, QSFServiceConsumer msgProducerConfig, QSFMethodInvokeSpecial[] methodSpecials) { + return (T) Proxy.newProxyInstance(interfaceCls.getClassLoader(), new Class[]{interfaceCls}, + new QSFConsumerServiceProxyInvocationHandler(interfaceCls, msgProducerConfig, methodSpecials)); + } + + public static T createMockProxy(Class interfaceCls) { + return (T) Proxy.newProxyInstance(interfaceCls.getClassLoader(), new Class[]{interfaceCls}, + new QSFConsumerMockProxyInvocationHandler()); + } +} diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/consumer/QSFConsumerServiceProxyInvocationHandler.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/consumer/QSFConsumerServiceProxyInvocationHandler.java new file mode 100644 index 00000000..858ab311 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/consumer/QSFConsumerServiceProxyInvocationHandler.java @@ -0,0 +1,155 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.spring.qsf.consumer; + +import org.apache.rocketmq.spring.qsf.annotation.msgproducer.QSFServiceConsumer; +import org.apache.rocketmq.spring.qsf.annotation.msgproducer.QSFMethodInvokeSpecial; +import org.apache.rocketmq.spring.qsf.annotation.msgproducer.QSFMsgSender; +import org.apache.rocketmq.spring.qsf.beans.ApplicationContextHelper; +import org.apache.rocketmq.spring.qsf.model.MethodInvokeInfo; +import org.apache.rocketmq.spring.qsf.postprocessor.QSFConsumerPostProcessor; +import org.apache.rocketmq.spring.qsf.preprocessor.QSFConsumerPreProcessor; +import org.apache.rocketmq.spring.qsf.util.IPUtils; +import org.apache.rocketmq.spring.qsf.util.KeyUtils; +import org.apache.rocketmq.spring.qsf.util.ReflectionUtils; +import lombok.extern.slf4j.Slf4j; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @desc + */ + +@Slf4j +public class QSFConsumerServiceProxyInvocationHandler implements InvocationHandler { + private QSFMsgSender qsfMsgSender = ApplicationContextHelper.getBean(QSFMsgSender.class); + + private Class serviceInterface; + + private String serviceInterfaceName; + + private Map localJvmMethods = getLocalJvmMethods(); + + private String topic; + + private String tag; + + private QSFServiceConsumer.QueueType queueType; + + /** + * Consumer class configuration, including topic, tag + */ + private QSFServiceConsumer msgProducerConfig; + + /** + * Consumer method configuration, including whether the method needs to block & wait to be woken up by the callback + */ + private Map methodSpecialsMap; + + public QSFConsumerServiceProxyInvocationHandler(Class serviceInterface, QSFServiceConsumer msgProducerConfig, QSFMethodInvokeSpecial[] methodSpecials) { + this.serviceInterface = serviceInterface; + + this.serviceInterfaceName = serviceInterface.getName(); + + this.topic = msgProducerConfig.topic(); + this.tag = msgProducerConfig.tag(); + this.queueType = msgProducerConfig.queueType(); + + if (methodSpecials == null || methodSpecials.length == 0) { + this.methodSpecialsMap = Collections.emptyMap(); + return; + } + + this.methodSpecialsMap = new HashMap<>(methodSpecials.length); + for (QSFMethodInvokeSpecial methodSpecial : methodSpecials) { + if (methodSpecial == null || methodSpecial.methodName() == null || methodSpecial.methodName().trim().length() == 0) { + continue; + } + + this.methodSpecialsMap.put(methodSpecial.methodName().trim(), methodSpecial); + } + } + + private static Map getLocalJvmMethods() { + Method[] methods = Object.class.getMethods(); + int methodsCount = methods == null ? 0 : methods.length; + Map objectMethods = new HashMap<>(methodsCount); + if (methods != null) { + for (Method method : methods) { + objectMethods.put(ReflectionUtils.getMethodSignature(method), method); + } + } + + return objectMethods; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + String methodSignature = ReflectionUtils.getMethodSignature(method); + Method localJvmMethod = localJvmMethods.get(methodSignature); + if (localJvmMethod != null) { + return localJvmMethod.invoke(proxy, args); + } + + String methodName = method.getName(); + QSFMethodInvokeSpecial methodSpecial = this.methodSpecialsMap.get(methodName); + + Boolean syncCall = methodSpecial == null ? null : methodSpecial.syncCall(); + MethodInvokeInfo methodInvokeInfo = MethodInvokeInfo.builder() + .invokeBeanType(serviceInterfaceName) + .args(args) + .methodName(methodName) + .argsTypes(method.getParameterTypes()) + .sourceIp(IPUtils.getLocalIp()) + .sourceCallKey(KeyUtils.callKey()) + .syncCall(syncCall) + .build(); + + List qsfConsumerPreProcessorList = QSFConsumerPreProcessor.getQsfConsumerPreProcessorList(); + for (QSFConsumerPreProcessor preProcessor : qsfConsumerPreProcessorList) { + preProcessor.callBeforeMessageSend(methodInvokeInfo, msgProducerConfig, methodSpecial); + } + + qsfMsgSender.sendInvokeMsg(topic, tag, queueType, methodInvokeInfo); + + List qsfConsumerPostProcessorList = QSFConsumerPostProcessor.getQsfConsumerPostProcessorList(); + if (qsfConsumerPostProcessorList.size() == 0) { + // no post processor exists + return null; + } + + Object returnValue = null; + for (QSFConsumerPostProcessor qsfConsumerPostProcessor : qsfConsumerPostProcessorList) { + // After the message is processed, the post-processor is executed to handle idempotency, RPC callbacks, etc. + Object value = qsfConsumerPostProcessor.callAfterMessageSend(methodInvokeInfo, msgProducerConfig, methodSpecial); + if (returnValue == null && value != null) { + // take the first non-null return as the return value + // only the RPC callback post-processor will return the return value of the called method + returnValue = value; + } + } + + return returnValue; + } + +} \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/model/MethodInvokeInfo.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/model/MethodInvokeInfo.java new file mode 100644 index 00000000..30f90594 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/model/MethodInvokeInfo.java @@ -0,0 +1,122 @@ +/* + * 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.rocketmq.spring.qsf.model; + +import com.alibaba.fastjson.JSON; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; + +/** + * @desc describe method call information: invokeBeanType, methodName, argsTypes[], args[] + **/ + +@Data +@ToString +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class MethodInvokeInfo { + /** + * Invoke the principal bean type through which the provider bean will be found + **/ + private String invokeBeanType; + + /** + * method name + **/ + private String methodName; + + /** + * arguments type array + **/ + private Class[] argsTypes; + + /** + * method arguments + **/ + private Object[] args; + + /** + * message producer ip + */ + private String sourceIp; + + /** + * message producer call key + */ + private String sourceCallKey; + + /** + * whether to call synchronously + */ + private Boolean syncCall; + + /** + * build a method signature: + * {invokeBeanType}.{methodName}:{parametersTypes[0]#parametersTypes[1]...} + * @return + */ + public String buildMethodSignature() { + StringBuilder builder = new StringBuilder(); + builder.append(this.invokeBeanType).append('.').append(this.methodName); + if (this.argsTypes != null && this.argsTypes.length > 0) { + builder.append(':'); + for (int i = 0; i < argsTypes.length; i++) { + Class clazz = argsTypes[i]; + if (i > 0) { + builder.append('#'); + } + builder.append(clazz.getName()); + } + } + + return builder.toString(); + } + + /** + * build a method invoke instance signature: + * {invokeBeanType}.{methodName}:{parametersTypes[0]#parametersTypes[1]...}:{args[0]#args[1]...} + * can be used as an idempotent key + * @return + */ + public String buildMethodInvokeInstanceSignature() { + if (this.args == null || this.args.length == 0) { + return buildMethodSignature(); + } + + StringBuilder builder = new StringBuilder(); + builder.append(buildMethodSignature()).append(':'); + for (int i = 0; i < this.args.length; i++) { + Object obj = this.args[i]; + if (i > 0) { + builder.append('#'); + } + try { + builder.append(JSON.toJSON(obj)); + } catch (Throwable e) { + builder.append(obj); + } + } + + return builder.toString(); + } + +} \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/postprocessor/QSFConsumerPostProcessor.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/postprocessor/QSFConsumerPostProcessor.java new file mode 100644 index 00000000..cce5700e --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/postprocessor/QSFConsumerPostProcessor.java @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.spring.qsf.postprocessor; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.rocketmq.spring.qsf.annotation.msgproducer.QSFServiceConsumer; +import org.apache.rocketmq.spring.qsf.annotation.msgproducer.QSFMethodInvokeSpecial; +import org.apache.rocketmq.spring.qsf.model.MethodInvokeInfo; + +import org.springframework.beans.factory.InitializingBean; + +/** + * @desc After the mq producer sends a message, call QSFConsumerPostProcessor + */ +public abstract class QSFConsumerPostProcessor implements InitializingBean { + + protected static List qsfConsumerPostProcessorList = new ArrayList<>(); + + /** + * message sending post processing + * + * @param methodInvokeInfo invoke info structure + * @param msgProducerConfig + * @param methodSpecial + * @return + */ + public abstract Object callAfterMessageSend(MethodInvokeInfo methodInvokeInfo, QSFServiceConsumer msgProducerConfig, QSFMethodInvokeSpecial methodSpecial); + + @Override + public void afterPropertiesSet() throws Exception { + // After the bean is initialized, add the current bean to the post-processor list. + synchronized (qsfConsumerPostProcessorList) { + qsfConsumerPostProcessorList.add(this); + } + } + + public static List getQsfConsumerPostProcessorList() { + return qsfConsumerPostProcessorList; + } +} \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/postprocessor/QSFProviderPostProcessor.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/postprocessor/QSFProviderPostProcessor.java new file mode 100644 index 00000000..abd1e7a9 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/postprocessor/QSFProviderPostProcessor.java @@ -0,0 +1,53 @@ +/* + * 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.rocketmq.spring.qsf.postprocessor; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.rocketmq.spring.qsf.model.MethodInvokeInfo; + +import org.springframework.beans.factory.InitializingBean; + +/** + * @desc After the mq listener processes the message, call QSFProviderPostProcessor; you can perform tasks such as notification message processing completion, passing return value, etc. + */ +public abstract class QSFProviderPostProcessor implements InitializingBean { + + protected static List qsfProviderPostProcessorList = new ArrayList<>(); + + /** + * Called after mq processing + * + * @param methodInvokeInfo + * @param returnValue + */ + public abstract void callAfterMessageProcess(MethodInvokeInfo methodInvokeInfo, Object returnValue); + + @Override + public void afterPropertiesSet() throws Exception { + // After the bean is initialized, add the current bean to the post-processor list. + synchronized (qsfProviderPostProcessorList) { + qsfProviderPostProcessorList.add(this); + } + } + + public static List getQsfProviderPostProcessorList() { + return qsfProviderPostProcessorList; + } +} \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/preprocessor/QSFConsumerPreProcessor.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/preprocessor/QSFConsumerPreProcessor.java new file mode 100644 index 00000000..c431e8cf --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/preprocessor/QSFConsumerPreProcessor.java @@ -0,0 +1,56 @@ +/* + * 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.rocketmq.spring.qsf.preprocessor; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.rocketmq.spring.qsf.annotation.msgproducer.QSFServiceConsumer; +import org.apache.rocketmq.spring.qsf.annotation.msgproducer.QSFMethodInvokeSpecial; +import org.apache.rocketmq.spring.qsf.model.MethodInvokeInfo; + +import org.springframework.beans.factory.InitializingBean; + +/** + * @desc before the mq producer sends a message, call QSFConsumerPreProcessor + */ +public abstract class QSFConsumerPreProcessor implements InitializingBean { + + protected static List qsfConsumerPreProcessorList = new ArrayList<>(); + + /** + * message sending pre-processing + * @param methodInvokeInfo + * @param msgProducerConfig + * @param methodSpecial + * @return + */ + public abstract MethodInvokeInfo callBeforeMessageSend(MethodInvokeInfo methodInvokeInfo, QSFServiceConsumer msgProducerConfig, QSFMethodInvokeSpecial methodSpecial); + + @Override + public void afterPropertiesSet() throws Exception { + // After the bean is initialized, add the current bean to the preprocessor list. + synchronized (qsfConsumerPreProcessorList) { + qsfConsumerPreProcessorList.add(this); + } + } + + public static List getQsfConsumerPreProcessorList() { + return qsfConsumerPreProcessorList; + } +} \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/preprocessor/QSFProviderPreProcessor.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/preprocessor/QSFProviderPreProcessor.java new file mode 100644 index 00000000..d582cbab --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/preprocessor/QSFProviderPreProcessor.java @@ -0,0 +1,53 @@ +/* + * 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.rocketmq.spring.qsf.preprocessor; + +import java.util.ArrayList; +import java.util.List; + +import org.apache.rocketmq.spring.qsf.model.MethodInvokeInfo; + +import org.springframework.beans.factory.InitializingBean; + +/** + * @desc Before the mq listener processes the message, call QSFProviderPreProcessor + */ +public abstract class QSFProviderPreProcessor implements InitializingBean { + + protected static List qsfProviderPreProcessorList = new ArrayList<>(); + + /** + * called before mq processing, and pass the return value + * @param methodInvokeInfo + * @return true:continue processing; false:break down processing + */ + public abstract boolean callBeforeMessageProcess(MethodInvokeInfo methodInvokeInfo); + + @Override + public void afterPropertiesSet() throws Exception { + // After the bean is initialized, add the current bean to the preprocessor list + synchronized (qsfProviderPreProcessorList) { + qsfProviderPreProcessorList.add(this); + } + } + + public static List getQsfProviderPreProcessorList() { + return qsfProviderPreProcessorList; + } + +} \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/provider/QSFMsgListener.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/provider/QSFMsgListener.java new file mode 100644 index 00000000..f9ad7fa7 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/provider/QSFMsgListener.java @@ -0,0 +1,21 @@ +/* + * 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.rocketmq.spring.qsf.provider; + +public interface QSFMsgListener { +} diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/provider/QSFProviderBeanPostProcessor.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/provider/QSFProviderBeanPostProcessor.java new file mode 100644 index 00000000..2492e539 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/provider/QSFProviderBeanPostProcessor.java @@ -0,0 +1,122 @@ +/* + * 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.rocketmq.spring.qsf.provider; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.rocketmq.spring.qsf.annotation.msgconsumer.QSFServiceProvider; +import org.apache.rocketmq.spring.qsf.beans.ApplicationContextHelper; +import org.apache.rocketmq.client.consumer.listener.MessageListener; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.stereotype.Component; + +/** + * @desc QSFProvider bean post processing: bind the msg listener, parse the MethodInvokeInfo object in the msg body, and call the service method by reflection. + */ + +@Component +public class QSFProviderBeanPostProcessor implements BeanPostProcessor, ApplicationContextAware { + @Autowired + @Qualifier("namesrvAddr") + private String namesrvAddr; + + private Map msgListenerHolder = new ConcurrentHashMap<>(96); + + private ApplicationContext applicationContext; + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + return bean; + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (ApplicationContextHelper.getApplicationContext() == null) { + ApplicationContextHelper.setApplicationContext(applicationContext); + } + + Class clazz = bean.getClass(); + QSFServiceProvider msgConsumerCfg = AnnotatedElementUtils.findMergedAnnotation(clazz, QSFServiceProvider.class); + if (msgConsumerCfg == null) { + return bean; + } + + if (msgConsumerCfg.topic() == null || msgConsumerCfg.topic().trim().length() == 0) { + throw new BeanCreationException("MsgConsumer/QSFProvider topic should not be empty " + bean); + } + + MessageListener messageListener; + if (msgConsumerCfg.messageListenerBeanClass() == QSFServiceProvider.DEFAULT_MESSAGE_LISTENER_CLASS) { + messageListener = (MessageListener)bean; + } else { + messageListener = ApplicationContextHelper.getBean(msgConsumerCfg.messageListenerBeanClass()); + } + // If no MessageListener bean found or bean is not MessageListenerConcurrently/MessageListenerOrderly, BeanCreationException thrown + if (messageListener == null + || (!(messageListener instanceof MessageListenerConcurrently) && !(messageListener instanceof MessageListenerOrderly))) { + throw new BeanCreationException("MsgConsumer/QSFProvider messageListenerBeanClass should be a bean implement MessageListenerConcurrently or MessageListenerOrderly" + bean); + } + + if (msgConsumerCfg.consumerId() == null || msgConsumerCfg.consumerId().trim().length() == 0) { + throw new BeanCreationException("MsgConsumer/QSFProvider consumerId should not be empty " + bean); + } + + if (msgConsumerCfg.minConsumeThreads() < 1 || msgConsumerCfg.minConsumeThreads() > 256 || msgConsumerCfg.minConsumeThreads() > msgConsumerCfg.maxConsumeThreads()) { + throw new BeanCreationException("MsgConsumer/QSFProvider minConsumeThreads should between [1,256] and less than maxConsumeThreads " + bean); + } + + if (msgConsumerCfg.queueType() == null) { + throw new BeanCreationException("QSFProvider queueType should not be empty " + bean); + } + + // register queue consumer + String listenerKey = genListenerKey(msgConsumerCfg); + QSFMsgListener msgListener = msgListenerHolder.get(listenerKey); + if (msgListener == null) { + switch (msgConsumerCfg.queueType()) { + case ROCKET_MQ: + default: + msgListener = new QSFRocketmqMsgListener(msgConsumerCfg, messageListener, namesrvAddr); + } + + msgListenerHolder.put(listenerKey, msgListener); + } + + return bean; + } + + private String genListenerKey(QSFServiceProvider anno) { + return anno.consumerId() + "@" + anno.topic(); + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } +} \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/provider/QSFRocketmqMsgListener.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/provider/QSFRocketmqMsgListener.java new file mode 100644 index 00000000..409e16e8 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/provider/QSFRocketmqMsgListener.java @@ -0,0 +1,73 @@ +/* + * 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.rocketmq.spring.qsf.provider; + +import org.apache.rocketmq.spring.qsf.annotation.msgconsumer.QSFServiceProvider; +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.MessageSelector; +import org.apache.rocketmq.client.consumer.listener.MessageListener; + +/** + * @desc qsf Rocketmq message listener + **/ + +@Slf4j +public class QSFRocketmqMsgListener implements QSFMsgListener { + private DefaultMQPushConsumer pushConsumer; + + public QSFRocketmqMsgListener(QSFServiceProvider msgConsumerConfig, MessageListener messageListener, String namesrvAddr) { + switch (msgConsumerConfig.consumeModel()) { + case push: + default: + initPushConsumer(msgConsumerConfig, messageListener, namesrvAddr); + break; + } + + log.info(" QSFRocketmqMsgListener inited for msgConsumerConfig:{}", msgConsumerConfig); + } + + private void initPushConsumer(QSFServiceProvider msgConsumerConfig, MessageListener messageListener, String namesrvAddr) { + try { + pushConsumer = new DefaultMQPushConsumer(msgConsumerConfig.consumerId()); + pushConsumer.setNamesrvAddr(namesrvAddr); + pushConsumer.setConsumerGroup(msgConsumerConfig.consumerId()); + pushConsumer.setConsumeThreadMin(msgConsumerConfig.minConsumeThreads()); + pushConsumer.setConsumeThreadMax(msgConsumerConfig.maxConsumeThreads()); + if (msgConsumerConfig.selectorSql() != null && msgConsumerConfig.selectorSql().trim().length() > 0) { + pushConsumer.subscribe(msgConsumerConfig.topic(), + MessageSelector.bySql(msgConsumerConfig.selectorSql().trim())); + } else if (msgConsumerConfig.tags() != null && msgConsumerConfig.tags().trim().length() > 0) { + pushConsumer.subscribe(msgConsumerConfig.topic(), msgConsumerConfig.tags().trim()); + } else { + pushConsumer.subscribe(msgConsumerConfig.topic(), "*"); + } + + pushConsumer.setMessageModel(msgConsumerConfig.messageModel()); + + pushConsumer.setMessageListener(messageListener); + + pushConsumer.start(); + log.info(" pushConsumer started msgConsumerConfig:{}", msgConsumerConfig); + + } catch (Throwable e) { + throw new RuntimeException("init push consumer fail for msgConsumerConfig:" + msgConsumerConfig, e); + } + } + +} \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/util/ClearableAfterApplicationStarted.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/util/ClearableAfterApplicationStarted.java new file mode 100644 index 00000000..589cf1d6 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/util/ClearableAfterApplicationStarted.java @@ -0,0 +1,22 @@ +/* + * 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.rocketmq.spring.qsf.util; + +public interface ClearableAfterApplicationStarted { + void clearAfterApplicationStart() throws Exception; +} diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/util/IPUtils.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/util/IPUtils.java new file mode 100644 index 00000000..277334de --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/util/IPUtils.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.rocketmq.spring.qsf.util; + +import lombok.extern.slf4j.Slf4j; + +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.util.Enumeration; + +/** + * @desc + **/ + +@Slf4j +public class IPUtils { + private static String staticIp; + + private static String staticHost; + + static { + InetAddress localHostAddress = getLocalHostAddress(); + + // ip init + staticIp = localHostAddress.getHostAddress(); + log.info(" local ip:{}", staticIp); + + // host init + staticHost = localHostAddress.getHostName(); + log.info(" local host:{}", staticHost); + } + + public static String getLocalHostName() { + return staticHost; + } + + public static String getLocalIp() { + return staticIp; + } + + public static InetAddress getLocalHostAddress() { + try { + InetAddress candidateAddr = null; + + Enumeration networkInterfaces = NetworkInterface.getNetworkInterfaces(); + while (networkInterfaces.hasMoreElements()) { + NetworkInterface networkInterface = networkInterfaces.nextElement(); + Enumeration inetAddrs = networkInterface.getInetAddresses(); + while (inetAddrs.hasMoreElements()) { + InetAddress inetAddr = inetAddrs.nextElement(); + if (inetAddr.isLoopbackAddress()) { + continue; + } + + if (inetAddr.isSiteLocalAddress()) { + return inetAddr; + } + + if (candidateAddr == null) { + candidateAddr = inetAddr; + } + } + } + + return candidateAddr == null ? InetAddress.getLocalHost() : candidateAddr; + } catch (Throwable e) { + log.warn(" getLocalHostAddress fail", e); + } + return null; + } +} \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/util/InvokeUtils.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/util/InvokeUtils.java new file mode 100644 index 00000000..1a008ceb --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/util/InvokeUtils.java @@ -0,0 +1,117 @@ +/* + * 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.rocketmq.spring.qsf.util; + +import lombok.extern.slf4j.Slf4j; + +import java.lang.reflect.InvocationTargetException; +import java.util.Arrays; + +/** + * @desc supoort invoking with retry + **/ + +@Slf4j +public class InvokeUtils { + private static final int DEFAULT_MAX_INVOKE_TIMES = 5; + + private static final long DEFAULT_RETRY_TIMEOUT = 2000; + + private static final long DEFAULT_RETRY_INTERVAL = 10; + + /** + * Method invocation with retry, used for key link retry to prevent timeout, current limit, etc. + * + * @param handler + * @param methodName + * @param paramClasses parameter objects type + * @param params parameter objects + * @return + * @throws InvocationTargetException + * @throws IllegalAccessException + */ + public static Object invokeWithRetry(final Object handler, final String methodName, final Class[] paramClasses, final Object[] params) { + return invokeWithRetry(handler, methodName, paramClasses, params, null, DEFAULT_MAX_INVOKE_TIMES, DEFAULT_RETRY_TIMEOUT, DEFAULT_RETRY_INTERVAL); + } + + /** + * Method invocation with retry, used for key link retry to prevent timeout, current limit, etc. + * + * @param handler + * @param methodName + * @param paramClasses parameter objects type + * @param params parameter objects + * @param successMethod The method name for judging the success of the call, such as "isSuccess"; when it is empty, if no exception occurs, the call is considered successful. + * @return + * @throws InvocationTargetException + * @throws IllegalAccessException + */ + public static Object invokeWithRetry(final Object handler, final String methodName, final Class[] paramClasses, final Object[] params, final String successMethod) { + return invokeWithRetry(handler, methodName, paramClasses, params, successMethod, DEFAULT_MAX_INVOKE_TIMES, DEFAULT_RETRY_TIMEOUT, DEFAULT_RETRY_INTERVAL); + } + + /** + * Method invocation with retry, used for key link retry to prevent timeout, current limit, etc. + * + * @param handler + * @param methodName + * @param paramClasses parameter objects type + * @param params parameter objects + * @param successMethod The method name for judging the success of the call, such as "isSuccess"; when it is empty, if no exception occurs, the call is considered successful. + * @param maxInvokeTimes Maximum number of attempts to call + * @return + * @throws InvocationTargetException + * @throws IllegalAccessException + */ + public static Object invokeWithRetry(final Object handler, final String methodName, final Class[] paramClasses, final Object[] params, final String successMethod, final int maxInvokeTimes, final long retryTimeout, final long retryInterval) { + int tryCnt = 0; + boolean isSuc = false; + Object returnObj = null; + long now = System.currentTimeMillis(); + Throwable realEx = null; + while (!isSuc && tryCnt < maxInvokeTimes && (System.currentTimeMillis() - now < retryTimeout)) { + tryCnt++; + try { + returnObj = ReflectionMethodInvoker.invoke(handler, methodName, paramClasses, params); +// log.info(" invokeWithRetry {}#{} params:{} resp:{}", handler.toString(), methodName, Arrays.toString(params), returnObj); + if (successMethod != null && successMethod.length() > 0) { + isSuc = (boolean)ReflectionMethodInvoker.invoke(returnObj, successMethod); + } else { + isSuc = true; + } + } catch (Throwable ex) { + log.warn(" invokeWithRetry fail handler:{} methodName:{} paramClasses:{} params:{} successMethod:{} tryCnt:{} returnObj:{}", handler, methodName, + Arrays.asList(paramClasses), Arrays.asList(params), successMethod, tryCnt, returnObj, ex); + realEx = ex; + } + if (!isSuc) { + try { + Thread.sleep(retryInterval); + } catch (Throwable e) { + log.warn(" invokeWithRetry sleep fail", e); + } + } + } + + if (!isSuc) { + throw new RuntimeException("invokeWithRetry fail, handler:" + handler + " methodName:" + methodName + " params:" + Arrays.asList(params), realEx); + } + + return returnObj; + } +} \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/util/KeyUtils.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/util/KeyUtils.java new file mode 100644 index 00000000..d38d3fdf --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/util/KeyUtils.java @@ -0,0 +1,28 @@ +/* + * 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.rocketmq.spring.qsf.util; + +/** + * @desc + **/ + +public class KeyUtils { + public static String callKey() { + return IPUtils.getLocalIp() + ":" + System.currentTimeMillis() + ":" + Thread.currentThread().getId(); + } +} \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/util/QSFStringUtils.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/util/QSFStringUtils.java new file mode 100644 index 00000000..470e1378 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/util/QSFStringUtils.java @@ -0,0 +1,32 @@ +/* + * 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.rocketmq.spring.qsf.util; + +/** + * @desc + **/ + +public class QSFStringUtils { + public static boolean isTrimEmpty(String checkString) { + return checkString == null || checkString.trim().length() == 0; + } + + public static boolean isNotTrimEmpty(String checkString) { + return !isTrimEmpty(checkString); + } +} diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/util/ReflectionMethodInvoker.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/util/ReflectionMethodInvoker.java new file mode 100644 index 00000000..3a2ac71a --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/util/ReflectionMethodInvoker.java @@ -0,0 +1,149 @@ +/* + * 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.rocketmq.spring.qsf.util; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import lombok.extern.slf4j.Slf4j; + +/** + * @desc Use reflection to call a method based on method signature, method handler, parameter. + **/ + +@Slf4j +public class ReflectionMethodInvoker { + private static Map methodCacheMap = new ConcurrentHashMap(96); + + public static Object invoke(final Object handler, final String methodName) { + return invoke(handler, methodName, new Class[0], new Object[0]); + } + + + /** + * Use reflection to call a method. + * Note: The actual parameter of the method may be any parent class of params + * primitive/boxed paramClazz parameters point to different methods. + * + * @param handler + * @param methodName + * @param params + * @return + * @throws InvocationTargetException + * @throws IllegalAccessException + */ + public static Object invoke(final Object handler, final String methodName, Object... params) { + Class[] paramClasses; + if (params == null) { + paramClasses = new Class[0]; + } else { + paramClasses = new Class[params.length]; + for (int i = 0; i < params.length; i++) { + paramClasses[i] = params[i].getClass(); + } + } + return invoke(handler, methodName, paramClasses, params); + } + + + /** + * Use reflection to call a method. + * Note: The actual parameter of the method may be any parent class of params + * primitive/boxed paramClazz parameters point to different methods. + * + * @param handler + * @param methodName + * @param paramClasses + * @param params + * @return + * @throws InvocationTargetException + * @throws IllegalAccessException + */ + public static Object invoke(final Object handler, final String methodName, final Class[] paramClasses, final Object[] params) { + Method method = getMethod(handler.getClass(), methodName, paramClasses); + + //invoke private method + method.setAccessible(true); + + Object returnObj = null; + try { + if (paramClasses == null || paramClasses.length == 0) { + returnObj = method.invoke(handler); + } else { + returnObj = method.invoke(handler, params); + } + } catch (Throwable th) { + String errorMsg = String.format("ReflectionMethodInvoker invoke fail, handler=%s methodName=%s paramClasses=%s params=%s", handler, methodName, + paramClasses, params); + log.error(" " + errorMsg, th); + throw new RuntimeException(errorMsg, th); + } + + log.debug(" ReflectionMethodInvoker invoke OK, handler={} methodName={} paramClasses={} params={} return={}", handler, methodName, + paramClasses, params, returnObj); + + return returnObj; + } + + /** + * Cascading lookup methods on the inheritance tree + * @param clazz + * @param methodName + * @param paramClazz + * @return + */ + public static Method getMethod(Class clazz, String methodName, final Class... paramClazz) { + StringBuilder keyBuilder = new StringBuilder(); + keyBuilder.append(clazz.getName()).append('_').append(methodName); + Class[] paramClasses = paramClazz; + if (paramClasses != null) { + for (Class paramCls : paramClasses) { + keyBuilder.append('_').append(paramCls.getName()); + } + } else { + paramClasses = new Class[0]; + } + + String key = keyBuilder.toString(); + Method method = methodCacheMap.get(key); + if (method == null) { + try { + method = clazz.getDeclaredMethod(methodName, paramClasses); + } catch (NoSuchMethodException e) { + try { + method = clazz.getMethod(methodName, paramClasses); + } catch (NoSuchMethodException ex) { + if (clazz.getSuperclass() == null) { + return method; + } else { + method = getMethod(clazz.getSuperclass(), methodName, paramClasses); + } + } + } + + if (method != null) { + methodCacheMap.put(key, method); + } + } + + return method; + } + +} \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/util/ReflectionUtils.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/util/ReflectionUtils.java new file mode 100644 index 00000000..2d4d1238 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-core/src/main/java/org/apache/rocketmq/spring/qsf/util/ReflectionUtils.java @@ -0,0 +1,73 @@ +/* + * 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.rocketmq.spring.qsf.util; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * @desc + **/ + +public class ReflectionUtils { + + private static final int INIT_LIST_SIZE = 96; + + public static List getAllFields(Class clazz) { + List allFields = new ArrayList<>(INIT_LIST_SIZE); + + return getAllFields(clazz, allFields); + } + + public static String getMethodSignature(Method method) { + StringBuilder signature = new StringBuilder(); + signature.append(method.getName()); + Class[] paramClasses = method.getParameterTypes(); + if (paramClasses != null) { + for (Class paramCls : paramClasses) { + signature.append('_').append(paramCls.getName()); + } + } + + return signature.toString(); + } + + private static List getAllFields(Class clazz, List allFields) { + if (clazz == null) { + return Collections.EMPTY_LIST; + } + + Field[] fields = clazz.getDeclaredFields(); + allFields.addAll(Arrays.asList(fields)); + + getAllFields(clazz.getSuperclass(), allFields); + + Class[] interfaces = clazz.getInterfaces(); + if (interfaces != null && interfaces.length > 0) { + for (int i = 0; i < interfaces.length; i++) { + getAllFields(interfaces[i], allFields); + } + } + + return allFields; + } +} \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/pom.xml b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/pom.xml new file mode 100644 index 00000000..b8e9fa92 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/pom.xml @@ -0,0 +1,36 @@ + + + + + org.apache.rocketmq + rocketmq-spring-qsf + 1.0.0-SNAPSHOT + + 4.0.0 + + rocketmq-spring-qsf-demo + pom + + + rocketmq-spring-qsf-demo-core + rocketmq-spring-qsf-demo-idempotency + rocketmq-spring-qsf-demo-callback-dubbo + + + diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-callback-dubbo/pom.xml b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-callback-dubbo/pom.xml new file mode 100644 index 00000000..021db983 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-callback-dubbo/pom.xml @@ -0,0 +1,92 @@ + + + + + org.apache.rocketmq + rocketmq-spring-qsf-demo + 1.0.0-SNAPSHOT + + 4.0.0 + + rocketmq-spring-qsf-demo-callback-dubbo + jar + + + + + org.apache.rocketmq + rocketmq-spring-qsf-callback-dubbo + + + + + org.springframework.boot + spring-boot-actuator-autoconfigure + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + + + org.apache.curator + curator-framework + + + org.apache.curator + curator-recipes + + + + + org.projectlombok + lombok + provided + + + + org.springframework.boot + spring-boot-test + test + + + org.springframework + spring-test + test + + + junit + junit + test + + + org.springframework.boot + spring-boot-starter + + + org.slf4j + jcl-over-slf4j + + + + diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-callback-dubbo/src/main/java/org/apache/rocketmq/spring/qsf/demo/QSFCallbackDubboDemoApplication.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-callback-dubbo/src/main/java/org/apache/rocketmq/spring/qsf/demo/QSFCallbackDubboDemoApplication.java new file mode 100644 index 00000000..17ec3946 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-callback-dubbo/src/main/java/org/apache/rocketmq/spring/qsf/demo/QSFCallbackDubboDemoApplication.java @@ -0,0 +1,30 @@ +/* + * 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.rocketmq.spring.qsf.demo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication(scanBasePackages = {"org.apache.rocketmq.spring.qsf"}) +public class QSFCallbackDubboDemoApplication { + + public static void main(String[] args) { + SpringApplication.run(QSFCallbackDubboDemoApplication.class, args); + } + +} \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-callback-dubbo/src/main/java/org/apache/rocketmq/spring/qsf/demo/controller/QSFCallbackDubboDemoController.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-callback-dubbo/src/main/java/org/apache/rocketmq/spring/qsf/demo/controller/QSFCallbackDubboDemoController.java new file mode 100644 index 00000000..96dabc41 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-callback-dubbo/src/main/java/org/apache/rocketmq/spring/qsf/demo/controller/QSFCallbackDubboDemoController.java @@ -0,0 +1,67 @@ +/* + * 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.rocketmq.spring.qsf.demo.controller; + +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.spring.qsf.annotation.msgproducer.QSFMethodInvokeSpecial; +import org.apache.rocketmq.spring.qsf.annotation.msgproducer.QSFMsgProducer; +import org.apache.rocketmq.spring.qsf.demo.qsfprovider.QSFCallbackDubboDemoService; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.Map; + +/** + * @desc + **/ + +@RestController +@RequestMapping("/demo/qsf") +@Slf4j +public class QSFCallbackDubboDemoController { + + @QSFMsgProducer(topic = "rocketmq_topic_qsf_demo_callback_dubbo", methodSpecials = { + @QSFMethodInvokeSpecial(methodName = "testQSFCallback", syncCall = true) + }) + private QSFCallbackDubboDemoService qsfCallbackDubboDemoService; + + @GetMapping("/basic") + public Map qsfBasic(HttpServletRequest request) { + Map resultMap = new HashMap<>(); + + // test QSF basic usage + qsfCallbackDubboDemoService.testQSFBasic(100L, "hello world"); + + return resultMap; + } + + @GetMapping("/callback") + public Map qsfCallback(HttpServletRequest request) { + Map resultMap = new HashMap<>(); + + // test QSF callback + String syncResult = qsfCallbackDubboDemoService.testQSFCallback(100L, "hello world"); + log.info("syncEcho result:{}", syncResult); + resultMap.put("syncResult", syncResult); + + return resultMap; + } +} \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-callback-dubbo/src/main/java/org/apache/rocketmq/spring/qsf/demo/qsfprovider/QSFCallbackDubboDemoService.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-callback-dubbo/src/main/java/org/apache/rocketmq/spring/qsf/demo/qsfprovider/QSFCallbackDubboDemoService.java new file mode 100644 index 00000000..34af0119 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-callback-dubbo/src/main/java/org/apache/rocketmq/spring/qsf/demo/qsfprovider/QSFCallbackDubboDemoService.java @@ -0,0 +1,38 @@ +/* + * 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.rocketmq.spring.qsf.demo.qsfprovider; + +public interface QSFCallbackDubboDemoService { + + /** + * + * @param id + * @param name + * @return + */ + void testQSFBasic(long id, String name); + + /** + * test + * @param id + * @param name + * @return + */ + String testQSFCallback(long id, String name); + +} diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-callback-dubbo/src/main/java/org/apache/rocketmq/spring/qsf/demo/qsfprovider/QSFCallbackDubboDemoServiceImpl.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-callback-dubbo/src/main/java/org/apache/rocketmq/spring/qsf/demo/qsfprovider/QSFCallbackDubboDemoServiceImpl.java new file mode 100644 index 00000000..fd94985e --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-callback-dubbo/src/main/java/org/apache/rocketmq/spring/qsf/demo/qsfprovider/QSFCallbackDubboDemoServiceImpl.java @@ -0,0 +1,43 @@ +/* + * 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.rocketmq.spring.qsf.demo.qsfprovider; + +import org.apache.rocketmq.spring.qsf.annotation.msgconsumer.QSFMsgConsumer; + +import lombok.extern.slf4j.Slf4j; + +/** + * @desc + **/ + +@QSFMsgConsumer(consumerId = "rocketmq_consumer_qsf_demo_callback_dubbo", topic = "rocketmq_topic_qsf_demo_callback_dubbo") +@Slf4j +public class QSFCallbackDubboDemoServiceImpl implements QSFCallbackDubboDemoService { + + @Override + public void testQSFBasic(long id, String name) { + log.info("in service call: testQSFBasic id:{} name:{}", id, name); + } + + @Override + public String testQSFCallback(long id, String name) { + log.info("in service call: testQSFCallback id:{} name:{}", id, name); + + return "syncEcho:" + name; + } +} \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-callback-dubbo/src/main/resources/application.properties b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-callback-dubbo/src/main/resources/application.properties new file mode 100644 index 00000000..3e956c14 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-callback-dubbo/src/main/resources/application.properties @@ -0,0 +1,24 @@ +# 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. + +management.security.enabled = false + +management.endpoints.web.exposure.include=* + +# http port +server.port=7003 + +# endpoint config +management.server.port=6003 \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-callback-dubbo/src/main/resources/application.yml b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-callback-dubbo/src/main/resources/application.yml new file mode 100644 index 00000000..dc945859 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-callback-dubbo/src/main/resources/application.yml @@ -0,0 +1,40 @@ +# 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. + +qsf: + project: + name: qsfdemo + rocketmq: + name-server: 127.0.0.1:9876 + +dubbo: + registry: + protocol: zookeeper + address: 127.0.0.1:2181 + id: my-registry + protocol: + # use dubbo default port 20880 to callback + port: 20880 + name: dubbo + status: server + id: dubbo + application: + name: demo-provider + id: demo-provider + qosEnable: true + qosPort: 22223 + scan: + basePackages: org.apache.rocketmq.spring.qsf + diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-callback-dubbo/src/main/resources/logback-spring.xml b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-callback-dubbo/src/main/resources/logback-spring.xml new file mode 100644 index 00000000..259c1672 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-callback-dubbo/src/main/resources/logback-spring.xml @@ -0,0 +1,37 @@ + + + + + + + + + %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-core/pom.xml b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-core/pom.xml new file mode 100644 index 00000000..71a120a7 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-core/pom.xml @@ -0,0 +1,75 @@ + + + + + org.apache.rocketmq + rocketmq-spring-qsf-demo + 1.0.0-SNAPSHOT + + 4.0.0 + + rocketmq-spring-qsf-demo-core + jar + + + + + org.apache.rocketmq + rocketmq-spring-qsf-core + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.projectlombok + lombok + provided + + + + org.springframework.boot + spring-boot-test + test + + + org.springframework + spring-test + test + + + junit + junit + test + + + org.springframework.boot + spring-boot-starter + + + org.slf4j + jcl-over-slf4j + + + + diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-core/src/main/java/org/apache/rocketmq/spring/qsf/demo/QSFCoreDemoApplication.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-core/src/main/java/org/apache/rocketmq/spring/qsf/demo/QSFCoreDemoApplication.java new file mode 100644 index 00000000..eb7f3555 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-core/src/main/java/org/apache/rocketmq/spring/qsf/demo/QSFCoreDemoApplication.java @@ -0,0 +1,30 @@ +/* + * 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.rocketmq.spring.qsf.demo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication(scanBasePackages = {"org.apache.rocketmq.spring.qsf"}) +public class QSFCoreDemoApplication { + + public static void main(String[] args) { + SpringApplication.run(QSFCoreDemoApplication.class, args); + } + +} \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-core/src/main/java/org/apache/rocketmq/spring/qsf/demo/controller/QSFCoreDemoController.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-core/src/main/java/org/apache/rocketmq/spring/qsf/demo/controller/QSFCoreDemoController.java new file mode 100644 index 00000000..1fb02271 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-core/src/main/java/org/apache/rocketmq/spring/qsf/demo/controller/QSFCoreDemoController.java @@ -0,0 +1,52 @@ +/* + * 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.rocketmq.spring.qsf.demo.controller; + +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.spring.qsf.annotation.msgproducer.QSFMsgProducer; +import org.apache.rocketmq.spring.qsf.demo.qsfprovider.QSFCoreDemoService; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.Map; + +/** + * @desc + **/ + +@RestController +@RequestMapping("/demo/qsf") +@Slf4j +public class QSFCoreDemoController { + + @QSFMsgProducer(topic = "rocketmq_topic_qsf_demo_core") + private QSFCoreDemoService qsfCoreDemoService; + + @GetMapping("/basic") + public Map qsfBasic(HttpServletRequest request) { + Map resultMap = new HashMap<>(); + + // test QSF basic usage + qsfCoreDemoService.testQSFBasic(100L, "hello world"); + + return resultMap; + } +} \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-core/src/main/java/org/apache/rocketmq/spring/qsf/demo/qsfprovider/QSFCoreDemoService.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-core/src/main/java/org/apache/rocketmq/spring/qsf/demo/qsfprovider/QSFCoreDemoService.java new file mode 100644 index 00000000..845db898 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-core/src/main/java/org/apache/rocketmq/spring/qsf/demo/qsfprovider/QSFCoreDemoService.java @@ -0,0 +1,29 @@ +/* + * 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.rocketmq.spring.qsf.demo.qsfprovider; + +public interface QSFCoreDemoService { + + /** + * + * @param id + * @param name + * @return + */ + void testQSFBasic(long id, String name); +} \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-core/src/main/java/org/apache/rocketmq/spring/qsf/demo/qsfprovider/QSFCoreDemoServiceImpl.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-core/src/main/java/org/apache/rocketmq/spring/qsf/demo/qsfprovider/QSFCoreDemoServiceImpl.java new file mode 100644 index 00000000..2e48a17e --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-core/src/main/java/org/apache/rocketmq/spring/qsf/demo/qsfprovider/QSFCoreDemoServiceImpl.java @@ -0,0 +1,36 @@ +/* + * 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.rocketmq.spring.qsf.demo.qsfprovider; + +import org.apache.rocketmq.spring.qsf.annotation.msgconsumer.QSFMsgConsumer; + +import lombok.extern.slf4j.Slf4j; + +/** + * @desc + **/ + +@QSFMsgConsumer(consumerId = "rocketmq_consumer_qsf_demo_core", topic = "rocketmq_topic_qsf_demo_core") +@Slf4j +public class QSFCoreDemoServiceImpl implements QSFCoreDemoService { + + @Override + public void testQSFBasic(long id, String name) { + log.info("in service call: testQSFBasic id:{} name:{}", id, name); + } +} \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-core/src/main/resources/application.properties b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-core/src/main/resources/application.properties new file mode 100644 index 00000000..ad14b1cc --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-core/src/main/resources/application.properties @@ -0,0 +1,24 @@ +# 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. + +management.security.enabled = false + +management.endpoints.web.exposure.include=* + +# http port +server.port=7001 + +# endpoint config +management.server.port=6001 \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-core/src/main/resources/application.yml b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-core/src/main/resources/application.yml new file mode 100644 index 00000000..74ea08ab --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-core/src/main/resources/application.yml @@ -0,0 +1,20 @@ +# 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. + +qsf: + project: + name: qsfdemo + rocketmq: + name-server: 127.0.0.1:9876 diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-core/src/main/resources/logback-spring.xml b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-core/src/main/resources/logback-spring.xml new file mode 100644 index 00000000..259c1672 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-core/src/main/resources/logback-spring.xml @@ -0,0 +1,37 @@ + + + + + + + + + %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-idempotency/pom.xml b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-idempotency/pom.xml new file mode 100644 index 00000000..1f92c195 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-idempotency/pom.xml @@ -0,0 +1,75 @@ + + + + + org.apache.rocketmq + rocketmq-spring-qsf-demo + 1.0.0-SNAPSHOT + + 4.0.0 + + rocketmq-spring-qsf-demo-idempotency + jar + + + + + org.apache.rocketmq + rocketmq-spring-qsf-idempotency + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.projectlombok + lombok + provided + + + + org.springframework.boot + spring-boot-test + test + + + org.springframework + spring-test + test + + + junit + junit + test + + + org.springframework.boot + spring-boot-starter + + + org.slf4j + jcl-over-slf4j + + + + diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-idempotency/src/main/java/org/apache/rocketmq/spring/qsf/demo/QSFIdemptencyDemoApplication.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-idempotency/src/main/java/org/apache/rocketmq/spring/qsf/demo/QSFIdemptencyDemoApplication.java new file mode 100644 index 00000000..7c6ce909 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-idempotency/src/main/java/org/apache/rocketmq/spring/qsf/demo/QSFIdemptencyDemoApplication.java @@ -0,0 +1,30 @@ +/* + * 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.rocketmq.spring.qsf.demo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication(scanBasePackages = {"org.apache.rocketmq.spring.qsf"}) +public class QSFIdemptencyDemoApplication { + + public static void main(String[] args) { + SpringApplication.run(QSFIdemptencyDemoApplication.class, args); + } + +} \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-idempotency/src/main/java/org/apache/rocketmq/spring/qsf/demo/controller/QSFIdemptencyDemoController.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-idempotency/src/main/java/org/apache/rocketmq/spring/qsf/demo/controller/QSFIdemptencyDemoController.java new file mode 100644 index 00000000..0093d679 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-idempotency/src/main/java/org/apache/rocketmq/spring/qsf/demo/controller/QSFIdemptencyDemoController.java @@ -0,0 +1,63 @@ +/* + * 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.rocketmq.spring.qsf.demo.controller; + +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.spring.qsf.annotation.msgproducer.QSFMsgProducer; +import org.apache.rocketmq.spring.qsf.demo.qsfprovider.QSFIdemptencyDemoService; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.Map; + +/** + * @desc + **/ + +@RestController +@RequestMapping("/demo/qsf") +@Slf4j +public class QSFIdemptencyDemoController { + + @QSFMsgProducer(topic = "rocketmq_topic_qsf_demo_idem") + private QSFIdemptencyDemoService qsfIdemptencyDemoService; + + @GetMapping("/basic") + public Map qsfBasic(HttpServletRequest request) { + Map resultMap = new HashMap<>(); + + // test QSF basic usage + qsfIdemptencyDemoService.testQSFBasic(100L, "hello world"); + + return resultMap; + } + + @GetMapping("/idem") + public Map qsfIdempotency(HttpServletRequest request) { + Map resultMap = new HashMap<>(); + + // test QSF idempotency, method with same parameters will be invoked exactly once + qsfIdemptencyDemoService.testQSFIdempotency(100L, "hello world"); + qsfIdemptencyDemoService.testQSFIdempotency(100L, "hello world"); + + return resultMap; + } +} \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-idempotency/src/main/java/org/apache/rocketmq/spring/qsf/demo/qsfprovider/QSFIdemptencyDemoService.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-idempotency/src/main/java/org/apache/rocketmq/spring/qsf/demo/qsfprovider/QSFIdemptencyDemoService.java new file mode 100644 index 00000000..06575e96 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-idempotency/src/main/java/org/apache/rocketmq/spring/qsf/demo/qsfprovider/QSFIdemptencyDemoService.java @@ -0,0 +1,37 @@ +/* + * 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.rocketmq.spring.qsf.demo.qsfprovider; + +public interface QSFIdemptencyDemoService { + + /** + * + * @param id + * @param name + * @return + */ + void testQSFBasic(long id, String name); + + /** + * + * @param id + * @param name + * @return + */ + void testQSFIdempotency(long id, String name); +} diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-idempotency/src/main/java/org/apache/rocketmq/spring/qsf/demo/qsfprovider/QSFIdemptencyDemoServiceImpl.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-idempotency/src/main/java/org/apache/rocketmq/spring/qsf/demo/qsfprovider/QSFIdemptencyDemoServiceImpl.java new file mode 100644 index 00000000..c3408fec --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-idempotency/src/main/java/org/apache/rocketmq/spring/qsf/demo/qsfprovider/QSFIdemptencyDemoServiceImpl.java @@ -0,0 +1,43 @@ +/* + * 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.rocketmq.spring.qsf.demo.qsfprovider; + +import org.apache.rocketmq.spring.qsf.annotation.msgconsumer.QSFMsgConsumer; + +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.spring.qsf.idempotency.QSFIdempotency; + +/** + * @desc + **/ + +@QSFMsgConsumer(consumerId = "rocketmq_consumer_qsf_demo_idem", topic = "rocketmq_topic_qsf_demo_idem") +@Slf4j +public class QSFIdemptencyDemoServiceImpl implements QSFIdemptencyDemoService { + + @Override + public void testQSFBasic(long id, String name) { + log.info("in service call: testQSFBasic id:{} name:{}", id, name); + } + + @Override + @QSFIdempotency(idempotentMethodExecuteTimeout = 60000) + public void testQSFIdempotency(long id, String name) { + log.info("in service call: testQSFIdempotency id:{} name:{}", id, name); + } +} \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-idempotency/src/main/resources/application.properties b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-idempotency/src/main/resources/application.properties new file mode 100644 index 00000000..5ae7480e --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-idempotency/src/main/resources/application.properties @@ -0,0 +1,24 @@ +# 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. + +management.security.enabled = false + +management.endpoints.web.exposure.include=* + +# http port +server.port=7002 + +# endpoint config +management.server.port=6002 \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-idempotency/src/main/resources/application.yml b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-idempotency/src/main/resources/application.yml new file mode 100644 index 00000000..a4dac572 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-idempotency/src/main/resources/application.yml @@ -0,0 +1,38 @@ +# 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. + +qsf: + project: + name: qsfdemo + rocketmq: + name-server: 127.0.0.1:9876 + store: + redis: + database: 0 + # redis host & port , cluster.nodes should exist at least one, if both exist, cluster.nodes takes effect + host: 127.0.0.1 + port: 6379 + timeout: 6000 + password: + jedis: + pool: + max-total: 1000 + max-wait: -1 + max-idle: 10 + min-idle: 5 + cluster: + max-attempts: 5 + # for example : 127.0.0.1:6379,127.0.0.1:6378 + nodes: diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-idempotency/src/main/resources/logback-spring.xml b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-idempotency/src/main/resources/logback-spring.xml new file mode 100644 index 00000000..259c1672 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-demo/rocketmq-spring-qsf-demo-idempotency/src/main/resources/logback-spring.xml @@ -0,0 +1,37 @@ + + + + + + + + + %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-idempotency/pom.xml b/rocketmq-spring-qsf/rocketmq-spring-qsf-idempotency/pom.xml new file mode 100644 index 00000000..c9d8ea04 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-idempotency/pom.xml @@ -0,0 +1,76 @@ + + + + + + rocketmq-spring-qsf + org.apache.rocketmq + 1.0.0-SNAPSHOT + + 4.0.0 + + org.apache.rocketmq + rocketmq-spring-qsf-idempotency + + + + org.apache.rocketmq + rocketmq-spring-qsf-core + + + org.apache.rocketmq + rocketmq-spring-qsf-state-store + + + + org.projectlombok + lombok + provided + + + + + + + org.apache.maven.plugins + maven-resources-plugin + 3.1.0 + + UTF-8 + + + + org.apache.maven.plugins + maven-source-plugin + 3.1.0 + + + attach-sources + verify + + jar-no-fork + + + + + + + + \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-idempotency/src/main/java/org/apache/rocketmq/spring/qsf/idempotency/IdempotencyLockUtils.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-idempotency/src/main/java/org/apache/rocketmq/spring/qsf/idempotency/IdempotencyLockUtils.java new file mode 100644 index 00000000..e18ec67c --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-idempotency/src/main/java/org/apache/rocketmq/spring/qsf/idempotency/IdempotencyLockUtils.java @@ -0,0 +1,34 @@ +/* + * 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.rocketmq.spring.qsf.idempotency; + +import org.apache.rocketmq.spring.qsf.util.IPUtils; + +/** + * @desc + */ +public class IdempotencyLockUtils { + + public static String lockKey(String idempotencyKey) { + return "lock:" + idempotencyKey; + } + + public static String lockValue() { + return IPUtils.getLocalIp() + ":" + Thread.currentThread().getId(); + } +} \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-idempotency/src/main/java/org/apache/rocketmq/spring/qsf/idempotency/IdempotencyParams.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-idempotency/src/main/java/org/apache/rocketmq/spring/qsf/idempotency/IdempotencyParams.java new file mode 100644 index 00000000..49cd4334 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-idempotency/src/main/java/org/apache/rocketmq/spring/qsf/idempotency/IdempotencyParams.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.rocketmq.spring.qsf.idempotency; + +import lombok.Data; +import lombok.ToString; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import lombok.Builder; + +/** + * @desc + */ +@Data +@ToString +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class IdempotencyParams { + /** + * null means no need for idempotency, true means need for idempotency + * use idempotency should import rocketmq-spring-qsf-idempotency + */ + private boolean idempotent; + + /** + * idempotency expiration milliseconds, null means no need for idempotency, 0 or negative means no expiration + */ + private long idempotencyMillisecondsToExpire; + + /** + * idempotent method execute timeout + */ + private long idempotentMethodExecuteTimeout; +} \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-idempotency/src/main/java/org/apache/rocketmq/spring/qsf/idempotency/IdempotencyParamsManager.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-idempotency/src/main/java/org/apache/rocketmq/spring/qsf/idempotency/IdempotencyParamsManager.java new file mode 100644 index 00000000..7209d63b --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-idempotency/src/main/java/org/apache/rocketmq/spring/qsf/idempotency/IdempotencyParamsManager.java @@ -0,0 +1,75 @@ +/* + * 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.rocketmq.spring.qsf.idempotency; + +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.spring.qsf.beans.ApplicationContextHelper; +import org.apache.rocketmq.spring.qsf.model.MethodInvokeInfo; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @desc + */ +@Component +@Slf4j +public class IdempotencyParamsManager { + private Map methodSignatureIdempotencyParamsMap = new ConcurrentHashMap<>(); + + public IdempotencyParams getIdempotencyParams(MethodInvokeInfo methodInvokeInfo) { + String methodSignature = methodInvokeInfo.buildMethodSignature(); + IdempotencyParams idempotencyParams = methodSignatureIdempotencyParamsMap.get(methodSignature); + if (idempotencyParams == null) { + Object serviceBean = ApplicationContextHelper.getBeanByTypeName(methodInvokeInfo.getInvokeBeanType()); + Method method = null; + try { + method = serviceBean.getClass().getMethod(methodInvokeInfo.getMethodName(), methodInvokeInfo.getArgsTypes()); + } catch (NoSuchMethodException e) { + log.error(" getMethod fail:{}", methodInvokeInfo, e); + throw new RuntimeException("getMethod fail:" + methodSignature, e); + } + QSFIdempotency qsfIdempotencyAnno = AnnotationUtils.getAnnotation(method, QSFIdempotency.class); + log.info(" getAnnotation QSFIdempotency for method:{} result:{}", methodSignature, qsfIdempotencyAnno); + if (qsfIdempotencyAnno != null) { + idempotencyParams = IdempotencyParams.builder() + .idempotent(true) + .idempotencyMillisecondsToExpire(qsfIdempotencyAnno.idempotencyMillisecondsToExpire()) + .idempotentMethodExecuteTimeout(qsfIdempotencyAnno.idempotentMethodExecuteTimeout()) + .build(); + } else { + idempotencyParams = IdempotencyParams.builder() + .idempotent(false) + .build(); + } + + methodSignatureIdempotencyParamsMap.put(methodSignature, idempotencyParams); + } + + if (!idempotencyParams.isIdempotent()) { + return null; + } + + return idempotencyParams; + } + + +} \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-idempotency/src/main/java/org/apache/rocketmq/spring/qsf/idempotency/QSFIdempotency.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-idempotency/src/main/java/org/apache/rocketmq/spring/qsf/idempotency/QSFIdempotency.java new file mode 100644 index 00000000..22567ac3 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-idempotency/src/main/java/org/apache/rocketmq/spring/qsf/idempotency/QSFIdempotency.java @@ -0,0 +1,44 @@ +/* + * 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.rocketmq.spring.qsf.idempotency; + +import org.springframework.stereotype.Component; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @desc annotatation for methods should be idempotent + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Component +public @interface QSFIdempotency { + + /** + * idempotency expiration milliseconds, null means no need for idempotency, 0 or negative means no expiration + */ + long idempotencyMillisecondsToExpire() default 3600000L; + + /** + * idempotent method execute timeout + */ + long idempotentMethodExecuteTimeout() default 3000L; +} \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-idempotency/src/main/java/org/apache/rocketmq/spring/qsf/idempotency/QSFIdempotencyProviderPostProcessor.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-idempotency/src/main/java/org/apache/rocketmq/spring/qsf/idempotency/QSFIdempotencyProviderPostProcessor.java new file mode 100644 index 00000000..63cd7987 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-idempotency/src/main/java/org/apache/rocketmq/spring/qsf/idempotency/QSFIdempotencyProviderPostProcessor.java @@ -0,0 +1,72 @@ +/* + * 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.rocketmq.spring.qsf.idempotency; + +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.spring.qsf.model.MethodInvokeInfo; +import org.apache.rocketmq.spring.qsf.postprocessor.QSFProviderPostProcessor; +import org.apache.rocketmq.spring.qsf.store.QSFJedisClient; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import redis.clients.jedis.params.SetParams; + +/** + * @desc + **/ +@Component +@Slf4j +public class QSFIdempotencyProviderPostProcessor extends QSFProviderPostProcessor implements InitializingBean { + + @Autowired + private IdempotencyParamsManager idempotencyParamsManager; + + @Autowired + private QSFJedisClient qsfJedisClient; + + @Override + public void callAfterMessageProcess(MethodInvokeInfo methodInvokeInfo, Object returnValue) { + IdempotencyParams idempotencyParams = idempotencyParamsManager.getIdempotencyParams(methodInvokeInfo); + if (idempotencyParams == null || !idempotencyParams.isIdempotent()) { + // No need for idempotency, normal execution + return; + } + + String idempotencyKey = methodInvokeInfo.buildMethodInvokeInstanceSignature(); + String executeValue = IdempotencyLockUtils.lockValue(); + SetParams setParams = SetParams.setParams(); + if (idempotencyParams.getIdempotencyMillisecondsToExpire() > 0) { + setParams.px(idempotencyParams.getIdempotencyMillisecondsToExpire()); + } + // record executed status + qsfJedisClient.set(idempotencyKey, executeValue, setParams); + + String invokeLockKey = IdempotencyLockUtils.lockKey(idempotencyKey); + String lockValue = executeValue; + String lockValueRemote = qsfJedisClient.get(invokeLockKey); + if (lockValue.equals(lockValueRemote)) { + // unlock + try { + qsfJedisClient.del(invokeLockKey); + } catch (Throwable e) { + log.info(" unlock fail, just wait for the lock to expire, no side effects, ", e); + } + } + } + +} \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-idempotency/src/main/java/org/apache/rocketmq/spring/qsf/idempotency/QSFIdempotencyProviderPreProcessor.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-idempotency/src/main/java/org/apache/rocketmq/spring/qsf/idempotency/QSFIdempotencyProviderPreProcessor.java new file mode 100644 index 00000000..e646318b --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-idempotency/src/main/java/org/apache/rocketmq/spring/qsf/idempotency/QSFIdempotencyProviderPreProcessor.java @@ -0,0 +1,76 @@ +/* + * 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.rocketmq.spring.qsf.idempotency; + +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.spring.qsf.model.MethodInvokeInfo; +import org.apache.rocketmq.spring.qsf.preprocessor.QSFProviderPreProcessor; +import org.apache.rocketmq.spring.qsf.store.QSFJedisClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import redis.clients.jedis.params.SetParams; + +/** + * @desc + **/ +@Component +@Slf4j +public class QSFIdempotencyProviderPreProcessor extends QSFProviderPreProcessor { + + @Autowired + private IdempotencyParamsManager idempotencyParamsManager; + + @Autowired + private QSFJedisClient qsfJedisClient; + + @Override + public boolean callBeforeMessageProcess(MethodInvokeInfo methodInvokeInfo) { + IdempotencyParams idempotencyParams = idempotencyParamsManager.getIdempotencyParams(methodInvokeInfo); + if (idempotencyParams == null || !idempotencyParams.isIdempotent()) { + // No need for idempotency, normal execution + return true; + } + + String idempotencyKey = methodInvokeInfo.buildMethodInvokeInstanceSignature(); + if (qsfJedisClient.exists(idempotencyKey)) { + log.info(" method has been called elsewhere, ignored here, idempotencyKey:{}", idempotencyKey); + return false; + } + + String invokeLockKey = IdempotencyLockUtils.lockKey(idempotencyKey); + String lockValue = IdempotencyLockUtils.lockValue(); + long idempotentMethodExecuteTimeout = idempotencyParams.getIdempotentMethodExecuteTimeout(); + SetParams setParams = SetParams.setParams().nx().px(idempotentMethodExecuteTimeout); + String statusCode = null; + long now = System.currentTimeMillis(); + while (!QSFJedisClient.SUCCESS_OK.equalsIgnoreCase(statusCode) && System.currentTimeMillis() - now < idempotentMethodExecuteTimeout) { + statusCode = qsfJedisClient.set(invokeLockKey, lockValue, setParams); + } + if (QSFJedisClient.SUCCESS_OK.equalsIgnoreCase(statusCode)) { + if (qsfJedisClient.exists(idempotencyKey)) { + log.info(" method has been called elsewhere, ignored here, idempotencyKey:{}", idempotencyKey); + return false; + } + return true; + } + + log.info(" method is calling elsewhere, ignored here, methodInvokeInfo:{}", methodInvokeInfo); + return false; + } + +} \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-state-store/pom.xml b/rocketmq-spring-qsf/rocketmq-spring-qsf-state-store/pom.xml new file mode 100644 index 00000000..1be05641 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-state-store/pom.xml @@ -0,0 +1,77 @@ + + + + + + rocketmq-spring-qsf + org.apache.rocketmq + 1.0.0-SNAPSHOT + + 4.0.0 + + org.apache.rocketmq + rocketmq-spring-qsf-state-store + + + + org.apache.rocketmq + rocketmq-spring-qsf-core + + + + redis.clients + jedis + + + + org.projectlombok + lombok + provided + + + + + + + org.apache.maven.plugins + maven-resources-plugin + 3.1.0 + + UTF-8 + + + + org.apache.maven.plugins + maven-source-plugin + 3.1.0 + + + attach-sources + verify + + jar-no-fork + + + + + + + + \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-state-store/src/main/java/org/apache/rocketmq/spring/qsf/store/QSFJedisClient.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-state-store/src/main/java/org/apache/rocketmq/spring/qsf/store/QSFJedisClient.java new file mode 100644 index 00000000..6521cf9c --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-state-store/src/main/java/org/apache/rocketmq/spring/qsf/store/QSFJedisClient.java @@ -0,0 +1,35 @@ +/* + * 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.rocketmq.spring.qsf.store; + +import redis.clients.jedis.params.SetParams; + +/** + * @desc + */ +public interface QSFJedisClient { + String SUCCESS_OK = "OK"; + + String get(final String key); + + String set(String key, String value, SetParams params); + + Boolean exists(final String key); + + Long del(final String key); +} diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-state-store/src/main/java/org/apache/rocketmq/spring/qsf/store/QSFJedisClusterClient.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-state-store/src/main/java/org/apache/rocketmq/spring/qsf/store/QSFJedisClusterClient.java new file mode 100644 index 00000000..ef1e8b79 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-state-store/src/main/java/org/apache/rocketmq/spring/qsf/store/QSFJedisClusterClient.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.rocketmq.spring.qsf.store; + +import org.apache.rocketmq.spring.qsf.util.InvokeUtils; +import redis.clients.jedis.JedisCluster; +import redis.clients.jedis.params.SetParams; + +/** + * @desc + */ +public class QSFJedisClusterClient implements QSFJedisClient { + private JedisCluster jedisCluster; + + public void setJedisCluster(JedisCluster jedisCluster) { + this.jedisCluster = jedisCluster; + } + + @Override + public String get(String key) { + return (String) InvokeUtils.invokeWithRetry(jedisCluster, "get", + new Class[]{String.class}, + new Object[]{key}); +// return jedisCluster.get(key); + } + + @Override + public String set(String key, String value, SetParams params) { + return (String) InvokeUtils.invokeWithRetry(jedisCluster, "set", + new Class[]{String.class, String.class, SetParams.class}, + new Object[]{key, value, params}); +// return jedisCluster.set(key, value, params); + } + + @Override + public Boolean exists(String key) { + return (Boolean) InvokeUtils.invokeWithRetry(jedisCluster, "exists", + new Class[]{String.class}, + new Object[]{key}); +// return jedisCluster.exists(key); + } + + @Override + public Long del(String key) { + return (Long) InvokeUtils.invokeWithRetry(jedisCluster, "del", + new Class[]{String.class}, + new Object[]{key}); +// return jedisCluster.del(key); + } +} diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-state-store/src/main/java/org/apache/rocketmq/spring/qsf/store/QSFJedisPoolClient.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-state-store/src/main/java/org/apache/rocketmq/spring/qsf/store/QSFJedisPoolClient.java new file mode 100644 index 00000000..a40cc54e --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-state-store/src/main/java/org/apache/rocketmq/spring/qsf/store/QSFJedisPoolClient.java @@ -0,0 +1,79 @@ +/* + * 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.rocketmq.spring.qsf.store; + +import org.apache.rocketmq.spring.qsf.util.InvokeUtils; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.params.SetParams; + +/** + * @desc + */ +public class QSFJedisPoolClient implements QSFJedisClient { + private JedisPool jedisPool; + + public void setJedisPool(JedisPool jedisPool) { + this.jedisPool = jedisPool; + } + + private Jedis getJedis() { + return jedisPool.getResource(); + } + + @Override + public String get(String key) { + try (Jedis jedis = getJedis()) { + return (String) InvokeUtils.invokeWithRetry(jedis, "get", + new Class[]{String.class}, + new Object[]{key}); +// return jedis.get(key); + } + } + + @Override + public String set(String key, String value, SetParams params) { + try (Jedis jedis = getJedis()) { + return (String) InvokeUtils.invokeWithRetry(jedis, "set", + new Class[]{String.class, String.class, SetParams.class}, + new Object[]{key, value, params}); +// return jedis.set(key, value, params); + } + } + + @Override + public Boolean exists(String key) { + try (Jedis jedis = getJedis()) { + return (Boolean) InvokeUtils.invokeWithRetry(jedis, "exists", + new Class[]{String.class}, + new Object[]{key}); +// return jedis.exists(key); + } + } + + @Override + public Long del(String key) { + try (Jedis jedis = getJedis()) { + return (Long) InvokeUtils.invokeWithRetry(jedis, "del", + new Class[]{String.class}, + new Object[]{key}); +// return jedis.del(key); + } + } + +} \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-state-store/src/main/java/org/apache/rocketmq/spring/qsf/store/QSFStateStoreBeans.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-state-store/src/main/java/org/apache/rocketmq/spring/qsf/store/QSFStateStoreBeans.java new file mode 100644 index 00000000..b3e5ff62 --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-state-store/src/main/java/org/apache/rocketmq/spring/qsf/store/QSFStateStoreBeans.java @@ -0,0 +1,88 @@ +/* + * 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.rocketmq.spring.qsf.store; + +import lombok.extern.slf4j.Slf4j; +import org.apache.rocketmq.spring.qsf.util.QSFStringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.HostAndPort; +import redis.clients.jedis.JedisPoolConfig; +import redis.clients.jedis.JedisCluster; + +import java.util.HashSet; +import java.util.Set; + +/** + * @desc QSFStateStoreBeans such as jedis bean + */ +@Configuration +@Slf4j +public class QSFStateStoreBeans { + private final static String RESULT_OK = "OK"; + @Autowired + private QSFStateStoreRedisConfigProperties redisConfigProperties; + + @Bean("qsfJedisClient") + public QSFJedisClient qsfJedisClient() { + log.info(" init qsfJedisClient config:{}", redisConfigProperties); + JedisPoolConfig config = new JedisPoolConfig(); + config.setMaxIdle(redisConfigProperties.getMaxIdle()); + config.setMaxTotal(redisConfigProperties.getMaxTotal()); + config.setMaxWaitMillis(redisConfigProperties.getMaxWait()); + config.setTestOnBorrow(false); + config.setTestOnReturn(false); + + QSFJedisClient qsfJedisClient; + int timeout = redisConfigProperties.getTimeout(); + if (QSFStringUtils.isNotTrimEmpty(redisConfigProperties.getClusterNodes())) { + Set nodes = new HashSet<>(); + for (String clusterNode : redisConfigProperties.getClusterNodes().split(",")) { + String[] ipAndPort = clusterNode.trim().split(":"); + int port = Integer.parseInt(ipAndPort[1].trim()); + nodes.add(new HostAndPort(ipAndPort[0].trim(), port)); + } + + JedisCluster jedisCluster; + if (QSFStringUtils.isNotTrimEmpty(redisConfigProperties.getPassword())) { + jedisCluster = new JedisCluster(nodes, timeout, timeout, redisConfigProperties.getMaxAttempts(), redisConfigProperties.getPassword().trim(), "QSFStateStore", config); + } else { + jedisCluster = new JedisCluster(nodes, timeout, timeout, redisConfigProperties.getMaxAttempts(), config); + } + + qsfJedisClient = new QSFJedisClusterClient(); + ((QSFJedisClusterClient)qsfJedisClient).setJedisCluster(jedisCluster); + } else { + JedisPool jedisPool; + if (QSFStringUtils.isNotTrimEmpty(redisConfigProperties.getPassword())) { + jedisPool = new JedisPool(config, redisConfigProperties.getHost(), redisConfigProperties.getPort(), timeout, redisConfigProperties.getPassword().trim()); + } else { + jedisPool = new JedisPool(config, redisConfigProperties.getHost(), redisConfigProperties.getPort(), timeout); + } + + qsfJedisClient = new QSFJedisPoolClient(); + ((QSFJedisPoolClient)qsfJedisClient).setJedisPool(jedisPool); + } + + log.info(" init qsfJedisClient done:{}", qsfJedisClient); + + return qsfJedisClient; + } +} \ No newline at end of file diff --git a/rocketmq-spring-qsf/rocketmq-spring-qsf-state-store/src/main/java/org/apache/rocketmq/spring/qsf/store/QSFStateStoreRedisConfigProperties.java b/rocketmq-spring-qsf/rocketmq-spring-qsf-state-store/src/main/java/org/apache/rocketmq/spring/qsf/store/QSFStateStoreRedisConfigProperties.java new file mode 100644 index 00000000..86d6c5ad --- /dev/null +++ b/rocketmq-spring-qsf/rocketmq-spring-qsf-state-store/src/main/java/org/apache/rocketmq/spring/qsf/store/QSFStateStoreRedisConfigProperties.java @@ -0,0 +1,62 @@ +/* + * 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.rocketmq.spring.qsf.store; + +import lombok.Data; +import lombok.ToString; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +/** + * @desc redis config + */ +@Component +@Data +@ToString +public class QSFStateStoreRedisConfigProperties { + @Value("${qsf.store.redis.host}") + private String host; + + @Value("${qsf.store.redis.port}") + private Integer port = 6379; + + @Value("${qsf.store.redis.cluster.max-attempts}") + private Integer maxAttempts; + + @Value("${qsf.store.redis.timeout}") + private Integer timeout; + + @Value("${qsf.store.redis.password:}") + private String password; + + @Value("${qsf.store.redis.jedis.pool.max-total}") + private Integer maxTotal; + + @Value("${qsf.store.redis.jedis.pool.max-idle}") + private Integer maxIdle; + + @Value("${qsf.store.redis.jedis.pool.min-idle}") + private Integer minIdle; + + @Value("${qsf.store.redis.jedis.pool.max-wait}") + private Long maxWait; + + @Value("${qsf.store.redis.cluster.nodes:}") + private String clusterNodes; + +} \ No newline at end of file From 8df8b7c2542ae7699177c2da66b017d1120d0ccf Mon Sep 17 00:00:00 2001 From: "huanzu.chen" Date: Sun, 10 Apr 2022 21:28:06 +0800 Subject: [PATCH 2/3] add QSF(queue service framework) --- rocketmq-spring-qsf/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocketmq-spring-qsf/README.md b/rocketmq-spring-qsf/README.md index aa79e783..4436d12e 100644 --- a/rocketmq-spring-qsf/README.md +++ b/rocketmq-spring-qsf/README.md @@ -3,7 +3,7 @@ *** ### QSF introduction -+ With QSF we can produce/consume rocket-mq messages non-intrusively, and base QSF we can implement standard MQ eventual consistency, idempotency, flow control and so on. ++ With QSF we can produce & consume rocket-mq messages in the form of a method call, and base QSF we can implement standard MQ eventual consistency, idempotency, flow control and so on. *** From 41faaaf247d9fe86417b742a5a3494d7d819724f Mon Sep 17 00:00:00 2001 From: chz Date: Thu, 23 Jun 2022 23:03:52 +0800 Subject: [PATCH 3/3] fix fastjson 1.2.68.noneautotype missing --- rocketmq-spring-qsf/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rocketmq-spring-qsf/pom.xml b/rocketmq-spring-qsf/pom.xml index e62bd2f5..f0ee2afb 100644 --- a/rocketmq-spring-qsf/pom.xml +++ b/rocketmq-spring-qsf/pom.xml @@ -121,7 +121,7 @@ com.alibaba fastjson - 1.2.68.noneautotype + 1.2.69_noneautotype