1. 接入 spring cloud stream,支持多租户
2. 弱化 spring cloud dubbo 集成,可通过加入依赖自动实现
This commit is contained in:
parent
4807547d73
commit
4381d938be
@ -29,6 +29,8 @@
|
|||||||
<mybatis-plus-generator.version>3.5.2</mybatis-plus-generator.version>
|
<mybatis-plus-generator.version>3.5.2</mybatis-plus-generator.version>
|
||||||
<dynamic-datasource.version>3.5.0</dynamic-datasource.version>
|
<dynamic-datasource.version>3.5.0</dynamic-datasource.version>
|
||||||
<redisson.version>3.17.3</redisson.version>
|
<redisson.version>3.17.3</redisson.version>
|
||||||
|
<!-- RPC 相关 -->
|
||||||
|
<dubbo.version>2.7.15</dubbo.version>
|
||||||
<!-- Config 配置中心相关 -->
|
<!-- Config 配置中心相关 -->
|
||||||
<apollo.version>1.9.2</apollo.version>
|
<apollo.version>1.9.2</apollo.version>
|
||||||
<!-- Job 定时任务相关 -->
|
<!-- Job 定时任务相关 -->
|
||||||
@ -233,6 +235,11 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- RPC 远程调用相关 -->
|
<!-- RPC 远程调用相关 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.dubbo</groupId>
|
||||||
|
<artifactId>dubbo-common</artifactId> <!-- 兜底,保证在不引入 spring-cloud-starter-dubbo 时,注解等不报错 -->
|
||||||
|
<version>${dubbo.version}</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.iocoder.cloud</groupId>
|
<groupId>cn.iocoder.cloud</groupId>
|
||||||
<artifactId>yudao-spring-boot-starter-rpc</artifactId>
|
<artifactId>yudao-spring-boot-starter-rpc</artifactId>
|
||||||
|
@ -8,7 +8,8 @@ import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnoreAspect;
|
|||||||
import cn.iocoder.yudao.framework.tenant.core.db.TenantDatabaseInterceptor;
|
import cn.iocoder.yudao.framework.tenant.core.db.TenantDatabaseInterceptor;
|
||||||
import cn.iocoder.yudao.framework.tenant.core.job.TenantJob;
|
import cn.iocoder.yudao.framework.tenant.core.job.TenantJob;
|
||||||
import cn.iocoder.yudao.framework.tenant.core.job.TenantJobHandlerDecorator;
|
import cn.iocoder.yudao.framework.tenant.core.job.TenantJobHandlerDecorator;
|
||||||
import cn.iocoder.yudao.framework.tenant.core.mq.TenantRedisMessageInterceptor;
|
import cn.iocoder.yudao.framework.tenant.core.mq.TenantChannelInterceptor;
|
||||||
|
import cn.iocoder.yudao.framework.tenant.core.mq.TenantFunctionAroundWrapper;
|
||||||
import cn.iocoder.yudao.framework.tenant.core.security.TenantSecurityWebFilter;
|
import cn.iocoder.yudao.framework.tenant.core.security.TenantSecurityWebFilter;
|
||||||
import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService;
|
import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService;
|
||||||
import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkServiceImpl;
|
import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkServiceImpl;
|
||||||
@ -23,8 +24,10 @@ import org.springframework.beans.factory.config.BeanPostProcessor;
|
|||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||||
|
import org.springframework.cloud.function.context.catalog.FunctionAroundWrapper;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.integration.config.GlobalChannelInterceptor;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
@ConditionalOnProperty(prefix = "yudao.tenant", value = "enable", matchIfMissing = true) // 允许使用 yudao.tenant.enable=false 禁用多租户
|
@ConditionalOnProperty(prefix = "yudao.tenant", value = "enable", matchIfMissing = true) // 允许使用 yudao.tenant.enable=false 禁用多租户
|
||||||
@ -82,14 +85,19 @@ public class YudaoTenantAutoConfiguration {
|
|||||||
// ========== MQ ==========
|
// ========== MQ ==========
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public TenantRedisMessageInterceptor tenantRedisMessageInterceptor() {
|
@GlobalChannelInterceptor // 必须添加在方法上,否则无法生效
|
||||||
return new TenantRedisMessageInterceptor();
|
public TenantChannelInterceptor tenantChannelInterceptor() {
|
||||||
|
return new TenantChannelInterceptor();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public FunctionAroundWrapper functionAroundWrapper() {
|
||||||
|
return new TenantFunctionAroundWrapper();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== Job ==========
|
// ========== Job ==========
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
|
|
||||||
public BeanPostProcessor jobHandlerBeanPostProcessor(TenantFrameworkService tenantFrameworkService) {
|
public BeanPostProcessor jobHandlerBeanPostProcessor(TenantFrameworkService tenantFrameworkService) {
|
||||||
return new BeanPostProcessor() {
|
return new BeanPostProcessor() {
|
||||||
|
|
||||||
|
@ -0,0 +1,32 @@
|
|||||||
|
package cn.iocoder.yudao.framework.tenant.core.mq;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.ReflectUtil;
|
||||||
|
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
|
||||||
|
import org.springframework.messaging.Message;
|
||||||
|
import org.springframework.messaging.MessageChannel;
|
||||||
|
import org.springframework.messaging.support.ChannelInterceptor;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 多租户的 {@link ChannelInterceptor} 实现类
|
||||||
|
* 发送消息时,设置租户编号到 Header 上
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
public class TenantChannelInterceptor implements ChannelInterceptor {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@SuppressWarnings({"unchecked", "NullableProblems"})
|
||||||
|
public Message<?> preSend(Message<?> message, MessageChannel channel) {
|
||||||
|
Long tenantId = TenantContextHolder.getTenantId();
|
||||||
|
if (tenantId != null) {
|
||||||
|
Map<String, Object> headers = (Map<String, Object>) ReflectUtil.getFieldValue(message.getHeaders(), "headers");
|
||||||
|
headers.put(HEADER_TENANT_ID, tenantId);
|
||||||
|
}
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
package cn.iocoder.yudao.framework.tenant.core.mq;
|
||||||
|
|
||||||
|
import cn.hutool.core.map.MapUtil;
|
||||||
|
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
|
||||||
|
import org.springframework.cloud.function.context.catalog.FunctionAroundWrapper;
|
||||||
|
import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry;
|
||||||
|
import org.springframework.messaging.Message;
|
||||||
|
|
||||||
|
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 多租户 FunctionAroundWrapper 实现类
|
||||||
|
* 消费消息时,设置租户编号到 Context 上
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
public class TenantFunctionAroundWrapper extends FunctionAroundWrapper {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Object doApply(Object input, SimpleFunctionRegistry.FunctionInvocationWrapper targetFunction) {
|
||||||
|
// 如果不是 MQ 消息,则直接跳过
|
||||||
|
if (!(input instanceof Message)) {
|
||||||
|
return targetFunction.apply(input);
|
||||||
|
}
|
||||||
|
// 如果没有多租户,则直接跳过
|
||||||
|
Message<?> message = (Message<?>) input;
|
||||||
|
Long tenantId = MapUtil.getLong(message.getHeaders(), HEADER_TENANT_ID);
|
||||||
|
if (tenantId == null) {
|
||||||
|
return targetFunction.apply(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有多租户,则使用多租户上下文
|
||||||
|
return TenantUtils.execute(tenantId, () -> targetFunction.apply(input));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,42 +0,0 @@
|
|||||||
package cn.iocoder.yudao.framework.tenant.core.mq;
|
|
||||||
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
|
||||||
import cn.iocoder.yudao.framework.mq.core.interceptor.RedisMessageInterceptor;
|
|
||||||
import cn.iocoder.yudao.framework.mq.core.message.AbstractRedisMessage;
|
|
||||||
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 多租户 {@link AbstractRedisMessage} 拦截器
|
|
||||||
*
|
|
||||||
* 1. Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中
|
|
||||||
* 2. Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中
|
|
||||||
*
|
|
||||||
* @author 芋道源码
|
|
||||||
*/
|
|
||||||
public class TenantRedisMessageInterceptor implements RedisMessageInterceptor {
|
|
||||||
|
|
||||||
private static final String HEADER_TENANT_ID = "tenant-id";
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void sendMessageBefore(AbstractRedisMessage message) {
|
|
||||||
Long tenantId = TenantContextHolder.getTenantId();
|
|
||||||
if (tenantId != null) {
|
|
||||||
message.addHeader(HEADER_TENANT_ID, tenantId.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void consumeMessageBefore(AbstractRedisMessage message) {
|
|
||||||
String tenantIdStr = message.getHeader(HEADER_TENANT_ID);
|
|
||||||
if (StrUtil.isNotEmpty(tenantIdStr)) {
|
|
||||||
TenantContextHolder.setTenantId(Long.valueOf(tenantIdStr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void consumeMessageAfter(AbstractRedisMessage message) {
|
|
||||||
// 注意,Consumer 是一个逻辑的入口,所以不考虑原本上下文就存在租户编号的情况
|
|
||||||
TenantContextHolder.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -2,6 +2,8 @@ package cn.iocoder.yudao.framework.tenant.core.util;
|
|||||||
|
|
||||||
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
|
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
|
||||||
|
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 多租户 Util
|
* 多租户 Util
|
||||||
*
|
*
|
||||||
@ -32,4 +34,27 @@ public class TenantUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用指定租户,执行对应的逻辑
|
||||||
|
*
|
||||||
|
* 注意,如果当前是忽略租户的情况下,会被强制设置成不忽略租户
|
||||||
|
* 当然,执行完成后,还是会恢复回去
|
||||||
|
*
|
||||||
|
* @param tenantId 租户编号
|
||||||
|
* @param supplier 逻辑
|
||||||
|
*/
|
||||||
|
public static <T> T execute(Long tenantId, Supplier<T> supplier) {
|
||||||
|
Long oldTenantId = TenantContextHolder.getTenantId();
|
||||||
|
Boolean oldIgnore = TenantContextHolder.isIgnore();
|
||||||
|
try {
|
||||||
|
TenantContextHolder.setTenantId(tenantId);
|
||||||
|
TenantContextHolder.setIgnore(false);
|
||||||
|
// 执行逻辑
|
||||||
|
return supplier.get();
|
||||||
|
} finally {
|
||||||
|
TenantContextHolder.setTenantId(oldTenantId);
|
||||||
|
TenantContextHolder.setIgnore(oldIgnore);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -20,12 +20,6 @@
|
|||||||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<!-- DB 相关 -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>cn.iocoder.cloud</groupId>
|
|
||||||
<artifactId>yudao-spring-boot-starter-redis</artifactId>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- MQ 相关 -->
|
<!-- MQ 相关 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.alibaba.cloud</groupId>
|
<groupId>com.alibaba.cloud</groupId>
|
||||||
|
@ -1,15 +1,10 @@
|
|||||||
package cn.iocoder.yudao.framework.mq.config;
|
package cn.iocoder.yudao.framework.mq.config;
|
||||||
|
|
||||||
import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate;
|
|
||||||
import cn.iocoder.yudao.framework.mq.core.interceptor.RedisMessageInterceptor;
|
|
||||||
import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
|
|
||||||
import com.alibaba.cloud.stream.binder.rocketmq.convert.RocketMQMessageConverter;
|
import com.alibaba.cloud.stream.binder.rocketmq.convert.RocketMQMessageConverter;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
|
||||||
import org.springframework.messaging.converter.*;
|
import org.springframework.messaging.converter.*;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -21,19 +16,8 @@ import java.util.List;
|
|||||||
* @author 芋道源码
|
* @author 芋道源码
|
||||||
*/
|
*/
|
||||||
@Configuration
|
@Configuration
|
||||||
@AutoConfigureAfter(YudaoRedisAutoConfiguration.class)
|
|
||||||
@Slf4j
|
|
||||||
public class YudaoMQAutoConfiguration {
|
public class YudaoMQAutoConfiguration {
|
||||||
|
|
||||||
@Bean
|
|
||||||
public RedisMQTemplate redisMQTemplate(StringRedisTemplate redisTemplate,
|
|
||||||
List<RedisMessageInterceptor> interceptors) {
|
|
||||||
RedisMQTemplate redisMQTemplate = new RedisMQTemplate(redisTemplate);
|
|
||||||
// 添加拦截器
|
|
||||||
interceptors.forEach(redisMQTemplate::addInterceptor);
|
|
||||||
return redisMQTemplate;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 覆盖 {@link RocketMQMessageConverter} 的配置,去掉 fastjson 的转换器,解决不兼容的问题
|
* 覆盖 {@link RocketMQMessageConverter} 的配置,去掉 fastjson 的转换器,解决不兼容的问题
|
||||||
*/
|
*/
|
||||||
|
@ -1,87 +0,0 @@
|
|||||||
package cn.iocoder.yudao.framework.mq.core;
|
|
||||||
|
|
||||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
|
||||||
import cn.iocoder.yudao.framework.mq.core.interceptor.RedisMessageInterceptor;
|
|
||||||
import cn.iocoder.yudao.framework.mq.core.message.AbstractRedisMessage;
|
|
||||||
import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessage;
|
|
||||||
import cn.iocoder.yudao.framework.mq.core.stream.AbstractStreamMessage;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Getter;
|
|
||||||
import org.springframework.data.redis.connection.stream.RecordId;
|
|
||||||
import org.springframework.data.redis.connection.stream.StreamRecords;
|
|
||||||
import org.springframework.data.redis.core.RedisTemplate;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Redis MQ 操作模板类
|
|
||||||
*
|
|
||||||
* @author 芋道源码
|
|
||||||
*/
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class RedisMQTemplate {
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
private final RedisTemplate<String, ?> redisTemplate;
|
|
||||||
/**
|
|
||||||
* 拦截器数组
|
|
||||||
*/
|
|
||||||
@Getter
|
|
||||||
private final List<RedisMessageInterceptor> interceptors = new ArrayList<>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 发送 Redis 消息,基于 Redis pub/sub 实现
|
|
||||||
*
|
|
||||||
* @param message 消息
|
|
||||||
*/
|
|
||||||
public <T extends AbstractChannelMessage> void send(T message) {
|
|
||||||
try {
|
|
||||||
sendMessageBefore(message);
|
|
||||||
// 发送消息
|
|
||||||
redisTemplate.convertAndSend(message.getChannel(), JsonUtils.toJsonString(message));
|
|
||||||
} finally {
|
|
||||||
sendMessageAfter(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 发送 Redis 消息,基于 Redis Stream 实现
|
|
||||||
*
|
|
||||||
* @param message 消息
|
|
||||||
* @return 消息记录的编号对象
|
|
||||||
*/
|
|
||||||
public <T extends AbstractStreamMessage> RecordId send(T message) {
|
|
||||||
try {
|
|
||||||
sendMessageBefore(message);
|
|
||||||
// 发送消息
|
|
||||||
return redisTemplate.opsForStream().add(StreamRecords.newRecord()
|
|
||||||
.ofObject(JsonUtils.toJsonString(message)) // 设置内容
|
|
||||||
.withStreamKey(message.getStreamKey())); // 设置 stream key
|
|
||||||
} finally {
|
|
||||||
sendMessageAfter(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 添加拦截器
|
|
||||||
*
|
|
||||||
* @param interceptor 拦截器
|
|
||||||
*/
|
|
||||||
public void addInterceptor(RedisMessageInterceptor interceptor) {
|
|
||||||
interceptors.add(interceptor);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sendMessageBefore(AbstractRedisMessage message) {
|
|
||||||
// 正序
|
|
||||||
interceptors.forEach(interceptor -> interceptor.sendMessageBefore(message));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void sendMessageAfter(AbstractRedisMessage message) {
|
|
||||||
// 倒序
|
|
||||||
for (int i = interceptors.size() - 1; i >= 0; i--) {
|
|
||||||
interceptors.get(i).sendMessageAfter(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
package cn.iocoder.yudao.framework.mq.core.interceptor;
|
|
||||||
|
|
||||||
import cn.iocoder.yudao.framework.mq.core.message.AbstractRedisMessage;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@link AbstractRedisMessage} 消息拦截器
|
|
||||||
* 通过拦截器,作为插件机制,实现拓展。
|
|
||||||
* 例如说,多租户场景下的 MQ 消息处理
|
|
||||||
*
|
|
||||||
* @author 芋道源码
|
|
||||||
*/
|
|
||||||
public interface RedisMessageInterceptor {
|
|
||||||
|
|
||||||
default void sendMessageBefore(AbstractRedisMessage message) {
|
|
||||||
}
|
|
||||||
|
|
||||||
default void sendMessageAfter(AbstractRedisMessage message) {
|
|
||||||
}
|
|
||||||
|
|
||||||
default void consumeMessageBefore(AbstractRedisMessage message) {
|
|
||||||
}
|
|
||||||
|
|
||||||
default void consumeMessageAfter(AbstractRedisMessage message) {
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
package cn.iocoder.yudao.framework.mq.core.message;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Redis 消息抽象基类
|
|
||||||
*
|
|
||||||
* @author 芋道源码
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
public abstract class AbstractRedisMessage {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 头
|
|
||||||
*/
|
|
||||||
private Map<String, String> headers = new HashMap<>();
|
|
||||||
|
|
||||||
public String getHeader(String key) {
|
|
||||||
return headers.get(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addHeader(String key, String value) {
|
|
||||||
headers.put(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,4 @@
|
|||||||
|
/**
|
||||||
|
* TODO 芋艿,后续删除,临时占位
|
||||||
|
*/
|
||||||
|
package cn.iocoder.yudao.framework.mq.core;
|
@ -1,21 +0,0 @@
|
|||||||
package cn.iocoder.yudao.framework.mq.core.pubsub;
|
|
||||||
|
|
||||||
import cn.iocoder.yudao.framework.mq.core.message.AbstractRedisMessage;
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Redis Channel Message 抽象类
|
|
||||||
*
|
|
||||||
* @author 芋道源码
|
|
||||||
*/
|
|
||||||
public abstract class AbstractChannelMessage extends AbstractRedisMessage {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获得 Redis Channel
|
|
||||||
*
|
|
||||||
* @return Channel
|
|
||||||
*/
|
|
||||||
@JsonIgnore // 避免序列化。原因是,Redis 发布 Channel 消息的时候,已经会指定。
|
|
||||||
public abstract String getChannel();
|
|
||||||
|
|
||||||
}
|
|
@ -1,103 +0,0 @@
|
|||||||
package cn.iocoder.yudao.framework.mq.core.pubsub;
|
|
||||||
|
|
||||||
import cn.hutool.core.util.TypeUtil;
|
|
||||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
|
||||||
import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate;
|
|
||||||
import cn.iocoder.yudao.framework.mq.core.interceptor.RedisMessageInterceptor;
|
|
||||||
import cn.iocoder.yudao.framework.mq.core.message.AbstractRedisMessage;
|
|
||||||
import lombok.Setter;
|
|
||||||
import lombok.SneakyThrows;
|
|
||||||
import org.springframework.data.redis.connection.Message;
|
|
||||||
import org.springframework.data.redis.connection.MessageListener;
|
|
||||||
|
|
||||||
import java.lang.reflect.Type;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Redis Pub/Sub 监听器抽象类,用于实现广播消费
|
|
||||||
*
|
|
||||||
* @param <T> 消息类型。一定要填写噢,不然会报错
|
|
||||||
*
|
|
||||||
* @author 芋道源码
|
|
||||||
*/
|
|
||||||
public abstract class AbstractChannelMessageListener<T extends AbstractChannelMessage> implements MessageListener {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 消息类型
|
|
||||||
*/
|
|
||||||
private final Class<T> messageType;
|
|
||||||
/**
|
|
||||||
* Redis Channel
|
|
||||||
*/
|
|
||||||
private final String channel;
|
|
||||||
/**
|
|
||||||
* RedisMQTemplate
|
|
||||||
*/
|
|
||||||
@Setter
|
|
||||||
private RedisMQTemplate redisMQTemplate;
|
|
||||||
|
|
||||||
@SneakyThrows
|
|
||||||
protected AbstractChannelMessageListener() {
|
|
||||||
this.messageType = getMessageClass();
|
|
||||||
this.channel = messageType.newInstance().getChannel();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获得 Sub 订阅的 Redis Channel 通道
|
|
||||||
*
|
|
||||||
* @return channel
|
|
||||||
*/
|
|
||||||
public final String getChannel() {
|
|
||||||
return channel;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public final void onMessage(Message message, byte[] bytes) {
|
|
||||||
T messageObj = JsonUtils.parseObject(message.getBody(), messageType);
|
|
||||||
try {
|
|
||||||
consumeMessageBefore(messageObj);
|
|
||||||
// 消费消息
|
|
||||||
this.onMessage(messageObj);
|
|
||||||
} finally {
|
|
||||||
consumeMessageAfter(messageObj);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理消息
|
|
||||||
*
|
|
||||||
* @param message 消息
|
|
||||||
*/
|
|
||||||
public abstract void onMessage(T message);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 通过解析类上的泛型,获得消息类型
|
|
||||||
*
|
|
||||||
* @return 消息类型
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private Class<T> getMessageClass() {
|
|
||||||
Type type = TypeUtil.getTypeArgument(getClass(), 0);
|
|
||||||
if (type == null) {
|
|
||||||
throw new IllegalStateException(String.format("类型(%s) 需要设置消息类型", getClass().getName()));
|
|
||||||
}
|
|
||||||
return (Class<T>) type;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void consumeMessageBefore(AbstractRedisMessage message) {
|
|
||||||
assert redisMQTemplate != null;
|
|
||||||
List<RedisMessageInterceptor> interceptors = redisMQTemplate.getInterceptors();
|
|
||||||
// 正序
|
|
||||||
interceptors.forEach(interceptor -> interceptor.consumeMessageBefore(message));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void consumeMessageAfter(AbstractRedisMessage message) {
|
|
||||||
assert redisMQTemplate != null;
|
|
||||||
List<RedisMessageInterceptor> interceptors = redisMQTemplate.getInterceptors();
|
|
||||||
// 倒序
|
|
||||||
for (int i = interceptors.size() - 1; i >= 0; i--) {
|
|
||||||
interceptors.get(i).consumeMessageAfter(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
package cn.iocoder.yudao.framework.mq.core.stream;
|
|
||||||
|
|
||||||
import cn.iocoder.yudao.framework.mq.core.message.AbstractRedisMessage;
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Redis Stream Message 抽象类
|
|
||||||
*
|
|
||||||
* @author 芋道源码
|
|
||||||
*/
|
|
||||||
public abstract class AbstractStreamMessage extends AbstractRedisMessage {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获得 Redis Stream Key
|
|
||||||
*
|
|
||||||
* @return Channel
|
|
||||||
*/
|
|
||||||
@JsonIgnore // 避免序列化
|
|
||||||
public abstract String getStreamKey();
|
|
||||||
|
|
||||||
}
|
|
@ -1,113 +0,0 @@
|
|||||||
package cn.iocoder.yudao.framework.mq.core.stream;
|
|
||||||
|
|
||||||
import cn.hutool.core.util.TypeUtil;
|
|
||||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
|
||||||
import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate;
|
|
||||||
import cn.iocoder.yudao.framework.mq.core.interceptor.RedisMessageInterceptor;
|
|
||||||
import cn.iocoder.yudao.framework.mq.core.message.AbstractRedisMessage;
|
|
||||||
import lombok.Getter;
|
|
||||||
import lombok.Setter;
|
|
||||||
import lombok.SneakyThrows;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.data.redis.connection.stream.ObjectRecord;
|
|
||||||
import org.springframework.data.redis.stream.StreamListener;
|
|
||||||
|
|
||||||
import java.lang.reflect.Type;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Redis Stream 监听器抽象类,用于实现集群消费
|
|
||||||
*
|
|
||||||
* @param <T> 消息类型。一定要填写噢,不然会报错
|
|
||||||
*
|
|
||||||
* @author 芋道源码
|
|
||||||
*/
|
|
||||||
public abstract class AbstractStreamMessageListener<T extends AbstractStreamMessage>
|
|
||||||
implements StreamListener<String, ObjectRecord<String, String>> {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 消息类型
|
|
||||||
*/
|
|
||||||
private final Class<T> messageType;
|
|
||||||
/**
|
|
||||||
* Redis Channel
|
|
||||||
*/
|
|
||||||
@Getter
|
|
||||||
private final String streamKey;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Redis 消费者分组,默认使用 spring.application.name 名字
|
|
||||||
*/
|
|
||||||
@Value("${spring.application.name}")
|
|
||||||
@Getter
|
|
||||||
private String group;
|
|
||||||
/**
|
|
||||||
* RedisMQTemplate
|
|
||||||
*/
|
|
||||||
@Setter
|
|
||||||
private RedisMQTemplate redisMQTemplate;
|
|
||||||
|
|
||||||
@SneakyThrows
|
|
||||||
protected AbstractStreamMessageListener() {
|
|
||||||
this.messageType = getMessageClass();
|
|
||||||
this.streamKey = messageType.newInstance().getStreamKey();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onMessage(ObjectRecord<String, String> message) {
|
|
||||||
// 消费消息
|
|
||||||
T messageObj = JsonUtils.parseObject(message.getValue(), messageType);
|
|
||||||
try {
|
|
||||||
consumeMessageBefore(messageObj);
|
|
||||||
// 消费消息
|
|
||||||
this.onMessage(messageObj);
|
|
||||||
// ack 消息消费完成
|
|
||||||
redisMQTemplate.getRedisTemplate().opsForStream().acknowledge(group, message);
|
|
||||||
// TODO 芋艿:需要额外考虑以下几个点:
|
|
||||||
// 1. 处理异常的情况
|
|
||||||
// 2. 发送日志;以及事务的结合
|
|
||||||
// 3. 消费日志;以及通用的幂等性
|
|
||||||
// 4. 消费失败的重试,https://zhuanlan.zhihu.com/p/60501638
|
|
||||||
} finally {
|
|
||||||
consumeMessageAfter(messageObj);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理消息
|
|
||||||
*
|
|
||||||
* @param message 消息
|
|
||||||
*/
|
|
||||||
public abstract void onMessage(T message);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 通过解析类上的泛型,获得消息类型
|
|
||||||
*
|
|
||||||
* @return 消息类型
|
|
||||||
*/
|
|
||||||
@SuppressWarnings("unchecked")
|
|
||||||
private Class<T> getMessageClass() {
|
|
||||||
Type type = TypeUtil.getTypeArgument(getClass(), 0);
|
|
||||||
if (type == null) {
|
|
||||||
throw new IllegalStateException(String.format("类型(%s) 需要设置消息类型", getClass().getName()));
|
|
||||||
}
|
|
||||||
return (Class<T>) type;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void consumeMessageBefore(AbstractRedisMessage message) {
|
|
||||||
assert redisMQTemplate != null;
|
|
||||||
List<RedisMessageInterceptor> interceptors = redisMQTemplate.getInterceptors();
|
|
||||||
// 正序
|
|
||||||
interceptors.forEach(interceptor -> interceptor.consumeMessageBefore(message));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void consumeMessageAfter(AbstractRedisMessage message) {
|
|
||||||
assert redisMQTemplate != null;
|
|
||||||
List<RedisMessageInterceptor> interceptors = redisMQTemplate.getInterceptors();
|
|
||||||
// 倒序
|
|
||||||
for (int i = interceptors.size() - 1; i >= 0; i--) {
|
|
||||||
interceptors.get(i).consumeMessageAfter(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -35,8 +35,13 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.alibaba.cloud</groupId>
|
<groupId>org.apache.dubbo</groupId>
|
||||||
<artifactId>spring-cloud-starter-dubbo</artifactId>
|
<artifactId>dubbo-common</artifactId> <!-- 兜底,保证在不引入 spring-cloud-starter-dubbo 时,注解等不报错 -->
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- -->
|
||||||
|
<!-- <dependency>-->
|
||||||
|
<!-- <groupId>com.alibaba.cloud</groupId>-->
|
||||||
|
<!-- <artifactId>spring-cloud-starter-dubbo</artifactId>-->
|
||||||
|
<!-- </dependency>-->
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
spring:
|
spring:
|
||||||
main:
|
main:
|
||||||
allow-circular-references: true # 允许循环依赖,因为项目是三层架构,无法避免这个情况。
|
allow-circular-references: true # 允许循环依赖,因为项目是三层架构,无法避免这个情况。
|
||||||
|
allow-bean-definition-overriding: true # 允许 Bean 覆盖,例如说 Dubbo 或者 Feign 等会存在重复定义的服务
|
||||||
|
|
||||||
# Servlet 配置
|
# Servlet 配置
|
||||||
servlet:
|
servlet:
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
package cn.iocoder.yudao.module.system.mq.consumer.mail;
|
package cn.iocoder.yudao.module.system.mq.consumer.mail;
|
||||||
|
|
||||||
import cn.iocoder.yudao.framework.mq.core.stream.AbstractStreamMessageListener;
|
|
||||||
import cn.iocoder.yudao.module.system.mq.message.mail.MailSendMessage;
|
import cn.iocoder.yudao.module.system.mq.message.mail.MailSendMessage;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
package cn.iocoder.yudao.module.system.mq.message.mail;
|
package cn.iocoder.yudao.module.system.mq.message.mail;
|
||||||
|
|
||||||
import cn.iocoder.yudao.framework.mq.core.stream.AbstractStreamMessage;
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.EqualsAndHashCode;
|
|
||||||
|
|
||||||
import javax.validation.constraints.NotNull;
|
import javax.validation.constraints.NotNull;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@ -39,5 +37,4 @@ public class MailSendMessage {
|
|||||||
*/
|
*/
|
||||||
private Integer userType;
|
private Integer userType;
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
spring:
|
spring:
|
||||||
main:
|
main:
|
||||||
allow-circular-references: true # 允许循环依赖,因为项目是三层架构,无法避免这个情况。
|
allow-circular-references: true # 允许循环依赖,因为项目是三层架构,无法避免这个情况。
|
||||||
|
allow-bean-definition-overriding: true # 允许 Bean 覆盖,例如说 Dubbo 或者 Feign 等会存在重复定义的服务
|
||||||
|
|
||||||
# Servlet 配置
|
# Servlet 配置
|
||||||
servlet:
|
servlet:
|
||||||
|
Loading…
Reference in New Issue
Block a user