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..4436d12e --- /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 in the form of a method call, 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..f0ee2afb --- /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.69_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 00000000..02a419e6 Binary files /dev/null and b/rocketmq-spring-qsf/qsf_architecture_cn.jpg differ diff --git a/rocketmq-spring-qsf/qsf_architecture_en.jpg b/rocketmq-spring-qsf/qsf_architecture_en.jpg new file mode 100644 index 00000000..a098a9e8 Binary files /dev/null and b/rocketmq-spring-qsf/qsf_architecture_en.jpg differ 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