1. 接入 spring cloud stream,支持多租户

2. 弱化 spring cloud dubbo 集成,可通过加入依赖自动实现
This commit is contained in:
YunaiV 2022-06-22 23:59:19 +08:00
parent 4807547d73
commit 4381d938be
21 changed files with 125 additions and 474 deletions

View File

@ -29,6 +29,8 @@
<mybatis-plus-generator.version>3.5.2</mybatis-plus-generator.version>
<dynamic-datasource.version>3.5.0</dynamic-datasource.version>
<redisson.version>3.17.3</redisson.version>
<!-- RPC 相关 -->
<dubbo.version>2.7.15</dubbo.version>
<!-- Config 配置中心相关 -->
<apollo.version>1.9.2</apollo.version>
<!-- Job 定时任务相关 -->
@ -233,6 +235,11 @@
</dependency>
<!-- RPC 远程调用相关 -->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-common</artifactId> <!-- 兜底,保证在不引入 spring-cloud-starter-dubbo 时,注解等不报错 -->
<version>${dubbo.version}</version>
</dependency>
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-rpc</artifactId>

View File

@ -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.job.TenantJob;
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.service.TenantFrameworkService;
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.context.properties.EnableConfigurationProperties;
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.Configuration;
import org.springframework.integration.config.GlobalChannelInterceptor;
@Configuration
@ConditionalOnProperty(prefix = "yudao.tenant", value = "enable", matchIfMissing = true) // 允许使用 yudao.tenant.enable=false 禁用多租户
@ -82,14 +85,19 @@ public class YudaoTenantAutoConfiguration {
// ========== MQ ==========
@Bean
public TenantRedisMessageInterceptor tenantRedisMessageInterceptor() {
return new TenantRedisMessageInterceptor();
@GlobalChannelInterceptor // 必须添加在方法上否则无法生效
public TenantChannelInterceptor tenantChannelInterceptor() {
return new TenantChannelInterceptor();
}
@Bean
public FunctionAroundWrapper functionAroundWrapper() {
return new TenantFunctionAroundWrapper();
}
// ========== Job ==========
@Bean
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
public BeanPostProcessor jobHandlerBeanPostProcessor(TenantFrameworkService tenantFrameworkService) {
return new BeanPostProcessor() {

View File

@ -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;
}
}

View File

@ -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));
}
}

View File

@ -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();
}
}

View File

@ -2,6 +2,8 @@ package cn.iocoder.yudao.framework.tenant.core.util;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import java.util.function.Supplier;
/**
* 多租户 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);
}
}
}

View File

@ -20,12 +20,6 @@
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<dependencies>
<!-- DB 相关 -->
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-redis</artifactId>
</dependency>
<!-- MQ 相关 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>

View File

@ -1,15 +1,10 @@
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 lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.messaging.converter.*;
import java.util.ArrayList;
@ -21,19 +16,8 @@ import java.util.List;
* @author 芋道源码
*/
@Configuration
@AutoConfigureAfter(YudaoRedisAutoConfiguration.class)
@Slf4j
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 的转换器解决不兼容的问题
*/

View File

@ -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);
}
}
}

View File

@ -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) {
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,4 @@
/**
* TODO 芋艿后续删除临时占位
*/
package cn.iocoder.yudao.framework.mq.core;

View File

@ -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();
}

View File

@ -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);
}
}
}

View File

@ -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();
}

View File

@ -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);
}
}
}

View File

@ -35,8 +35,13 @@
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-dubbo</artifactId>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-common</artifactId> <!-- 兜底,保证在不引入 spring-cloud-starter-dubbo 时,注解等不报错 -->
</dependency>
<!-- -->
<!-- <dependency>-->
<!-- <groupId>com.alibaba.cloud</groupId>-->
<!-- <artifactId>spring-cloud-starter-dubbo</artifactId>-->
<!-- </dependency>-->
</dependencies>
</project>

View File

@ -1,6 +1,7 @@
spring:
main:
allow-circular-references: true # 允许循环依赖,因为项目是三层架构,无法避免这个情况。
allow-bean-definition-overriding: true # 允许 Bean 覆盖,例如说 Dubbo 或者 Feign 等会存在重复定义的服务
# Servlet 配置
servlet:

View File

@ -1,6 +1,5 @@
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 lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

View File

@ -1,8 +1,6 @@
package cn.iocoder.yudao.module.system.mq.message.mail;
import cn.iocoder.yudao.framework.mq.core.stream.AbstractStreamMessage;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotNull;
import java.util.Map;
@ -39,5 +37,4 @@ public class MailSendMessage {
*/
private Integer userType;
}

View File

@ -1,6 +1,7 @@
spring:
main:
allow-circular-references: true # 允许循环依赖,因为项目是三层架构,无法避免这个情况。
allow-bean-definition-overriding: true # 允许 Bean 覆盖,例如说 Dubbo 或者 Feign 等会存在重复定义的服务
# Servlet 配置
servlet: