Commit f8ed0e15f0a860d32107f2238e7ea7b2823fff42
1 parent
c066ea46
mq:增加 kafka、rocketmq、rabbitmq 的实现支持
Showing
20 changed files
with
645 additions
and
14 deletions
yudao-dependencies/pom.xml
| ... | ... | @@ -30,6 +30,8 @@ |
| 30 | 30 | <mybatis-plus-join.version>1.4.6</mybatis-plus-join.version> |
| 31 | 31 | <redisson.version>3.18.0</redisson.version> |
| 32 | 32 | <dm8.jdbc.version>8.1.3.62</dm8.jdbc.version> |
| 33 | + <!-- 消息队列 --> | |
| 34 | + <rocketmq-spring.version>2.2.3</rocketmq-spring.version> | |
| 33 | 35 | <!-- 服务保障相关 --> |
| 34 | 36 | <lock4j.version>2.2.5</lock4j.version> |
| 35 | 37 | <resilience4j.version>1.7.1</resilience4j.version> |
| ... | ... | @@ -98,11 +100,6 @@ |
| 98 | 100 | </dependency> |
| 99 | 101 | <dependency> |
| 100 | 102 | <groupId>cn.iocoder.boot</groupId> |
| 101 | - <artifactId>yudao-spring-boot-starter-biz-trade</artifactId> | |
| 102 | - <version>${revision}</version> | |
| 103 | - </dependency> | |
| 104 | - <dependency> | |
| 105 | - <groupId>cn.iocoder.boot</groupId> | |
| 106 | 103 | <artifactId>yudao-spring-boot-starter-biz-dict</artifactId> |
| 107 | 104 | <version>${revision}</version> |
| 108 | 105 | </dependency> |
| ... | ... | @@ -260,6 +257,12 @@ |
| 260 | 257 | <version>${revision}</version> |
| 261 | 258 | </dependency> |
| 262 | 259 | |
| 260 | + <dependency> | |
| 261 | + <groupId>org.apache.rocketmq</groupId> | |
| 262 | + <artifactId>rocketmq-spring-boot-starter</artifactId> | |
| 263 | + <version>${rocketmq-spring.version}</version> | |
| 264 | + </dependency> | |
| 265 | + | |
| 263 | 266 | <!-- 服务保障相关 --> |
| 264 | 267 | <dependency> |
| 265 | 268 | <groupId>cn.iocoder.boot</groupId> | ... | ... |
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/core/KeyValue.java
| ... | ... | @@ -4,6 +4,8 @@ import lombok.AllArgsConstructor; |
| 4 | 4 | import lombok.Data; |
| 5 | 5 | import lombok.NoArgsConstructor; |
| 6 | 6 | |
| 7 | +import java.io.Serializable; | |
| 8 | + | |
| 7 | 9 | /** |
| 8 | 10 | * Key Value 的键值对 |
| 9 | 11 | * |
| ... | ... | @@ -12,7 +14,7 @@ import lombok.NoArgsConstructor; |
| 12 | 14 | @Data |
| 13 | 15 | @NoArgsConstructor |
| 14 | 16 | @AllArgsConstructor |
| 15 | -public class KeyValue<K, V> { | |
| 17 | +public class KeyValue<K, V> implements Serializable { | |
| 16 | 18 | |
| 17 | 19 | private K key; |
| 18 | 20 | private V value; | ... | ... |
yudao-framework/yudao-spring-boot-starter-biz-tenant/pom.xml
| ... | ... | @@ -49,6 +49,21 @@ |
| 49 | 49 | <groupId>cn.iocoder.boot</groupId> |
| 50 | 50 | <artifactId>yudao-spring-boot-starter-mq</artifactId> |
| 51 | 51 | </dependency> |
| 52 | + <dependency> | |
| 53 | + <groupId>org.springframework.kafka</groupId> | |
| 54 | + <artifactId>spring-kafka</artifactId> | |
| 55 | + <optional>true</optional> | |
| 56 | + </dependency> | |
| 57 | + <dependency> | |
| 58 | + <groupId>org.springframework.amqp</groupId> | |
| 59 | + <artifactId>spring-rabbit</artifactId> | |
| 60 | + <optional>true</optional> | |
| 61 | + </dependency> | |
| 62 | + <dependency> | |
| 63 | + <groupId>org.apache.rocketmq</groupId> | |
| 64 | + <artifactId>rocketmq-spring-boot-starter</artifactId> | |
| 65 | + <optional>true</optional> | |
| 66 | + </dependency> | |
| 52 | 67 | |
| 53 | 68 | <!-- Test 测试相关 --> |
| 54 | 69 | <dependency> | ... | ... |
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/config/YudaoTenantAutoConfiguration.java
| ... | ... | @@ -6,7 +6,9 @@ import cn.iocoder.yudao.framework.redis.config.YudaoCacheProperties; |
| 6 | 6 | import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnoreAspect; |
| 7 | 7 | import cn.iocoder.yudao.framework.tenant.core.db.TenantDatabaseInterceptor; |
| 8 | 8 | import cn.iocoder.yudao.framework.tenant.core.job.TenantJobAspect; |
| 9 | -import cn.iocoder.yudao.framework.tenant.core.mq.TenantRedisMessageInterceptor; | |
| 9 | +import cn.iocoder.yudao.framework.tenant.core.mq.rabbitmq.TenantRabbitMQInitializer; | |
| 10 | +import cn.iocoder.yudao.framework.tenant.core.mq.redis.TenantRedisMessageInterceptor; | |
| 11 | +import cn.iocoder.yudao.framework.tenant.core.mq.rocketmq.TenantRocketMQInitializer; | |
| 10 | 12 | import cn.iocoder.yudao.framework.tenant.core.redis.TenantRedisCacheManager; |
| 11 | 13 | import cn.iocoder.yudao.framework.tenant.core.security.TenantSecurityWebFilter; |
| 12 | 14 | import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService; |
| ... | ... | @@ -18,6 +20,7 @@ import cn.iocoder.yudao.module.system.api.tenant.TenantApi; |
| 18 | 20 | import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; |
| 19 | 21 | import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor; |
| 20 | 22 | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| 23 | +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; | |
| 21 | 24 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
| 22 | 25 | import org.springframework.boot.context.properties.EnableConfigurationProperties; |
| 23 | 26 | import org.springframework.boot.web.servlet.FilterRegistrationBean; |
| ... | ... | @@ -92,6 +95,18 @@ public class YudaoTenantAutoConfiguration { |
| 92 | 95 | return new TenantRedisMessageInterceptor(); |
| 93 | 96 | } |
| 94 | 97 | |
| 98 | + @Bean | |
| 99 | + @ConditionalOnClass(name = "org.springframework.amqp.rabbit.core.RabbitTemplate") | |
| 100 | + public TenantRabbitMQInitializer tenantRabbitMQInitializer() { | |
| 101 | + return new TenantRabbitMQInitializer(); | |
| 102 | + } | |
| 103 | + | |
| 104 | + @Bean | |
| 105 | + @ConditionalOnClass(name = "org.apache.rocketmq.spring.core.RocketMQTemplate") | |
| 106 | + public TenantRocketMQInitializer tenantRocketMQInitializer() { | |
| 107 | + return new TenantRocketMQInitializer(); | |
| 108 | + } | |
| 109 | + | |
| 95 | 110 | // ========== Job ========== |
| 96 | 111 | |
| 97 | 112 | @Bean | ... | ... |
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/kafka/TenantKafkaEnvironmentPostProcessor.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.tenant.core.mq.kafka; | |
| 2 | + | |
| 3 | +import cn.hutool.core.util.StrUtil; | |
| 4 | +import lombok.extern.slf4j.Slf4j; | |
| 5 | +import org.springframework.boot.SpringApplication; | |
| 6 | +import org.springframework.boot.env.EnvironmentPostProcessor; | |
| 7 | +import org.springframework.core.env.ConfigurableEnvironment; | |
| 8 | + | |
| 9 | +/** | |
| 10 | + * 多租户的 Kafka 的 {@link EnvironmentPostProcessor} 实现类 | |
| 11 | + * | |
| 12 | + * Kafka Producer 发送消息时,增加 {@link TenantKafkaProducerInterceptor} 拦截器 | |
| 13 | + * | |
| 14 | + * @author 芋道源码 | |
| 15 | + */ | |
| 16 | +@Slf4j | |
| 17 | +public class TenantKafkaEnvironmentPostProcessor implements EnvironmentPostProcessor { | |
| 18 | + | |
| 19 | + private static final String PROPERTY_KEY_INTERCEPTOR_CLASSES = "spring.kafka.producer.properties.interceptor.classes"; | |
| 20 | + | |
| 21 | + @Override | |
| 22 | + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { | |
| 23 | + // 添加 TenantKafkaProducerInterceptor 拦截器 | |
| 24 | + try { | |
| 25 | + String value = environment.getProperty(PROPERTY_KEY_INTERCEPTOR_CLASSES); | |
| 26 | + if (StrUtil.isEmpty(value)) { | |
| 27 | + value = TenantKafkaProducerInterceptor.class.getName(); | |
| 28 | + } else { | |
| 29 | + value += "," + TenantKafkaProducerInterceptor.class.getName(); | |
| 30 | + } | |
| 31 | + environment.getSystemProperties().put(PROPERTY_KEY_INTERCEPTOR_CLASSES, value); | |
| 32 | + } catch (NoClassDefFoundError ignore) { | |
| 33 | + // 如果触发 NoClassDefFoundError 异常,说明 TenantKafkaProducerInterceptor 类不存在,即没引入 kafka-spring 依赖 | |
| 34 | + } | |
| 35 | + } | |
| 36 | + | |
| 37 | +} | ... | ... |
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/kafka/TenantKafkaProducerInterceptor.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.tenant.core.mq.kafka; | |
| 2 | + | |
| 3 | +import cn.hutool.core.util.ReflectUtil; | |
| 4 | +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; | |
| 5 | +import org.apache.kafka.clients.producer.ProducerInterceptor; | |
| 6 | +import org.apache.kafka.clients.producer.ProducerRecord; | |
| 7 | +import org.apache.kafka.clients.producer.RecordMetadata; | |
| 8 | +import org.apache.kafka.common.header.Headers; | |
| 9 | +import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; | |
| 10 | + | |
| 11 | +import java.util.Map; | |
| 12 | + | |
| 13 | +import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; | |
| 14 | + | |
| 15 | +/** | |
| 16 | + * Kafka 消息队列的多租户 {@link ProducerInterceptor} 实现类 | |
| 17 | + * | |
| 18 | + * 1. Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中 | |
| 19 | + * 2. Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中,通过 {@link InvocableHandlerMethod} 实现 | |
| 20 | + * | |
| 21 | + * @author 芋道源码 | |
| 22 | + */ | |
| 23 | +public class TenantKafkaProducerInterceptor implements ProducerInterceptor<Object, Object> { | |
| 24 | + | |
| 25 | + @Override | |
| 26 | + public ProducerRecord<Object, Object> onSend(ProducerRecord<Object, Object> record) { | |
| 27 | + Long tenantId = TenantContextHolder.getTenantId(); | |
| 28 | + if (tenantId != null) { | |
| 29 | + Headers headers = (Headers) ReflectUtil.getFieldValue(record, "headers"); // private 属性,没有 get 方法,智能反射 | |
| 30 | + headers.add(HEADER_TENANT_ID, tenantId.toString().getBytes()); | |
| 31 | + } | |
| 32 | + return record; | |
| 33 | + } | |
| 34 | + | |
| 35 | + @Override | |
| 36 | + public void onAcknowledgement(RecordMetadata metadata, Exception exception) { | |
| 37 | + } | |
| 38 | + | |
| 39 | + @Override | |
| 40 | + public void close() { | |
| 41 | + } | |
| 42 | + | |
| 43 | + @Override | |
| 44 | + public void configure(Map<String, ?> configs) { | |
| 45 | + } | |
| 46 | + | |
| 47 | +} | ... | ... |
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/rabbitmq/TenantRabbitMQInitializer.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.tenant.core.mq.rabbitmq; | |
| 2 | + | |
| 3 | +import org.springframework.amqp.rabbit.core.RabbitTemplate; | |
| 4 | +import org.springframework.beans.BeansException; | |
| 5 | +import org.springframework.beans.factory.config.BeanPostProcessor; | |
| 6 | + | |
| 7 | +/** | |
| 8 | + * 多租户的 RabbitMQ 初始化器 | |
| 9 | + * | |
| 10 | + * @author 芋道源码 | |
| 11 | + */ | |
| 12 | +public class TenantRabbitMQInitializer implements BeanPostProcessor { | |
| 13 | + | |
| 14 | + @Override | |
| 15 | + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { | |
| 16 | + if (bean instanceof RabbitTemplate) { | |
| 17 | + RabbitTemplate rabbitTemplate = (RabbitTemplate) bean; | |
| 18 | + rabbitTemplate.addBeforePublishPostProcessors(new TenantRabbitMQMessagePostProcessor()); | |
| 19 | + } | |
| 20 | + return bean; | |
| 21 | + } | |
| 22 | + | |
| 23 | +} | |
| 0 | 24 | \ No newline at end of file | ... | ... |
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/rabbitmq/TenantRabbitMQMessagePostProcessor.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.tenant.core.mq.rabbitmq; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; | |
| 4 | +import org.apache.kafka.clients.producer.ProducerInterceptor; | |
| 5 | +import org.springframework.amqp.AmqpException; | |
| 6 | +import org.springframework.amqp.core.Message; | |
| 7 | +import org.springframework.amqp.core.MessagePostProcessor; | |
| 8 | +import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; | |
| 9 | + | |
| 10 | +import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; | |
| 11 | + | |
| 12 | +/** | |
| 13 | + * RabbitMQ 消息队列的多租户 {@link ProducerInterceptor} 实现类 | |
| 14 | + * | |
| 15 | + * 1. Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中 | |
| 16 | + * 2. Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中,通过 {@link InvocableHandlerMethod} 实现 | |
| 17 | + * | |
| 18 | + * @author 芋道源码 | |
| 19 | + */ | |
| 20 | +public class TenantRabbitMQMessagePostProcessor implements MessagePostProcessor { | |
| 21 | + | |
| 22 | + @Override | |
| 23 | + public Message postProcessMessage(Message message) throws AmqpException { | |
| 24 | + Long tenantId = TenantContextHolder.getTenantId(); | |
| 25 | + if (tenantId != null) { | |
| 26 | + message.getMessageProperties().getHeaders().put(HEADER_TENANT_ID, tenantId); | |
| 27 | + } | |
| 28 | + return message; | |
| 29 | + } | |
| 30 | + | |
| 31 | +} | ... | ... |
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/TenantRedisMessageInterceptor.java renamed to yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/redis/TenantRedisMessageInterceptor.java
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/rocketmq/TenantRocketMQConsumeMessageHook.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.tenant.core.mq.rocketmq; | |
| 2 | + | |
| 3 | +import cn.hutool.core.lang.Assert; | |
| 4 | +import cn.hutool.core.util.StrUtil; | |
| 5 | +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; | |
| 6 | +import org.apache.rocketmq.client.hook.ConsumeMessageContext; | |
| 7 | +import org.apache.rocketmq.client.hook.ConsumeMessageHook; | |
| 8 | +import org.apache.rocketmq.common.message.MessageExt; | |
| 9 | +import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; | |
| 10 | + | |
| 11 | +import java.util.List; | |
| 12 | + | |
| 13 | +import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; | |
| 14 | + | |
| 15 | +/** | |
| 16 | + * RocketMQ 消息队列的多租户 {@link ConsumeMessageHook} 实现类 | |
| 17 | + * | |
| 18 | + * Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中,通过 {@link InvocableHandlerMethod} 实现 | |
| 19 | + * | |
| 20 | + * @author 芋道源码 | |
| 21 | + */ | |
| 22 | +public class TenantRocketMQConsumeMessageHook implements ConsumeMessageHook { | |
| 23 | + | |
| 24 | + @Override | |
| 25 | + public String hookName() { | |
| 26 | + return getClass().getSimpleName(); | |
| 27 | + } | |
| 28 | + | |
| 29 | + @Override | |
| 30 | + public void consumeMessageBefore(ConsumeMessageContext context) { | |
| 31 | + // 校验,消息必须是单条,不然设置租户可能不正确 | |
| 32 | + List<MessageExt> messages = context.getMsgList(); | |
| 33 | + Assert.isTrue(messages.size() == 1, "消息条数({})不正确", messages.size()); | |
| 34 | + // 设置租户编号 | |
| 35 | + String tenantId = messages.get(0).getUserProperty(HEADER_TENANT_ID); | |
| 36 | + if (StrUtil.isNotEmpty(tenantId)) { | |
| 37 | + TenantContextHolder.setTenantId(Long.parseLong(tenantId)); | |
| 38 | + } | |
| 39 | + } | |
| 40 | + | |
| 41 | + @Override | |
| 42 | + public void consumeMessageAfter(ConsumeMessageContext context) { | |
| 43 | + TenantContextHolder.clear(); | |
| 44 | + } | |
| 45 | + | |
| 46 | +} | ... | ... |
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/rocketmq/TenantRocketMQInitializer.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.tenant.core.mq.rocketmq; | |
| 2 | + | |
| 3 | +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; | |
| 4 | +import org.apache.rocketmq.client.impl.consumer.DefaultMQPushConsumerImpl; | |
| 5 | +import org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl; | |
| 6 | +import org.apache.rocketmq.client.producer.DefaultMQProducer; | |
| 7 | +import org.apache.rocketmq.spring.core.RocketMQTemplate; | |
| 8 | +import org.apache.rocketmq.spring.support.DefaultRocketMQListenerContainer; | |
| 9 | +import org.springframework.beans.BeansException; | |
| 10 | +import org.springframework.beans.factory.config.BeanPostProcessor; | |
| 11 | + | |
| 12 | +/** | |
| 13 | + * 多租户的 RocketMQ 初始化器 | |
| 14 | + * | |
| 15 | + * @author 芋道源码 | |
| 16 | + */ | |
| 17 | +public class TenantRocketMQInitializer implements BeanPostProcessor { | |
| 18 | + | |
| 19 | + @Override | |
| 20 | + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { | |
| 21 | + if (bean instanceof DefaultRocketMQListenerContainer) { | |
| 22 | + DefaultRocketMQListenerContainer container = (DefaultRocketMQListenerContainer) bean; | |
| 23 | + initTenantConsumer(container.getConsumer()); | |
| 24 | + } else if (bean instanceof RocketMQTemplate) { | |
| 25 | + RocketMQTemplate template = (RocketMQTemplate) bean; | |
| 26 | + initTenantProducer(template.getProducer()); | |
| 27 | + } | |
| 28 | + return bean; | |
| 29 | + } | |
| 30 | + | |
| 31 | + private void initTenantProducer(DefaultMQProducer producer) { | |
| 32 | + if (producer == null) { | |
| 33 | + return; | |
| 34 | + } | |
| 35 | + DefaultMQProducerImpl producerImpl = producer.getDefaultMQProducerImpl(); | |
| 36 | + if (producerImpl == null) { | |
| 37 | + return; | |
| 38 | + } | |
| 39 | + producerImpl.registerSendMessageHook(new TenantRocketMQSendMessageHook()); | |
| 40 | + } | |
| 41 | + | |
| 42 | + private void initTenantConsumer(DefaultMQPushConsumer consumer) { | |
| 43 | + if (consumer == null) { | |
| 44 | + return; | |
| 45 | + } | |
| 46 | + DefaultMQPushConsumerImpl consumerImpl = consumer.getDefaultMQPushConsumerImpl(); | |
| 47 | + if (consumerImpl == null) { | |
| 48 | + return; | |
| 49 | + } | |
| 50 | + consumerImpl.registerConsumeMessageHook(new TenantRocketMQConsumeMessageHook()); | |
| 51 | + } | |
| 52 | + | |
| 53 | +} | |
| 0 | 54 | \ No newline at end of file | ... | ... |
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/rocketmq/TenantRocketMQSendMessageHook.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.tenant.core.mq.rocketmq; | |
| 2 | + | |
| 3 | +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; | |
| 4 | +import org.apache.rocketmq.client.hook.SendMessageContext; | |
| 5 | +import org.apache.rocketmq.client.hook.SendMessageHook; | |
| 6 | + | |
| 7 | +import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; | |
| 8 | + | |
| 9 | +/** | |
| 10 | + * RocketMQ 消息队列的多租户 {@link SendMessageHook} 实现类 | |
| 11 | + * | |
| 12 | + * Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中 | |
| 13 | + * | |
| 14 | + * @author 芋道源码 | |
| 15 | + */ | |
| 16 | +public class TenantRocketMQSendMessageHook implements SendMessageHook { | |
| 17 | + | |
| 18 | + @Override | |
| 19 | + public String hookName() { | |
| 20 | + return getClass().getSimpleName(); | |
| 21 | + } | |
| 22 | + | |
| 23 | + @Override | |
| 24 | + public void sendMessageBefore(SendMessageContext sendMessageContext) { | |
| 25 | + Long tenantId = TenantContextHolder.getTenantId(); | |
| 26 | + if (tenantId == null) { | |
| 27 | + return; | |
| 28 | + } | |
| 29 | + sendMessageContext.getMessage().putUserProperty(HEADER_TENANT_ID, tenantId.toString()); | |
| 30 | + } | |
| 31 | + | |
| 32 | + @Override | |
| 33 | + public void sendMessageAfter(SendMessageContext sendMessageContext) { | |
| 34 | + } | |
| 35 | + | |
| 36 | +} | ... | ... |
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethod.java
0 → 100644
| 1 | +/* | |
| 2 | + * Copyright 2002-2021 the original author or authors. | |
| 3 | + * | |
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
| 5 | + * you may not use this file except in compliance with the License. | |
| 6 | + * You may obtain a copy of the License at | |
| 7 | + * | |
| 8 | + * https://www.apache.org/licenses/LICENSE-2.0 | |
| 9 | + * | |
| 10 | + * Unless required by applicable law or agreed to in writing, software | |
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 13 | + * See the License for the specific language governing permissions and | |
| 14 | + * limitations under the License. | |
| 15 | + */ | |
| 16 | + | |
| 17 | +package org.springframework.messaging.handler.invocation; | |
| 18 | + | |
| 19 | +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; | |
| 20 | +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; | |
| 21 | +import org.springframework.core.DefaultParameterNameDiscoverer; | |
| 22 | +import org.springframework.core.MethodParameter; | |
| 23 | +import org.springframework.core.ParameterNameDiscoverer; | |
| 24 | +import org.springframework.core.ResolvableType; | |
| 25 | +import org.springframework.lang.Nullable; | |
| 26 | +import org.springframework.messaging.Message; | |
| 27 | +import org.springframework.messaging.handler.HandlerMethod; | |
| 28 | +import org.springframework.util.ObjectUtils; | |
| 29 | + | |
| 30 | +import java.lang.reflect.InvocationTargetException; | |
| 31 | +import java.lang.reflect.Method; | |
| 32 | +import java.lang.reflect.Type; | |
| 33 | +import java.util.Arrays; | |
| 34 | + | |
| 35 | +import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; | |
| 36 | + | |
| 37 | +/** | |
| 38 | + * Extension of {@link HandlerMethod} that invokes the underlying method with | |
| 39 | + * argument values resolved from the current HTTP request through a list of | |
| 40 | + * {@link HandlerMethodArgumentResolver}. | |
| 41 | + * | |
| 42 | + * 针对 rabbitmq-spring 和 kafka-spring,不存在合适的拓展点,可以实现 Consumer 消费前,读取 Header 中的 tenant-id 设置到 {@link TenantContextHolder} 中 | |
| 43 | + * TODO 芋艿:持续跟进,看看有没新的拓展点 | |
| 44 | + * | |
| 45 | + * @author Rossen Stoyanchev | |
| 46 | + * @author Juergen Hoeller | |
| 47 | + * @since 4.0 | |
| 48 | + */ | |
| 49 | +public class InvocableHandlerMethod extends HandlerMethod { | |
| 50 | + | |
| 51 | + private static final Object[] EMPTY_ARGS = new Object[0]; | |
| 52 | + | |
| 53 | + private HandlerMethodArgumentResolverComposite resolvers = new HandlerMethodArgumentResolverComposite(); | |
| 54 | + | |
| 55 | + private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); | |
| 56 | + | |
| 57 | + /** | |
| 58 | + * Create an instance from a {@code HandlerMethod}. | |
| 59 | + */ | |
| 60 | + public InvocableHandlerMethod(HandlerMethod handlerMethod) { | |
| 61 | + super(handlerMethod); | |
| 62 | + } | |
| 63 | + | |
| 64 | + /** | |
| 65 | + * Create an instance from a bean instance and a method. | |
| 66 | + */ | |
| 67 | + public InvocableHandlerMethod(Object bean, Method method) { | |
| 68 | + super(bean, method); | |
| 69 | + } | |
| 70 | + | |
| 71 | + /** | |
| 72 | + * Construct a new handler method with the given bean instance, method name and parameters. | |
| 73 | + * @param bean the object bean | |
| 74 | + * @param methodName the method name | |
| 75 | + * @param parameterTypes the method parameter types | |
| 76 | + * @throws NoSuchMethodException when the method cannot be found | |
| 77 | + */ | |
| 78 | + public InvocableHandlerMethod(Object bean, String methodName, Class<?>... parameterTypes) | |
| 79 | + throws NoSuchMethodException { | |
| 80 | + | |
| 81 | + super(bean, methodName, parameterTypes); | |
| 82 | + } | |
| 83 | + | |
| 84 | + /** | |
| 85 | + * Set {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers} to use for resolving method argument values. | |
| 86 | + */ | |
| 87 | + public void setMessageMethodArgumentResolvers(HandlerMethodArgumentResolverComposite argumentResolvers) { | |
| 88 | + this.resolvers = argumentResolvers; | |
| 89 | + } | |
| 90 | + | |
| 91 | + /** | |
| 92 | + * Set the ParameterNameDiscoverer for resolving parameter names when needed | |
| 93 | + * (e.g. default request attribute name). | |
| 94 | + * <p>Default is a {@link DefaultParameterNameDiscoverer}. | |
| 95 | + */ | |
| 96 | + public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) { | |
| 97 | + this.parameterNameDiscoverer = parameterNameDiscoverer; | |
| 98 | + } | |
| 99 | + | |
| 100 | + /** | |
| 101 | + * Invoke the method after resolving its argument values in the context of the given message. | |
| 102 | + * <p>Argument values are commonly resolved through | |
| 103 | + * {@link HandlerMethodArgumentResolver HandlerMethodArgumentResolvers}. | |
| 104 | + * The {@code providedArgs} parameter however may supply argument values to be used directly, | |
| 105 | + * i.e. without argument resolution. | |
| 106 | + * <p>Delegates to {@link #getMethodArgumentValues} and calls {@link #doInvoke} with the | |
| 107 | + * resolved arguments. | |
| 108 | + * @param message the current message being processed | |
| 109 | + * @param providedArgs "given" arguments matched by type, not resolved | |
| 110 | + * @return the raw value returned by the invoked method | |
| 111 | + * @throws Exception raised if no suitable argument resolver can be found, | |
| 112 | + * or if the method raised an exception | |
| 113 | + * @see #getMethodArgumentValues | |
| 114 | + * @see #doInvoke | |
| 115 | + */ | |
| 116 | + @Nullable | |
| 117 | + public Object invoke(Message<?> message, Object... providedArgs) throws Exception { | |
| 118 | + Object[] args = getMethodArgumentValues(message, providedArgs); | |
| 119 | + if (logger.isTraceEnabled()) { | |
| 120 | + logger.trace("Arguments: " + Arrays.toString(args)); | |
| 121 | + } | |
| 122 | + // 注意:如下是本类的改动点!!! | |
| 123 | + // 情况一:无租户编号的情况 | |
| 124 | + Long tenantId= parseTenantId(message); | |
| 125 | + if (tenantId == null) { | |
| 126 | + return doInvoke(args); | |
| 127 | + } | |
| 128 | + // 情况二:有租户的情况下 | |
| 129 | + return TenantUtils.execute(tenantId, () -> doInvoke(args)); | |
| 130 | + } | |
| 131 | + | |
| 132 | + private Long parseTenantId(Message<?> message) { | |
| 133 | + Object tenantId = message.getHeaders().get(HEADER_TENANT_ID); | |
| 134 | + if (tenantId == null) { | |
| 135 | + return null; | |
| 136 | + } | |
| 137 | + if (tenantId instanceof Long) { | |
| 138 | + return (Long) tenantId; | |
| 139 | + } | |
| 140 | + if (tenantId instanceof Number) { | |
| 141 | + return ((Number) tenantId).longValue(); | |
| 142 | + } | |
| 143 | + if (tenantId instanceof String) { | |
| 144 | + return Long.parseLong((String) tenantId); | |
| 145 | + } | |
| 146 | + if (tenantId instanceof byte[]) { | |
| 147 | + return Long.parseLong(new String((byte[]) tenantId)); | |
| 148 | + } | |
| 149 | + throw new IllegalArgumentException("未知的数据类型:" + tenantId); | |
| 150 | + } | |
| 151 | + | |
| 152 | + /** | |
| 153 | + * Get the method argument values for the current message, checking the provided | |
| 154 | + * argument values and falling back to the configured argument resolvers. | |
| 155 | + * <p>The resulting array will be passed into {@link #doInvoke}. | |
| 156 | + * @since 5.1.2 | |
| 157 | + */ | |
| 158 | + protected Object[] getMethodArgumentValues(Message<?> message, Object... providedArgs) throws Exception { | |
| 159 | + MethodParameter[] parameters = getMethodParameters(); | |
| 160 | + if (ObjectUtils.isEmpty(parameters)) { | |
| 161 | + return EMPTY_ARGS; | |
| 162 | + } | |
| 163 | + | |
| 164 | + Object[] args = new Object[parameters.length]; | |
| 165 | + for (int i = 0; i < parameters.length; i++) { | |
| 166 | + MethodParameter parameter = parameters[i]; | |
| 167 | + parameter.initParameterNameDiscovery(this.parameterNameDiscoverer); | |
| 168 | + args[i] = findProvidedArgument(parameter, providedArgs); | |
| 169 | + if (args[i] != null) { | |
| 170 | + continue; | |
| 171 | + } | |
| 172 | + if (!this.resolvers.supportsParameter(parameter)) { | |
| 173 | + throw new MethodArgumentResolutionException( | |
| 174 | + message, parameter, formatArgumentError(parameter, "No suitable resolver")); | |
| 175 | + } | |
| 176 | + try { | |
| 177 | + args[i] = this.resolvers.resolveArgument(parameter, message); | |
| 178 | + } | |
| 179 | + catch (Exception ex) { | |
| 180 | + // Leave stack trace for later, exception may actually be resolved and handled... | |
| 181 | + if (logger.isDebugEnabled()) { | |
| 182 | + String exMsg = ex.getMessage(); | |
| 183 | + if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) { | |
| 184 | + logger.debug(formatArgumentError(parameter, exMsg)); | |
| 185 | + } | |
| 186 | + } | |
| 187 | + throw ex; | |
| 188 | + } | |
| 189 | + } | |
| 190 | + return args; | |
| 191 | + } | |
| 192 | + | |
| 193 | + /** | |
| 194 | + * Invoke the handler method with the given argument values. | |
| 195 | + */ | |
| 196 | + @Nullable | |
| 197 | + protected Object doInvoke(Object... args) throws Exception { | |
| 198 | + try { | |
| 199 | + return getBridgedMethod().invoke(getBean(), args); | |
| 200 | + } | |
| 201 | + catch (IllegalArgumentException ex) { | |
| 202 | + assertTargetBean(getBridgedMethod(), getBean(), args); | |
| 203 | + String text = (ex.getMessage() != null ? ex.getMessage() : "Illegal argument"); | |
| 204 | + throw new IllegalStateException(formatInvokeError(text, args), ex); | |
| 205 | + } | |
| 206 | + catch (InvocationTargetException ex) { | |
| 207 | + // Unwrap for HandlerExceptionResolvers ... | |
| 208 | + Throwable targetException = ex.getTargetException(); | |
| 209 | + if (targetException instanceof RuntimeException) { | |
| 210 | + throw (RuntimeException) targetException; | |
| 211 | + } | |
| 212 | + else if (targetException instanceof Error) { | |
| 213 | + throw (Error) targetException; | |
| 214 | + } | |
| 215 | + else if (targetException instanceof Exception) { | |
| 216 | + throw (Exception) targetException; | |
| 217 | + } | |
| 218 | + else { | |
| 219 | + throw new IllegalStateException(formatInvokeError("Invocation failure", args), targetException); | |
| 220 | + } | |
| 221 | + } | |
| 222 | + } | |
| 223 | + | |
| 224 | + MethodParameter getAsyncReturnValueType(@Nullable Object returnValue) { | |
| 225 | + return new AsyncResultMethodParameter(returnValue); | |
| 226 | + } | |
| 227 | + | |
| 228 | + private class AsyncResultMethodParameter extends HandlerMethodParameter { | |
| 229 | + | |
| 230 | + @Nullable | |
| 231 | + private final Object returnValue; | |
| 232 | + | |
| 233 | + private final ResolvableType returnType; | |
| 234 | + | |
| 235 | + public AsyncResultMethodParameter(@Nullable Object returnValue) { | |
| 236 | + super(-1); | |
| 237 | + this.returnValue = returnValue; | |
| 238 | + this.returnType = ResolvableType.forType(super.getGenericParameterType()).getGeneric(); | |
| 239 | + } | |
| 240 | + | |
| 241 | + protected AsyncResultMethodParameter(AsyncResultMethodParameter original) { | |
| 242 | + super(original); | |
| 243 | + this.returnValue = original.returnValue; | |
| 244 | + this.returnType = original.returnType; | |
| 245 | + } | |
| 246 | + | |
| 247 | + @Override | |
| 248 | + public Class<?> getParameterType() { | |
| 249 | + if (this.returnValue != null) { | |
| 250 | + return this.returnValue.getClass(); | |
| 251 | + } | |
| 252 | + if (!ResolvableType.NONE.equals(this.returnType)) { | |
| 253 | + return this.returnType.toClass(); | |
| 254 | + } | |
| 255 | + return super.getParameterType(); | |
| 256 | + } | |
| 257 | + | |
| 258 | + @Override | |
| 259 | + public Type getGenericParameterType() { | |
| 260 | + return this.returnType.getType(); | |
| 261 | + } | |
| 262 | + | |
| 263 | + @Override | |
| 264 | + public AsyncResultMethodParameter clone() { | |
| 265 | + return new AsyncResultMethodParameter(this); | |
| 266 | + } | |
| 267 | + } | |
| 268 | + | |
| 269 | +} | ... | ... |
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/resources/META-INF/spring.factories
0 → 100644
yudao-framework/yudao-spring-boot-starter-mq/pom.xml
| ... | ... | @@ -12,7 +12,7 @@ |
| 12 | 12 | <packaging>jar</packaging> |
| 13 | 13 | |
| 14 | 14 | <name>${project.artifactId}</name> |
| 15 | - <description>消息队列,基于 Redis Pub/Sub 实现广播消费,基于 Stream 实现集群消费</description> | |
| 15 | + <description>消息队列,支持 Redis、RocketMQ、RabbitMQ、Kafka 四种</description> | |
| 16 | 16 | <url>https://github.com/YunaiV/ruoyi-vue-pro</url> |
| 17 | 17 | |
| 18 | 18 | <dependencies> |
| ... | ... | @@ -21,6 +21,23 @@ |
| 21 | 21 | <groupId>cn.iocoder.boot</groupId> |
| 22 | 22 | <artifactId>yudao-spring-boot-starter-redis</artifactId> |
| 23 | 23 | </dependency> |
| 24 | + | |
| 25 | + <!-- 消息队列相关 --> | |
| 26 | + <dependency> | |
| 27 | + <groupId>org.springframework.kafka</groupId> | |
| 28 | + <artifactId>spring-kafka</artifactId> | |
| 29 | + <optional>true</optional> | |
| 30 | + </dependency> | |
| 31 | + <dependency> | |
| 32 | + <groupId>org.springframework.amqp</groupId> | |
| 33 | + <artifactId>spring-rabbit</artifactId> | |
| 34 | + <optional>true</optional> | |
| 35 | + </dependency> | |
| 36 | + <dependency> | |
| 37 | + <groupId>org.apache.rocketmq</groupId> | |
| 38 | + <artifactId>rocketmq-spring-boot-starter</artifactId> | |
| 39 | + <optional>true</optional> | |
| 40 | + </dependency> | |
| 24 | 41 | </dependencies> |
| 25 | 42 | |
| 26 | 43 | -</project> |
| 44 | +</project> | |
| 27 | 45 | \ No newline at end of file | ... | ... |
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/package-info.java
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/rabbitmq/config/YudaoRabbitMQAutoConfiguration.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.mq.rabbitmq.config; | |
| 2 | + | |
| 3 | +import cn.hutool.core.util.ReflectUtil; | |
| 4 | +import lombok.extern.slf4j.Slf4j; | |
| 5 | +import org.springframework.amqp.utils.SerializationUtils; | |
| 6 | +import org.springframework.boot.autoconfigure.AutoConfiguration; | |
| 7 | +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; | |
| 8 | + | |
| 9 | +import java.lang.reflect.Field; | |
| 10 | + | |
| 11 | +/** | |
| 12 | + * RabbitMQ 消息队列配置类 | |
| 13 | + * | |
| 14 | + * @author 芋道源码 | |
| 15 | + */ | |
| 16 | +@AutoConfiguration | |
| 17 | +@Slf4j | |
| 18 | +@ConditionalOnClass(name = "org.springframework.amqp.rabbit.core.RabbitTemplate") | |
| 19 | +public class YudaoRabbitMQAutoConfiguration { | |
| 20 | + | |
| 21 | + static { | |
| 22 | + // 强制设置 SerializationUtils 的 TRUST_ALL 为 true,避免 RabbitMQ Consumer 反序列化消息报错 | |
| 23 | + // 为什么不通过设置 spring.amqp.deserialization.trust.all 呢?因为可能在 SerializationUtils static 初始化后 | |
| 24 | + Field trustAllField = ReflectUtil.getField(SerializationUtils.class, "TRUST_ALL"); | |
| 25 | + ReflectUtil.removeFinalModify(trustAllField); | |
| 26 | + ReflectUtil.setFieldValue(SerializationUtils.class, trustAllField, true); | |
| 27 | + } | |
| 28 | + | |
| 29 | +} | ... | ... |
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/rabbitmq/core/package-info.java
0 → 100644
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/rabbitmq/package-info.java
0 → 100644
yudao-framework/yudao-spring-boot-starter-mq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports