Prechádzať zdrojové kódy

feat: 发布1.5.0

* chore: 升级 Boot 版本 && feat: 将字段注入改为构造器注入

* feat: 新增多租户 starter

* feat: 用户上下文、租户上下文变更

* feat: 多租户、表级多租户支持

* feat: 抛出异常支持填充模板参数 (#6)

* feat: 基础实体类和多租户设置修改

* wip: 多租户及配置迁移

* feat: 兼容旧版本配置文件

* feat: 旧版本配置文件提示迁移

* docs: 更新注释

* feat: 多租户相关调整

* feat: web 日志记录

* style & docs: 调整代码顺序,修订注释

* chore: 升级 guava 版本

* feat: `helio-starter-rocketmq-aliyun` 的 `RocketListener` 注解新增 `consumeThreadNums`、`consumeBatchSize` 两个属性,特定需求可以单独指定

* feat: 增加 NPE 提示

* feat: 修改租户上下文读取与赋值逻辑

* feat: 升级至 dubbo3 版本,适配 API 变更

* feat: 打印日志时将公网IP也打印出来

* pref: 多租户相关代码精简优化

* feat: Web 访问日志切面

* feat: 去掉 Web 日志切面相关配置项

* feat: Web 日志切面相关配置项

* feat: 多租户相关变更

* chore: nacos-client 升级至 2.0.4

* feat: 去除时区相关,改为系统默认

* feat: 数据源级多租户

* feat: 多租户数据源 && wip: 升级 Boot 及第三方依赖版本

* feat: 数据源级多租户 & chore: 1.5.0

* feat: 抛出异常支持填充模板参数
Uncarbon 4 rokov pred
rodič
commit
2dbf81788a
69 zmenil súbory, kde vykonal 1526 pridanie a 464 odobranie
  1. 1 1
      helio-core/pom.xml
  2. 94 0
      helio-core/src/main/java/cc/uncarbon/framework/core/config/AdaptHistoryVersionAutoConfiguration.java
  3. 1 0
      helio-core/src/main/java/cc/uncarbon/framework/core/config/HelioPropertiesAutoConfiguration.java
  4. 11 16
      helio-core/src/main/java/cc/uncarbon/framework/core/constant/HelioConstant.java
  5. 6 1
      helio-core/src/main/java/cc/uncarbon/framework/core/context/TenantContext.java
  6. 60 0
      helio-core/src/main/java/cc/uncarbon/framework/core/context/TenantContextHolder.java
  7. 9 18
      helio-core/src/main/java/cc/uncarbon/framework/core/context/UserContext.java
  8. 27 47
      helio-core/src/main/java/cc/uncarbon/framework/core/context/UserContextHolder.java
  9. 20 10
      helio-core/src/main/java/cc/uncarbon/framework/core/enums/HelioBaseEnum.java
  10. 10 4
      helio-core/src/main/java/cc/uncarbon/framework/core/enums/TenantIsolateLevelEnum.java
  11. 37 2
      helio-core/src/main/java/cc/uncarbon/framework/core/exception/BusinessException.java
  12. 65 50
      helio-core/src/main/java/cc/uncarbon/framework/core/props/HelioProperties.java
  13. 2 1
      helio-core/src/main/resources/META-INF/spring.factories
  14. 1 1
      helio-starter-aop/pom.xml
  15. 6 1
      helio-starter-cloud/pom.xml
  16. 7 1
      helio-starter-crud/pom.xml
  17. 22 0
      helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/config/DynamicDataSourceAutoConfiguration.java
  18. 34 27
      helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/config/HelioMybatisPlusAutoConfiguration.java
  19. 2 8
      helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/config/InitHikariPoolAtStartupConfiguration.java
  20. 50 0
      helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/dynamicdatasource/DataSourceDefinition.java
  21. 12 0
      helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/dynamicdatasource/DataSourceDefinitionProvider.java
  22. 93 0
      helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/dynamicdatasource/HelioDynamicDataSourceRegistry.java
  23. 16 9
      helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/entity/HelioBaseEntity.java
  24. 33 0
      helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/entity/HelioNoTenantBaseEntity.java
  25. 0 1
      helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/enums/YesOrNoEnum.java
  26. 0 41
      helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/handler/HelioTenantLineHandler.java
  27. 3 3
      helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/handler/MybatisPlusAutoFillColumnHandler.java
  28. 5 1
      helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/service/HelioBaseService.java
  29. 5 1
      helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/service/impl/HelioBaseServiceImpl.java
  30. 20 0
      helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/support/TenantSupport.java
  31. 16 0
      helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/support/impl/DefaultTenantSupport.java
  32. 2 1
      helio-starter-crud/src/main/resources/META-INF/spring.factories
  33. 1 1
      helio-starter-dubbo/pom.xml
  34. 16 4
      helio-starter-dubbo/src/main/java/cc/uncarbon/framework/dubbo/filter/ConsumerContextFilter.java
  35. 69 80
      helio-starter-dubbo/src/main/java/cc/uncarbon/framework/dubbo/filter/HelioDubboExceptionFilter.java
  36. 13 2
      helio-starter-dubbo/src/main/java/cc/uncarbon/framework/dubbo/filter/ProviderContextFilter.java
  37. 1 1
      helio-starter-knife4j/pom.xml
  38. 6 6
      helio-starter-knife4j/src/main/java/cc/uncarbon/framework/knife4j/config/HelioKnife4jAutoConfiguration.java
  39. 1 1
      helio-starter-redis/pom.xml
  40. 24 18
      helio-starter-redis/src/main/java/cc/uncarbon/framework/redis/config/HelioRedisAutoConfiguration.java
  41. 3 3
      helio-starter-redis/src/main/java/cc/uncarbon/framework/redis/lock/impl/RedisDistributedLockImpl.java
  42. 1 1
      helio-starter-redis/src/main/resources/META-INF/spring.factories
  43. 1 1
      helio-starter-rocketmq-aliyun/pom.xml
  44. 19 2
      helio-starter-rocketmq-aliyun/src/main/java/cc/uncarbon/framework/rocketmq/annotation/RocketListener.java
  45. 5 7
      helio-starter-rocketmq-aliyun/src/main/java/cc/uncarbon/framework/rocketmq/config/AliyunRocketAutoConfiguration.java
  46. 15 8
      helio-starter-rocketmq-aliyun/src/main/java/cc/uncarbon/framework/rocketmq/core/factory/ConsumerPropertiesFactory.java
  47. 1 1
      helio-starter-satoken/pom.xml
  48. 22 8
      helio-starter-satoken/src/main/java/cc/uncarbon/framework/satoken/interceptor/DefaultSaTokenParseInterceptor.java
  49. 42 0
      helio-starter-satoken/src/main/java/cc/uncarbon/framework/satoken/util/IPUtil.java
  50. 1 1
      helio-starter-seata/pom.xml
  51. 35 0
      helio-starter-tenant/pom.xml
  52. 22 0
      helio-starter-tenant/src/main/java/cc/uncarbon/framework/tenant/annotation/EnableGlobalTenantDataSource.java
  53. 19 0
      helio-starter-tenant/src/main/java/cc/uncarbon/framework/tenant/annotation/IgnoreTenantDataSource.java
  54. 49 0
      helio-starter-tenant/src/main/java/cc/uncarbon/framework/tenant/config/GlobalTenantDataSourceConfiguration.java
  55. 43 0
      helio-starter-tenant/src/main/java/cc/uncarbon/framework/tenant/config/HelioTenantAutoConfiguration.java
  56. 29 0
      helio-starter-tenant/src/main/java/cc/uncarbon/framework/tenant/support/TenantDataSourceSupport.java
  57. 88 0
      helio-starter-tenant/src/main/java/cc/uncarbon/framework/tenant/support/TenantLineSupport.java
  58. 56 0
      helio-starter-tenant/src/main/java/cc/uncarbon/framework/tenant/support/TenantTableSupport.java
  59. 52 0
      helio-starter-tenant/src/main/java/cc/uncarbon/framework/tenant/tenantdatasource/GlobalTenantDataSourceAdvisor.java
  60. 48 0
      helio-starter-tenant/src/main/java/cc/uncarbon/framework/tenant/tenantdatasource/GlobalTenantDataSourceInterceptor.java
  61. 2 0
      helio-starter-tenant/src/main/resources/META-INF/spring.factories
  62. 1 1
      helio-starter-test/pom.xml
  63. 6 1
      helio-starter-web/pom.xml
  64. 114 0
      helio-starter-web/src/main/java/cc/uncarbon/framework/web/aspect/WebLoggingAspect.java
  65. 6 3
      helio-starter-web/src/main/java/cc/uncarbon/framework/web/config/JacksonExtendAutoConfiguration.java
  66. 4 2
      helio-starter-web/src/main/java/cc/uncarbon/framework/web/config/XssAutoConfiguration.java
  67. 0 55
      helio-starter-web/src/main/java/cc/uncarbon/framework/web/interceptor/DefaultTenantParseInterceptor.java
  68. 3 1
      helio-starter-web/src/main/java/cc/uncarbon/framework/web/util/IPUtil.java
  69. 38 11
      pom.xml

+ 1 - 1
helio-core/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>helio-starters</artifactId>
         <groupId>cc.uncarbon.framework</groupId>
-        <version>1.4.0</version>
+        <version>1.5.0</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <modelVersion>4.0.0</modelVersion>

+ 94 - 0
helio-core/src/main/java/cc/uncarbon/framework/core/config/AdaptHistoryVersionAutoConfiguration.java

@@ -0,0 +1,94 @@
+package cc.uncarbon.framework.core.config;
+
+import cn.hutool.core.util.StrUtil;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+import org.springframework.boot.env.OriginTrackedMapPropertySource;
+import org.springframework.context.EnvironmentAware;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.env.ConfigurableEnvironment;
+import org.springframework.core.env.Environment;
+import org.springframework.core.env.MutablePropertySources;
+import org.springframework.core.env.PropertySource;
+
+/**
+ * 兼容历史版本 自动配置类
+ *
+ * @author kong
+ * @author Uncarbon
+ */
+@Configuration
+@RequiredArgsConstructor
+public class AdaptHistoryVersionAutoConfiguration implements EnvironmentAware {
+
+    /**
+     * 旧的配置keys
+     */
+    private static final String[] oldPropertyPrefixes = {"helio.crud.tenant."};
+
+    /**
+     * 新的配置keys
+     */
+    private static final String[] newPropertyPrefixes = {"helio.tenant."};
+
+    /**
+     * 过时属性警告提示文案
+     */
+    private final Set<String> deprecatedPropertyWarnings = new LinkedHashSet<>();
+
+
+    @Override
+    public void setEnvironment(@NonNull Environment env) {
+        try {
+            ConfigurableEnvironment c = (ConfigurableEnvironment) env;
+            MutablePropertySources sources = c.getPropertySources();
+
+            Map<String, Object> newMap = new LinkedHashMap<>();
+            for (PropertySource<?> source : sources) {
+                if (source instanceof OriginTrackedMapPropertySource) {
+                    // 根据类判断是否为 SpringBoot .yml 或者 .properties 的配置
+                    Map<String, Object> bootProp = ((OriginTrackedMapPropertySource) source).getSource();
+
+                    for (String key : bootProp.keySet()) {
+                        if (key != null) {
+                            for (int i = 0; i < oldPropertyPrefixes.length; i++) {
+                                if (key.startsWith(oldPropertyPrefixes[i])) {
+                                    /*
+                                    将配置文件中所有 [helio.crud.tenant.] 开头的配置转移到 [helio.tenant.] 下
+                                    实践测试 只对 @Value 注解有效
+                                     */
+                                    String newKey = StrUtil.replace(key, oldPropertyPrefixes[i], newPropertyPrefixes[i]);
+                                    newMap.put(newKey, bootProp.get(key));
+
+                                    // 前缀相同的话,提示信息就保持一条(利用 Set 特性)
+                                    deprecatedPropertyWarnings.add(
+                                            StrUtil.format("配置文件属性前缀 {}* 已过时、不向下兼容,请转移到 {}* 下"
+                                                    , oldPropertyPrefixes[i], newPropertyPrefixes[i])
+                                    );
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+
+            // 追加到总配置里面
+            if (newMap.size() > 0) {
+                OriginTrackedMapPropertySource source = new OriginTrackedMapPropertySource(
+                        "AdaptHistoryVersionAutoConfiguration", newMap);
+                // 追加到末尾,优先级最低
+                c.getPropertySources().addLast(source);
+
+                System.err.println("\n"
+                        + StrUtil.join("\n", deprecatedPropertyWarnings)
+                        + "\n");
+            }
+        } catch (Exception e) {
+            // ignored
+        }
+    }
+}

+ 1 - 0
helio-core/src/main/java/cc/uncarbon/framework/core/config/HelioPropertiesAutoConfiguration.java

@@ -12,4 +12,5 @@ import org.springframework.context.annotation.Configuration;
 @EnableConfigurationProperties(value = {HelioProperties.class})
 @Configuration
 public class HelioPropertiesAutoConfiguration {
+
 }

+ 11 - 16
helio-core/src/main/java/cc/uncarbon/framework/core/constant/HelioConstant.java

@@ -17,18 +17,6 @@ public interface HelioConstant {
     }
 
     interface Version {
-        /**
-         * replaced with HTTP_API_VERSION_V1
-         */
-        @Deprecated
-        String APP_API_VERSION_V1 = "/api/v1";
-
-        /**
-         * replaced with HTTP_API_VERSION_V1
-         */
-        @Deprecated
-        String SAAS_API_VERSION_V1 = "/api/v1";
-
         /**
          * HTTP API 版本 v1
          */
@@ -41,7 +29,9 @@ public interface HelioConstant {
     }
 
     interface Jackson {
+        @Deprecated
         Locale LOCALE = Locale.CHINA;
+        @Deprecated
         String TIME_ZONE = "GMT+8";
         String DATE_FORMAT = "yyyy-MM-dd";
         String TIME_FORMAT = "HH:mm:ss";
@@ -49,13 +39,11 @@ public interface HelioConstant {
     }
 
     interface CRUD {
-        Long DEFAULT_TENANT_ID = 0L;
-        String DEFAULT_TENANT_NAME = "请设置租户ID";
-
         /**
          * 该租户ID为超级租户, 可以无视租户SQL拦截器
          */
-        Long PRIVILEGED_TENANT_ID = 0L;
+        @Deprecated
+        Long DEFAULT_PRIVILEGED_TENANT_ID = Tenant.DEFAULT_PRIVILEGED_TENANT_ID;
 
         /**
          * 租户ID
@@ -110,4 +98,11 @@ public interface HelioConstant {
         String UPDATE = "update";
         String DELETE = "delete";
     }
+
+    interface Tenant {
+        /**
+         * 该租户ID为超级租户, 可以无视租户SQL拦截器
+         */
+        Long DEFAULT_PRIVILEGED_TENANT_ID = 0L;
+    }
 }

+ 6 - 1
helio-core/src/main/java/cc/uncarbon/framework/core/context/TenantContext.java

@@ -10,7 +10,8 @@ import lombok.experimental.SuperBuilder;
 import java.io.Serializable;
 
 /**
- * 当前用户所属租户
+ * 当前租户上下文对象
+ *
  * @author Uncarbon
  */
 @Accessors(chain = true)
@@ -20,6 +21,10 @@ import java.io.Serializable;
 @Data
 public class TenantContext implements Serializable {
 
+    private static final long serialVersionUID = 1L;
+
+    public static final String CAMEL_NAME = "tenantContext";
+
     @ApiModelProperty(value = "租户ID")
     private Long tenantId;
 

+ 60 - 0
helio-core/src/main/java/cc/uncarbon/framework/core/context/TenantContextHolder.java

@@ -0,0 +1,60 @@
+package cc.uncarbon.framework.core.context;
+
+import com.alibaba.ttl.TransmittableThreadLocal;
+import lombok.experimental.UtilityClass;
+
+/**
+ * 租户上下文持有者类
+ *
+ * @author Uncarbon
+ */
+@UtilityClass
+public class TenantContextHolder {
+
+    private final TransmittableThreadLocal<TenantContext> THREAD_LOCAL_TENANT = new TransmittableThreadLocal<>();
+
+
+    /**
+     * 获取当前租户上下文
+     *
+     * @throws NullPointerException 链式调用时需要注意可能存在 NPE 问题
+     * @return null or 当前租户上下文
+     */
+    public TenantContext getTenantContext() throws NullPointerException {
+        return THREAD_LOCAL_TENANT.get();
+    }
+
+    /**
+     * 设置当前租户上下文
+     *
+     * @param tenantContext 新上下文,传 null 则为清除
+     */
+    public void setTenantContext(TenantContext tenantContext) {
+        if (tenantContext == null) {
+            THREAD_LOCAL_TENANT.remove();
+            return;
+        }
+
+        THREAD_LOCAL_TENANT.set(tenantContext);
+    }
+
+    /**
+     * 捷径API-取当前租户ID
+     *
+     * @return null or 当前租户ID
+     */
+    public Long getTenantId() {
+        TenantContext context = getTenantContext();
+        return context == null ? null : context.getTenantId();
+    }
+
+    /**
+     * 捷径API-取当前租户名
+     *
+     * @return null or 当前租户名
+     */
+    public String getTenantName() {
+        TenantContext context = getTenantContext();
+        return context == null ? null : context.getTenantName();
+    }
+}

+ 9 - 18
helio-core/src/main/java/cc/uncarbon/framework/core/context/UserContext.java

@@ -1,20 +1,20 @@
 package cc.uncarbon.framework.core.context;
 
-import cc.uncarbon.framework.core.constant.HelioConstant;
 import cc.uncarbon.framework.core.enums.HelioBaseEnum;
 import io.swagger.annotations.ApiModelProperty;
+import java.io.Serializable;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
 import lombok.experimental.Accessors;
 import lombok.experimental.SuperBuilder;
 
-import java.io.Serializable;
-import java.util.List;
-import java.util.Map;
-
 /**
  * 当前用户上下文对象
+ *
  * @author Uncarbon
  */
 @Accessors(chain = true)
@@ -24,7 +24,7 @@ import java.util.Map;
 @Data
 public class UserContext implements Serializable {
 
-    private static final long SerialVersionUID = 1L;
+    private static final long serialVersionUID = 1L;
 
     public static final String CAMEL_NAME = "userContext";
 
@@ -40,24 +40,15 @@ public class UserContext implements Serializable {
     @ApiModelProperty(value = "用户类型")
     private HelioBaseEnum<?> userType;
 
-    @ApiModelProperty(value = "用户对应角色ID数组(后台管理使用, 建议从小到大排序)")
-    private List<Long> rolesIds;
+    @ApiModelProperty(value = "用户拥有角色ID", notes = "后台管理使用", example = "[1, 2, 3]")
+    private Set<Long> rolesIds;
 
-    @ApiModelProperty(value = "用户对应角色串(后台管理使用)")
+    @ApiModelProperty(value = "用户拥有角色名称", notes = "后台管理使用", example = "[SuperAdmin, Admin, CEO]")
     private List<String> roles;
 
-    @ApiModelProperty(value = "用户拥有权限串(后台管理使用, 决定可见菜单及按钮)")
-    private List<String> permissions;
-
     @ApiModelProperty(value = "附加数据")
     private Map<String, Object> extraData;
 
-    @ApiModelProperty(value = "所属租户")
-    private TenantContext relationalTenant = TenantContext.builder()
-            .tenantId(HelioConstant.CRUD.DEFAULT_TENANT_ID)
-            .tenantName(HelioConstant.CRUD.DEFAULT_TENANT_NAME)
-            .build();
-
     @ApiModelProperty(value = "客户端IP地址")
     private String clientIP;
 

+ 27 - 47
helio-core/src/main/java/cc/uncarbon/framework/core/context/UserContextHolder.java

@@ -3,10 +3,8 @@ package cc.uncarbon.framework.core.context;
 import com.alibaba.ttl.TransmittableThreadLocal;
 import lombok.experimental.UtilityClass;
 
-import java.util.Optional;
-
 /**
- * 存储当前用户上下文
+ * 用户上下文持有者类
  *
  * @author Uncarbon
  */
@@ -16,43 +14,20 @@ public class UserContextHolder {
     private final TransmittableThreadLocal<UserContext> THREAD_LOCAL_USER = new TransmittableThreadLocal<>();
 
 
-    /**
-     * 是否真正存有当前用户上下文,即是否实际登录
-     *
-     * @return true or false
-     */
-    public boolean isActuallyHold() {
-        return THREAD_LOCAL_USER.get() != null;
-    }
-
     /**
      * 获取当前用户上下文
-     * 注意:即使未登录情况下,该方法也不会返回 null ,而是返回一个空对象
      *
-     * @return 当前用户上下文
+     * @throws NullPointerException 链式调用时需要注意可能存在 NPE 问题
+     * @return null or 当前用户上下文
      */
-    public UserContext getUserContext() {
-        /*
-        必须新创建,否则可能出现租户ID无法切换的BUG
-        不能用 .orElse ,即使条件不满足也会new
-         */
-        return Optional.ofNullable(THREAD_LOCAL_USER.get()).orElseGet(UserContext::new);
-    }
-
-    /**
-     * 获取 Optional 化的当前用户上下文
-     * 用于函数式编码
-     *
-     * @return 当前用户上下文 Optional
-     */
-    public Optional<UserContext> getUserContextOptional() {
-        return Optional.ofNullable(THREAD_LOCAL_USER.get());
+    public UserContext getUserContext() throws NullPointerException {
+        return THREAD_LOCAL_USER.get();
     }
 
     /**
      * 设置当前用户上下文
      *
-     * @param userContext 当前用户上下文
+     * @param userContext 新上下文,传 null 则为清除
      */
     public void setUserContext(UserContext userContext) {
         if (userContext == null) {
@@ -64,38 +39,43 @@ public class UserContextHolder {
     }
 
     /**
-     * 捷径-获取用户ID
+     * 捷径API-取当前用户ID
+     *
+     * @return null or 当前用户ID
      */
     public Long getUserId() {
-        return getUserContext().getUserId();
+        UserContext context = getUserContext();
+        return context == null ? null : context.getUserId();
     }
 
     /**
-     * 捷径-获取用户名
+     * 捷径API-取当前用户名
+     *
+     * @return null or 当前用户名
      */
     public String getUserName() {
-        return getUserContext().getUserName();
+        UserContext context = getUserContext();
+        return context == null ? null : context.getUserName();
     }
 
     /**
-     * 捷径-获取用户手机号
+     * 捷径API-取当前用户手机号
+     *
+     * @return null or 当前用户手机号
      */
     public String getUserPhoneNo() {
-        return getUserContext().getUserPhoneNo();
+        UserContext context = getUserContext();
+        return context == null ? null : context.getUserPhoneNo();
     }
 
     /**
-     * 捷径-获取所属租户
-     */
-    public TenantContext getRelationalTenant() {
-        return getUserContext().getRelationalTenant();
-    }
-
-    /**
-     * 捷径-设置所属租户
+     * 捷径API-取当前用户 IP 地址
+     *
+     * @return null or 当前用户 IP 地址
      */
-    public void setRelationalTenant(TenantContext newTenantContext) {
-        getUserContext().setRelationalTenant(newTenantContext);
+    public String getClientIP() {
+        UserContext context = getUserContext();
+        return context == null ? null : context.getClientIP();
     }
 
 }

+ 20 - 10
helio-core/src/main/java/cc/uncarbon/framework/core/enums/HelioBaseEnum.java

@@ -5,7 +5,6 @@ import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.NumberUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
-
 import java.io.Serializable;
 import java.util.Arrays;
 import java.util.Collection;
@@ -42,7 +41,7 @@ public interface HelioBaseEnum<T> extends Serializable {
     /**
      * 根据枚举的{@link HelioBaseEnum#getValue()}来查找.
      *
-     * @see this#find(Class, Predicate)
+     * @see #find(Class, Predicate)
      */
     static <T extends Enum<?> & HelioBaseEnum<?>> Optional<T> findByValue(Class<T> type, Object value) {
         return find(type, e -> e.getValue() == value || e.getValue().equals(value) || String.valueOf(e.getValue()).equalsIgnoreCase(String.valueOf(value)));
@@ -51,7 +50,7 @@ public interface HelioBaseEnum<T> extends Serializable {
     /**
      * 根据枚举的{@link HelioBaseEnum#getLabel()} 来查找.
      *
-     * @see this#find(Class, Predicate)
+     * @see #find(Class, Predicate)
      */
     static <T extends Enum<?> & HelioBaseEnum<?>> Optional<T> findByLabel(Class<T> type, String text) {
         return find(type, e -> e.getLabel().equalsIgnoreCase(text));
@@ -60,7 +59,7 @@ public interface HelioBaseEnum<T> extends Serializable {
     /**
      * 根据枚举的{@link HelioBaseEnum#getValue()},{@link HelioBaseEnum#getLabel()} ()}来查找.
      *
-     * @see this#find(Class, Predicate)
+     * @see #find(Class, Predicate)
      */
     static <T extends Enum<?> & HelioBaseEnum<?>> Optional<T> find(Class<T> type, Object target) {
         return find(type, v -> v.eq(target));
@@ -144,18 +143,27 @@ public interface HelioBaseEnum<T> extends Serializable {
         throw new IllegalArgumentException("枚举类的 value 不能自动转换为 int !");
     }
 
+    default String formatLabel(Object... templateParams) {
+        if (templateParams == null || templateParams.length <= 0) {
+            return this.getLabel();
+        }
+
+        return StrUtil.format(this.getLabel(), templateParams);
+    }
+
     /**
      * 断言不为null
      * 注意:枚举选项的 value 建议为整数类型
      *
      * @param object 需要判断的对象
+     * @param templateParams label 中如果有占位符的话,向里面填充的模板参数
      */
-    default void assertNotNull(Object object) {
+    default void assertNotNull(Object object, Object... templateParams) {
         if (ObjectUtil.isNotNull(object)) {
             return;
         }
 
-        throw new BusinessException(this.convertValue2Int(), this.getLabel());
+        throw new BusinessException(this.convertValue2Int(), this.formatLabel(templateParams));
     }
 
     /**
@@ -163,13 +171,14 @@ public interface HelioBaseEnum<T> extends Serializable {
      * 注意:枚举选项的 value 建议为整数类型
      *
      * @param str 需要判断的对象
+     * @param templateParams label 中如果有占位符的话,向里面填充的模板参数
      */
-    default void assertNotBlank(CharSequence str) {
+    default void assertNotBlank(CharSequence str, Object... templateParams) {
         if (StrUtil.isNotBlank(str)) {
             return;
         }
 
-        throw new BusinessException(this.convertValue2Int(), this.getLabel());
+        throw new BusinessException(this.convertValue2Int(), this.formatLabel(templateParams));
     }
 
     /**
@@ -177,13 +186,14 @@ public interface HelioBaseEnum<T> extends Serializable {
      * 注意:枚举选项的 value 建议为整数类型
      *
      * @param list 需要判断的对象
+     * @param templateParams label 中如果有占位符的话,向里面填充的模板参数
      */
-    default void assertNotEmpty(Collection<?> list) {
+    default void assertNotEmpty(Collection<?> list, Object... templateParams) {
         if (CollUtil.isNotEmpty(list)) {
             return;
         }
 
-        throw new BusinessException(this.convertValue2Int(), this.getLabel());
+        throw new BusinessException(this.convertValue2Int(), this.formatLabel(templateParams));
     }
 
 }

+ 10 - 4
helio-core/src/main/java/cc/uncarbon/framework/core/enums/TenantIsolateLevelEnum.java

@@ -6,18 +6,24 @@ import lombok.Getter;
 
 /**
  * 多租户隔离级别枚举类
+ *
  * @author Uncarbon
  */
 @AllArgsConstructor
 @Getter
 public enum TenantIsolateLevelEnum implements HelioBaseEnum<Integer> {
 
-    /*
-    未实现
-    DATASOURCE(4, "数据源级(即每个租户单独一个数据库)");
+    /**
+     * 行级,即每张表增加一个'租户ID'字段
      */
+    LINE(1, "行级"),
 
-    LINE(1, "行级(即每张表增加一个'租户ID'字段)");
+    /**
+     * 数据源级,即每个租户使用独立的数据源
+     */
+    DATASOURCE(2, "数据源级"),
+
+    ;
 
     private final Integer value;
     private final String label;

+ 37 - 2
helio-core/src/main/java/cc/uncarbon/framework/core/exception/BusinessException.java

@@ -1,11 +1,12 @@
 package cc.uncarbon.framework.core.exception;
 
+import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR;
+
 import cc.uncarbon.framework.core.enums.HelioBaseEnum;
+import cn.hutool.core.util.StrUtil;
 import lombok.Getter;
 import lombok.NoArgsConstructor;
 
-import static java.net.HttpURLConnection.HTTP_INTERNAL_ERROR;
-
 /**
  * 业务异常类
  *
@@ -17,21 +18,55 @@ public class BusinessException extends RuntimeException {
 
     private Integer code = null;
 
+    /**
+     * 仅错误信息
+     * @param msg 错误信息
+     */
     public BusinessException(String msg) {
         super(msg);
         this.code = HTTP_INTERNAL_ERROR;
     }
 
+    /**
+     * 错误码 + 错误信息
+     * @param code 错误码
+     * @param msg 错误信息
+     */
     public BusinessException(int code, String msg) {
         super(msg);
         this.code = code;
     }
 
+    /**
+     * 错误码 + 错误信息(支持模板参数填充)
+     * @param code 错误码
+     * @param msg 错误信息
+     * @param templateParams 模板参数
+     */
+    public BusinessException(int code, String msg, Object... templateParams) {
+        super(StrUtil.format(msg, templateParams));
+        this.code = code;
+    }
+
+    /**
+     * 从枚举类生成异常
+     * @param customEnum 枚举类对象
+     */
     public BusinessException(HelioBaseEnum<Integer> customEnum) {
         super(customEnum.getLabel());
         this.code = customEnum.getValue();
     }
 
+    /**
+     * 从枚举类生成异常(错误信息支持模板参数填充)
+     * @param customEnum 枚举类对象
+     * @param templateParams label 中如果有占位符的话,向里面填充的模板参数
+     */
+    public BusinessException(HelioBaseEnum<Integer> customEnum, Object... templateParams) {
+        super(customEnum.formatLabel(templateParams));
+        this.code = customEnum.getValue();
+    }
+
     /**
      * 关闭爬栈
      */

+ 65 - 50
helio-core/src/main/java/cc/uncarbon/framework/core/props/HelioProperties.java

@@ -1,32 +1,36 @@
 package cc.uncarbon.framework.core.props;
 
+import cc.uncarbon.framework.core.constant.HelioConstant;
 import cc.uncarbon.framework.core.enums.IdGeneratorStrategyEnum;
 import cc.uncarbon.framework.core.enums.TenantIsolateLevelEnum;
-import lombok.Data;
-import org.springframework.boot.context.properties.ConfigurationProperties;
-
 import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashSet;
 import java.util.List;
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
 
 /**
- * 配置文件读取类
+ * 配置项会被反序列化到本 Spring Bean 中,可自行依赖注入后使用
  *
  * @author Uncarbon
  */
-@ConfigurationProperties(prefix = "helio", ignoreInvalidFields = true)
+@ConfigurationProperties(prefix = "helio")
 @Data
 public class HelioProperties {
 
     private final Security security = new Security();
     private final Crud crud = new Crud();
     private final Knife4j knife4j = new Knife4j();
+    private final Tenant tenant = new Tenant();
+    private final Web web = new Web();
 
 
     @Data
     public static class Security {
 
         /**
-         * 放行路由地址(不进行登录校验)
+         * 放行路由地址,不进行登录校验
          */
         private final List<String> excludeRoutes = new ArrayList<>(64);
 
@@ -35,11 +39,6 @@ public class HelioProperties {
     @Data
     public static class Crud {
 
-        /**
-         * 多租户插件
-         */
-        private final Tenant tenant = new Tenant();
-
         /**
          * 乐观锁插件
          */
@@ -51,43 +50,18 @@ public class HelioProperties {
         private final IdGenerator idGenerator = new IdGenerator();
 
         /**
-         * 用的哪种数据库 如: mysql  postgresql
-         * 参考com.baomidou.mybatisplus.annotation.DbType
-         * 默认为mysql
+         * 用的哪种数据库 如: mysql  postgresql 参考com.baomidou.mybatisplus.annotation.DbType 默认为mysql
          */
         private String dbType = "mysql";
 
 
-        @Data
-        public static class Tenant {
-
-            /**
-             * 是否启用多租户
-             * 默认为false
-             */
-            private Boolean enabled = false;
-
-            /**
-             * 多租户隔离级别
-             * 默认为行级
-             */
-            private TenantIsolateLevelEnum isolateLevel = TenantIsolateLevelEnum.LINE;
-
-            /**
-             * 哪些表不启用租户隔离
-             */
-            private List<String> ignoredTables = new ArrayList<>(64);
-
-        }
-
         @Data
         public static class OptimisticLock {
 
             /**
-             * 是否启用乐观锁
-             * 默认为false
+             * 是否启用乐观锁 默认为 true
              */
-            private Boolean enabled = false;
+            private Boolean enabled = true;
 
         }
 
@@ -95,18 +69,17 @@ public class HelioProperties {
         public static class IdGenerator {
 
             /**
-             * 雪花ID-起始时刻
-             * 默认为2021-01-01
+             * ID生成器策略 默认为SNOWFLAKE
              */
-            String epochDate = "2021-01-01";
+            private IdGeneratorStrategyEnum strategy = IdGeneratorStrategyEnum.SNOWFLAKE;
+
             /**
-             * ID生成器策略
-             * 默认为SNOWFLAKE
+             * 雪花ID-起始时刻 默认为2021-01-01
              */
-            private IdGeneratorStrategyEnum strategy = IdGeneratorStrategyEnum.SNOWFLAKE;
+            String epochDate = "2021-01-01";
+
             /**
-             * 雪花ID-数据中心ID
-             * 默认为0
+             * 雪花ID-数据中心ID 默认为0
              */
             private Long datacenterId = 0L;
 
@@ -117,19 +90,61 @@ public class HelioProperties {
     public static class Knife4j {
 
         /**
-         * 标题
+         * Knife4j UI 上显示的标题
          */
         private String title = "";
 
         /**
-         * 简介
+         * Knife4j UI 上显示的简介
          */
         private String description = "";
 
         /**
-         * 版本号
+         * Knife4j UI 上显示的版本号
          */
         private String version = "";
 
     }
+
+    @Data
+    public static class Tenant {
+
+        /**
+         * 是否启用多租户 默认为 false
+         */
+        private Boolean enabled = false;
+
+        /**
+         * 多租户隔离级别 默认为行级
+         */
+        private TenantIsolateLevelEnum isolateLevel = TenantIsolateLevelEnum.LINE;
+
+        /**
+         * 哪些表忽略租户隔离 仅对【行级】隔离级别有效
+         */
+        private Collection<String> ignoredTables = new LinkedHashSet<>(64);
+
+        /**
+         * 特权租户ID,该租户可以无视租户SQL拦截器 目前仅对【行级】隔离级别有效 默认为 0
+         */
+        private Long privilegedTenantId = HelioConstant.Tenant.DEFAULT_PRIVILEGED_TENANT_ID;
+
+    }
+
+    @Data
+    public static class Web {
+
+        private final Logging logging = new Logging();
+
+        @Data
+        public static class Logging {
+
+            /**
+             * 是否启用 Web 访问日志切面,默认为 false
+             */
+            private Boolean enabled = Boolean.FALSE;
+
+        }
+
+    }
 }

+ 2 - 1
helio-core/src/main/resources/META-INF/spring.factories

@@ -1,2 +1,3 @@
 org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
-  cc.uncarbon.framework.core.config.HelioPropertiesAutoConfiguration
+  cc.uncarbon.framework.core.config.HelioPropertiesAutoConfiguration,\
+  cc.uncarbon.framework.core.config.AdaptHistoryVersionAutoConfiguration

+ 1 - 1
helio-starter-aop/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>helio-starters</artifactId>
         <groupId>cc.uncarbon.framework</groupId>
-        <version>1.4.0</version>
+        <version>1.5.0</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <modelVersion>4.0.0</modelVersion>

+ 6 - 1
helio-starter-cloud/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>helio-starters</artifactId>
         <groupId>cc.uncarbon.framework</groupId>
-        <version>1.4.0</version>
+        <version>1.5.0</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <modelVersion>4.0.0</modelVersion>
@@ -48,5 +48,10 @@
             <artifactId>nacos-client</artifactId>
             <version>${nacos-client.vesion}</version>
         </dependency>
+
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+        </dependency>
     </dependencies>
 </project>

+ 7 - 1
helio-starter-crud/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>helio-starters</artifactId>
         <groupId>cc.uncarbon.framework</groupId>
-        <version>1.4.0</version>
+        <version>1.5.0</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <modelVersion>4.0.0</modelVersion>
@@ -45,5 +45,11 @@
             <groupId>com.microsoft.sqlserver</groupId>
             <artifactId>sqljdbc4</artifactId>
         </dependency>
+
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
+            <optional>true</optional>
+        </dependency>
     </dependencies>
 </project>

+ 22 - 0
helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/config/DynamicDataSourceAutoConfiguration.java

@@ -0,0 +1,22 @@
+package cc.uncarbon.framework.crud.config;
+
+import cc.uncarbon.framework.crud.dynamicdatasource.DataSourceDefinitionProvider;
+import cc.uncarbon.framework.crud.dynamicdatasource.HelioDynamicDataSourceRegistry;
+import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
+import com.baomidou.dynamic.datasource.creator.HikariDataSourceCreator;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
+import org.springframework.context.annotation.Bean;
+
+@ConditionalOnClass(value = DynamicRoutingDataSource.class)
+public class DynamicDataSourceAutoConfiguration {
+
+    @Bean
+    public HelioDynamicDataSourceRegistry helioDynamicDataSourceRegistry(
+            DynamicRoutingDataSource dynamicRoutingDataSource,
+            HikariDataSourceCreator dataSourceCreator,
+            ObjectProvider<DataSourceDefinitionProvider> dataSourceDefinitionProviders
+    ) {
+        return new HelioDynamicDataSourceRegistry(dynamicRoutingDataSource, dataSourceCreator,dataSourceDefinitionProviders);
+    }
+}

+ 34 - 27
helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/config/HelioMybatisPlusAutoConfiguration.java

@@ -1,74 +1,78 @@
 package cc.uncarbon.framework.crud.config;
 
-import cc.uncarbon.framework.core.enums.TenantIsolateLevelEnum;
 import cc.uncarbon.framework.core.props.HelioProperties;
 import cc.uncarbon.framework.crud.handler.HelioIdentifierGeneratorHandler;
-import cc.uncarbon.framework.crud.handler.HelioTenantLineHandler;
 import cc.uncarbon.framework.crud.handler.MybatisPlusAutoFillColumnHandler;
-import com.baomidou.mybatisplus.annotation.DbType;
+import cc.uncarbon.framework.crud.support.TenantSupport;
+import cc.uncarbon.framework.crud.support.impl.DefaultTenantSupport;
 import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
 import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
 import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
 import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
 import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
-import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.transaction.annotation.EnableTransactionManagement;
 
-import javax.annotation.Resource;
-
 /**
  * Mybatis-Plus配置类
+ *
  * @author Uncarbon
  */
 @EnableTransactionManagement(
         proxyTargetClass = true
 )
 @Configuration
+@Slf4j
+@RequiredArgsConstructor
 public class HelioMybatisPlusAutoConfiguration {
 
-    @Resource
-    private HelioProperties helioProperties;
-
-    @Resource
-    private HelioTenantLineHandler helioTenantLineHandler;
+    private final HelioProperties helioProperties;
 
 
     @Bean
-    public MybatisPlusInterceptor mybatisPlusInterceptor() {
+    @ConditionalOnMissingBean
+    public MybatisPlusInterceptor mybatisPlusInterceptor(
+            TenantSupport tenantSupport
+    ) {
         MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
 
         /*
-        https://mybatis.plus/guide/interceptor.html#mybatisplusinterceptor
+        https://baomidou.com/pages/2976a3/#%E5%B1%9E%E6%80%A7
         使用多个功能需要注意顺序关系,建议使用如下顺序
 
         多租户,动态表名
         分页,乐观锁
         sql性能规范,防止全表更新与删除
          */
-        // 多租户 && 行级租户隔离级别
-        if (Boolean.TRUE.equals(helioProperties.getCrud().getTenant().getEnabled())
-                && TenantIsolateLevelEnum.LINE.equals(helioProperties.getCrud().getTenant().getIsolateLevel())
-        ) {
-            interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(helioTenantLineHandler));
+        if (Boolean.TRUE.equals(helioProperties.getTenant().getEnabled())) {
+            // 配置文件中启用了多租户功能,注入对应支持 bean
+            tenantSupport.support(helioProperties, interceptor);
         }
 
-        // 分页插件
-        PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.getDbType(helioProperties.getCrud().getDbType()));
-        // 设置sql的limit为无限制,默认是500
+        /*
+        分页插件
+         */
+        PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
+        // 设置sql的limit为无限制
         paginationInnerInterceptor.setMaxLimit(-1L);
         interceptor.addInnerInterceptor(paginationInnerInterceptor);
 
-        // 乐观锁
+        /*
+        乐观锁
+         */
         if (Boolean.TRUE.equals(helioProperties.getCrud().getOptimisticLock().getEnabled())) {
             interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
         }
 
-        // 防止全表更新与删除
+        /*
+        防止全表更新与删除
+         */
         interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
 
-
         return interceptor;
     }
 
@@ -76,6 +80,7 @@ public class HelioMybatisPlusAutoConfiguration {
      * 自定义ID生成器
      */
     @Bean
+    @ConditionalOnMissingBean
     public IdentifierGenerator helioSnowflakeIdentifierGeneratorHandler() {
         return new HelioIdentifierGeneratorHandler(helioProperties);
     }
@@ -84,16 +89,18 @@ public class HelioMybatisPlusAutoConfiguration {
      * 字段自动填充
      */
     @Bean
+    @ConditionalOnMissingBean
     public MybatisPlusAutoFillColumnHandler mybatisPlusAutoFillColumnHandler() {
         return new MybatisPlusAutoFillColumnHandler();
     }
 
     /**
-     * 行级租户
+     * 默认租户支持类
      */
     @Bean
-    public HelioTenantLineHandler helioTenantLineHandler() {
-        return new HelioTenantLineHandler();
+    @ConditionalOnMissingBean
+    public TenantSupport defaultTenantSupport() {
+        return new DefaultTenantSupport();
     }
 
 }

+ 2 - 8
helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/config/InitHikariPoolAtStartupConfiguration.java

@@ -1,11 +1,9 @@
 package cc.uncarbon.framework.crud.config;
 
+import javax.sql.DataSource;
 import org.springframework.boot.ApplicationRunner;
 import org.springframework.context.annotation.Bean;
 
-import javax.annotation.Resource;
-import javax.sql.DataSource;
-
 
 /**
  * 在项目启动时直接初始化Hikari连接池, 关闭按需连接
@@ -13,12 +11,8 @@ import javax.sql.DataSource;
  */
 public class InitHikariPoolAtStartupConfiguration {
 
-    @Resource
-    private DataSource dataSource;
-
-
     @Bean
-    public ApplicationRunner runner() {
+    public ApplicationRunner runner(DataSource dataSource) {
         return args -> dataSource.getConnection();
     }
 }

+ 50 - 0
helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/dynamicdatasource/DataSourceDefinition.java

@@ -0,0 +1,50 @@
+package cc.uncarbon.framework.crud.dynamicdatasource;
+
+import io.swagger.annotations.ApiModelProperty;
+import java.io.Serializable;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+import lombok.experimental.SuperBuilder;
+
+@Accessors(chain = true)
+@SuperBuilder
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class DataSourceDefinition implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 数据源名称,切换数据源时需要这个,最好不用数字开头
+     */
+    @ApiModelProperty(value = "数据源名称", notes = "切换数据源时需要这个,最好不用数字开头")
+    private String name;
+
+    /**
+     * 数据库驱动类名称
+     */
+    @ApiModelProperty(value = "数据库驱动类名称")
+    private String driverClassName;
+
+    /**
+     * 数据库连接地址
+     */
+    @ApiModelProperty(value = "数据库连接地址")
+    private String url;
+
+    /**
+     * 数据库账号
+     */
+    @ApiModelProperty(value = "数据库账号")
+    private String username;
+
+    /**
+     * 数据库密码
+     */
+    @ApiModelProperty(value = "数据库密码")
+    private String password;
+
+}

+ 12 - 0
helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/dynamicdatasource/DataSourceDefinitionProvider.java

@@ -0,0 +1,12 @@
+package cc.uncarbon.framework.crud.dynamicdatasource;
+
+public interface DataSourceDefinitionProvider {
+
+    /**
+     * 根据数据源名称,得到数据源定义
+     * @param dataSourceName 需要的数据源名称
+     * @return DataSourceDefinition
+     */
+    DataSourceDefinition getDataSourceDefinition(String dataSourceName);
+
+}

+ 93 - 0
helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/dynamicdatasource/HelioDynamicDataSourceRegistry.java

@@ -0,0 +1,93 @@
+package cc.uncarbon.framework.crud.dynamicdatasource;
+
+import cn.hutool.core.bean.BeanUtil;
+import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
+import com.baomidou.dynamic.datasource.creator.HikariDataSourceCreator;
+import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DataSourceProperty;
+import com.zaxxer.hikari.HikariDataSource;
+import javax.sql.DataSource;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.ObjectProvider;
+
+/**
+ * Helio 动态多数据源登记处
+ *
+ * @author chill
+ * @author Uncarbon
+ */
+@RequiredArgsConstructor
+@Slf4j
+// @ConditionalOnClass(value = DynamicRoutingDataSource.class)
+public class HelioDynamicDataSourceRegistry {
+
+    private final DynamicRoutingDataSource dynamicRoutingDataSource;
+    private final HikariDataSourceCreator dataSourceCreator;
+    private final ObjectProvider<DataSourceDefinitionProvider> dataSourceDefinitionProviders;
+
+
+    /**
+     * 查询是否已存在数据源
+     *
+     * @param dataSourceName 数据源名称
+     * @return boolean
+     */
+    public boolean containsDataSource(String dataSourceName) {
+        return this.containsDataSource(dataSourceName, false);
+    }
+
+    /**
+     * 查询是否已存在数据源
+     *
+     * @param dataSourceName 数据源名称
+     * @param obtainIfAbsent 缺失时尝试从 IoC 容器中获取,并加入数据源缓存
+     * @return boolean
+     */
+    public boolean containsDataSource(String dataSourceName, boolean obtainIfAbsent) {
+        /*
+        配置不存在则动态添加数据源,以懒加载的模式解决分布式场景的配置同步
+        为了保证数据完整性,配置后生成数据源缓存,后台便无法修改更换数据源,若一定要修改请迁移数据后重启服务或自行修改底层逻辑
+         */
+        if (dynamicRoutingDataSource.getDataSources().containsKey(dataSourceName)) {
+            return true;
+        }
+
+        if (!obtainIfAbsent) {
+            return false;
+        }
+
+        boolean successFlag = false;
+        for (DataSourceDefinitionProvider provider : dataSourceDefinitionProviders) {
+            DataSourceDefinition dataSourceDefinition = provider.getDataSourceDefinition(dataSourceName);
+
+            if (dataSourceDefinition != null) {
+                // 存在非空结果,则拷贝数据源配置
+                DataSourceProperty dataSourceProperty = new DataSourceProperty();
+                BeanUtil.copyProperties(dataSourceDefinition, dataSourceProperty);
+
+                // 补充字段
+                dataSourceProperty
+                        .setPoolName(dataSourceDefinition.getName())
+                        .setType(HikariDataSource.class)
+                ;
+
+                // 创建动态数据源
+                DataSource dataSource = dataSourceCreator.createDataSource(dataSourceProperty);
+
+                // 添加新数据源
+                dynamicRoutingDataSource.addDataSource(dataSourceName, dataSource);
+                log.info("已动态添加新数据源 {}", dataSourceName);
+
+                successFlag = true;
+                break;
+            }
+        }
+
+        if (!successFlag) {
+            log.error("动态添加新数据源失败,原因:没有 DataSourceDefinitionProvider 提供 {} 的 DataSourceDefinition", dataSourceName);
+        }
+
+        return successFlag;
+    }
+
+}

+ 16 - 9
helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/entity/HelioBaseEntity.java

@@ -1,9 +1,15 @@
 package cc.uncarbon.framework.crud.entity;
 
 import cc.uncarbon.framework.core.constant.HelioConstant;
-import com.baomidou.mybatisplus.annotation.*;
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import io.swagger.annotations.ApiModelProperty;
+import java.io.Serializable;
+import java.time.LocalDateTime;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
@@ -11,11 +17,9 @@ import lombok.experimental.Accessors;
 import lombok.experimental.SuperBuilder;
 import org.springframework.format.annotation.DateTimeFormat;
 
-import java.io.Serializable;
-import java.time.LocalDateTime;
-
 /**
- * 基础实体类
+ * 基础实体类,默认含[行级租户ID]
+ * T = 主键类型,一般用 Long
  *
  * @author Uncarbon
  */
@@ -24,14 +28,17 @@ import java.time.LocalDateTime;
 @AllArgsConstructor
 @NoArgsConstructor
 @Data
-public abstract class HelioBaseEntity<PK extends Serializable> implements Serializable {
+public abstract class HelioBaseEntity<T extends Serializable> implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
 
     /**
      * 主键ID
      */
     @ApiModelProperty(value = "主键ID")
     @TableId(value = "id", type = IdType.ASSIGN_ID)
-    private PK id;
+    private T id;
 
     /**
      * 行级租户ID
@@ -61,7 +68,7 @@ public abstract class HelioBaseEntity<PK extends Serializable> implements Serial
      */
     @ApiModelProperty(value = "创建时刻")
     @DateTimeFormat(pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
-    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT, timezone = HelioConstant.Jackson.TIME_ZONE)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
     @TableField(value = HelioConstant.CRUD.COLUMN_CREATED_AT, fill = FieldFill.INSERT)
     private LocalDateTime createdAt;
 
@@ -77,7 +84,7 @@ public abstract class HelioBaseEntity<PK extends Serializable> implements Serial
      */
     @ApiModelProperty(value = "更新时刻")
     @DateTimeFormat(pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
-    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT, timezone = HelioConstant.Jackson.TIME_ZONE)
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HelioConstant.Jackson.DATE_TIME_FORMAT)
     @TableField(value = HelioConstant.CRUD.COLUMN_UPDATED_AT, fill = FieldFill.INSERT_UPDATE)
     private LocalDateTime updatedAt;
 

+ 33 - 0
helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/entity/HelioNoTenantBaseEntity.java

@@ -0,0 +1,33 @@
+package cc.uncarbon.framework.crud.entity;
+
+import cc.uncarbon.framework.core.constant.HelioConstant;
+import com.baomidou.mybatisplus.annotation.TableField;
+import java.io.Serializable;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+import lombok.experimental.SuperBuilder;
+
+/**
+ * 基础实体类,去除[行级租户ID]
+ * T = 主键类型,一般用 Long
+ *
+ * @author Uncarbon
+ */
+@EqualsAndHashCode(callSuper = true)
+@Accessors(chain = true)
+@SuperBuilder
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public abstract class HelioNoTenantBaseEntity<T extends Serializable> extends HelioBaseEntity<T>  {
+
+    /**
+     * 覆盖掉行级租户ID
+     */
+    @TableField(value = HelioConstant.CRUD.COLUMN_TENANT_ID, exist = false)
+    private Long tenantId;
+
+}

+ 0 - 1
helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/enums/YesOrNoEnum.java

@@ -1,6 +1,5 @@
 package cc.uncarbon.framework.crud.enums;
 
-
 import cc.uncarbon.framework.core.enums.HelioBaseEnum;
 import com.baomidou.mybatisplus.annotation.EnumValue;
 import lombok.AllArgsConstructor;

+ 0 - 41
helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/handler/HelioTenantLineHandler.java

@@ -1,41 +0,0 @@
-package cc.uncarbon.framework.crud.handler;
-
-import cc.uncarbon.framework.core.constant.HelioConstant;
-import cc.uncarbon.framework.core.context.UserContextHolder;
-import cc.uncarbon.framework.core.props.HelioProperties;
-import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
-import net.sf.jsqlparser.expression.Expression;
-import net.sf.jsqlparser.expression.LongValue;
-
-import javax.annotation.Resource;
-
-/**
- * 行级租户拦截器
- * @author Uncarbon
- */
-public class HelioTenantLineHandler implements TenantLineHandler {
-
-    @Resource
-    private HelioProperties helioProperties;
-
-
-    @Override
-    public Expression getTenantId() {
-        return new LongValue(UserContextHolder.getRelationalTenant().getTenantId());
-    }
-
-    @Override
-    public String getTenantIdColumn() {
-        return HelioConstant.CRUD.COLUMN_TENANT_ID;
-    }
-
-    @Override
-    public boolean ignoreTable(String tableName) {
-        if (HelioConstant.CRUD.PRIVILEGED_TENANT_ID.equals(UserContextHolder.getRelationalTenant().getTenantId())) {
-            return true;
-        }
-
-        return helioProperties.getCrud().getTenant().getIgnoredTables().contains(tableName);
-    }
-
-}

+ 3 - 3
helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/handler/MybatisPlusAutoFillColumnHandler.java

@@ -1,11 +1,11 @@
 package cc.uncarbon.framework.crud.handler;
 
 import cc.uncarbon.framework.core.constant.HelioConstant;
+import cc.uncarbon.framework.core.context.TenantContextHolder;
 import cc.uncarbon.framework.core.context.UserContextHolder;
 import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
-import org.apache.ibatis.reflection.MetaObject;
-
 import java.time.LocalDateTime;
+import org.apache.ibatis.reflection.MetaObject;
 
 /**
  * 字段自动填充, 摘自Mybatis-Plus官方例程
@@ -16,7 +16,7 @@ public class MybatisPlusAutoFillColumnHandler implements MetaObjectHandler {
 
     @Override
     public void insertFill(MetaObject metaObject) {
-        this.strictInsertFill(metaObject, HelioConstant.CRUD.ENTITY_FIELD_TENANT_ID, Long.class, UserContextHolder.getRelationalTenant().getTenantId());
+        this.strictInsertFill(metaObject, HelioConstant.CRUD.ENTITY_FIELD_TENANT_ID, Long.class, TenantContextHolder.getTenantId());
         this.strictInsertFill(metaObject, HelioConstant.CRUD.ENTITY_FIELD_CREATED_AT, LocalDateTime.class, LocalDateTime.now());
         this.strictInsertFill(metaObject, HelioConstant.CRUD.ENTITY_FIELD_CREATED_BY, String.class, UserContextHolder.getUserName());
 

+ 5 - 1
helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/service/HelioBaseService.java

@@ -3,7 +3,11 @@ package cc.uncarbon.framework.crud.service;
 import com.baomidou.mybatisplus.extension.service.IService;
 
 /**
+ * 服务基础模板
+ * E = ENTITY 实体类
+ *
  * @author Uncarbon
  */
-public interface HelioBaseService<ENTITY> extends IService<ENTITY> {
+public interface HelioBaseService<E> extends IService<E> {
+
 }

+ 5 - 1
helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/service/impl/HelioBaseServiceImpl.java

@@ -7,9 +7,13 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import lombok.NoArgsConstructor;
 
 /**
+ * 服务实现类基础模板
+ * M = MAPPER 持久层 interface
+ * E = ENTITY 实体类
+ *
  * @author Uncarbon
  */
 @NoArgsConstructor
-public class HelioBaseServiceImpl<MAPPER extends BaseMapper<ENTITY>, ENTITY extends HelioBaseEntity<?>> extends ServiceImpl<MAPPER, ENTITY> implements HelioBaseService<ENTITY> {
+public class HelioBaseServiceImpl<M extends BaseMapper<E>, E extends HelioBaseEntity<?>> extends ServiceImpl<M, E> implements HelioBaseService<E> {
 
 }

+ 20 - 0
helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/support/TenantSupport.java

@@ -0,0 +1,20 @@
+package cc.uncarbon.framework.crud.support;
+
+import cc.uncarbon.framework.core.props.HelioProperties;
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+
+/**
+ * Mybatis-Plus 多租户支持接口
+ *
+ * @author Uncarbon
+ */
+public interface TenantSupport {
+
+    /**
+     * 不同的多租户隔离级别分别实现本方法,按需添加 SQL 拦截器
+     * @param helioProperties Helio 配置属性
+     * @param interceptor Mybatis-Plus 拦截器
+     */
+    void support(HelioProperties helioProperties, MybatisPlusInterceptor interceptor);
+
+}

+ 16 - 0
helio-starter-crud/src/main/java/cc/uncarbon/framework/crud/support/impl/DefaultTenantSupport.java

@@ -0,0 +1,16 @@
+package cc.uncarbon.framework.crud.support.impl;
+
+import cc.uncarbon.framework.core.props.HelioProperties;
+import cc.uncarbon.framework.crud.support.TenantSupport;
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+
+/**
+ * 多租户支持-默认处理
+ */
+public class DefaultTenantSupport implements TenantSupport {
+
+    @Override
+    public void support(HelioProperties helioProperties, MybatisPlusInterceptor interceptor) {
+        System.err.println("\n[多租户支持] >> 您启用了多租户,但未引入 helio-starter-tenant,无法对 SQL 进行拦截处理\n");
+    }
+}

+ 2 - 1
helio-starter-crud/src/main/resources/META-INF/spring.factories

@@ -1,2 +1,3 @@
 org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
-  cc.uncarbon.framework.crud.config.HelioMybatisPlusAutoConfiguration
+  cc.uncarbon.framework.crud.config.HelioMybatisPlusAutoConfiguration,\
+  cc.uncarbon.framework.crud.config.DynamicDataSourceAutoConfiguration

+ 1 - 1
helio-starter-dubbo/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>helio-starters</artifactId>
         <groupId>cc.uncarbon.framework</groupId>
-        <version>1.4.0</version>
+        <version>1.5.0</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <modelVersion>4.0.0</modelVersion>

+ 16 - 4
helio-starter-dubbo/src/main/java/cc/uncarbon/framework/dubbo/filter/ConsumerContextFilter.java

@@ -1,5 +1,7 @@
 package cc.uncarbon.framework.dubbo.filter;
 
+import cc.uncarbon.framework.core.context.TenantContext;
+import cc.uncarbon.framework.core.context.TenantContextHolder;
 import cc.uncarbon.framework.core.context.UserContext;
 import cc.uncarbon.framework.core.context.UserContextHolder;
 import cn.hutool.json.JSONUtil;
@@ -25,12 +27,22 @@ public class ConsumerContextFilter implements Filter {
 
     @Override
     public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
+        // 将用户上下文、租户上下文等业务字段,放进 Dubbo 附件中
+
         UserContext userContext = UserContextHolder.getUserContext();
-        String userContextJson = JSONUtil.toJsonStr(userContext);
+        if (userContext != null) {
+            String userContextJson = JSONUtil.toJsonStr(userContext);
+            log.debug("[Dubbo RPC] 设置当前用户上下文 >> {}", userContextJson);
+            RpcContext.getClientAttachment().setAttachment(UserContext.CAMEL_NAME, userContextJson);
+        }
 
-        // 放进Dubbo消费者附件中
-        log.debug("[Dubbo RPC] 设置当前用户上下文 >> {}", userContextJson);
-        RpcContext.getContext().setAttachment(UserContext.CAMEL_NAME, userContextJson);
+        TenantContext tenantContext = TenantContextHolder.getTenantContext();
+        if (tenantContext != null && tenantContext.getTenantId() != null) {
+            // 实际启用了租户
+            String tenantContextJson = JSONUtil.toJsonStr(tenantContext);
+            log.debug("[Dubbo RPC] 设置当前租户上下文 >> {}", tenantContextJson);
+            RpcContext.getClientAttachment().setAttachment(TenantContext.CAMEL_NAME, tenantContextJson);
+        }
 
         return invoker.invoke(invocation);
     }

+ 69 - 80
helio-starter-dubbo/src/main/java/cc/uncarbon/framework/dubbo/filter/HelioDubboExceptionFilter.java

@@ -7,10 +7,10 @@ import lombok.extern.slf4j.Slf4j;
 import org.apache.dubbo.common.utils.ReflectUtils;
 import org.apache.dubbo.rpc.Invocation;
 import org.apache.dubbo.rpc.Invoker;
-import org.apache.dubbo.rpc.ListenableFilter;
 import org.apache.dubbo.rpc.Result;
 import org.apache.dubbo.rpc.RpcContext;
 import org.apache.dubbo.rpc.RpcException;
+import org.apache.dubbo.rpc.filter.ExceptionFilter;
 import org.apache.dubbo.rpc.service.GenericService;
 
 /**
@@ -20,103 +20,92 @@ import org.apache.dubbo.rpc.service.GenericService;
  * @author Uncarbon
  **/
 @Slf4j
-public class HelioDubboExceptionFilter extends ListenableFilter {
+public class HelioDubboExceptionFilter extends ExceptionFilter {
 
     private static final String BUSINESS_EXCEPTION_SIMPLE_CLASS_NAME = BusinessException.class.getSimpleName();
 
-    public HelioDubboExceptionFilter() {
-        super.listener = new ExceptionListener();
-    }
-
     @Override
-    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
-        return invoker.invoke(invocation);
-    }
+    public void onResponse(Result appResponse, Invoker<?> invoker, Invocation invocation) {
+        if (!(
+                appResponse.hasException() && GenericService.class != invoker.getInterface()
+        )) {
+            return;
+        }
+
+        try {
+            Throwable exception = appResponse.getException();
 
-    static class ExceptionListener implements Listener {
-        @Override
-        public void onResponse(Result appResponse, Invoker<?> invoker, Invocation invocation) {
-            if (!(
-                    appResponse.hasException() && GenericService.class != invoker.getInterface()
-            )) {
+            // directly throw if it's checked exception
+            if (!(exception instanceof RuntimeException) && (exception instanceof Exception)) {
                 return;
             }
 
-            try {
-                Throwable exception = appResponse.getException();
-
-                // directly throw if it's checked exception
-                if (!(exception instanceof RuntimeException) && (exception instanceof Exception)) {
+            // directly throw if the exception appears in the signature
+            Method method = invoker.getInterface()
+                    .getMethod(invocation.getMethodName(), invocation.getParameterTypes());
+            Class<?>[] exceptionClasses = method.getExceptionTypes();
+            for (Class<?> exceptionClass : exceptionClasses) {
+                if (exception.getClass().equals(exceptionClass)) {
                     return;
                 }
+            }
 
-                // directly throw if the exception appears in the signature
-                Method method = invoker.getInterface()
-                        .getMethod(invocation.getMethodName(), invocation.getParameterTypes());
-                Class<?>[] exceptionClasses = method.getExceptionTypes();
-                for (Class<?> exceptionClass : exceptionClasses) {
-                    if (exception.getClass().equals(exceptionClass)) {
-                        return;
-                    }
-                }
-
-                // for the exception not found in method's signature, print ERROR message in server's log.
-                log.error(
-                        "Got unchecked and undeclared exception which called by {}. service: {}, method: {}, exception: {} >> {}",
-                        RpcContext.getContext().getRemoteHost(),
-                        invoker.getInterface().getName(),
-                        invocation.getMethodName(),
-                        exception.getClass().getName(),
-                        exception.getMessage(),
-                        exception);
-
-                // directly throw if exception class and interface class are in the same jar file.
-                String serviceFile = ReflectUtils.getCodeBase(invoker.getInterface());
-                String exceptionFile = ReflectUtils.getCodeBase(exception.getClass());
-                if (serviceFile == null || exceptionFile == null || serviceFile.equals(exceptionFile)) {
-                    return;
-                }
-                // directly throw if it's JDK exception
-                String className = exception.getClass().getName();
-                if (className.startsWith("java.") || className.startsWith("javax.")) {
-                    return;
-                }
-                // directly throw if it's dubbo exception
-                if (exception instanceof RpcException) {
-                    return;
-                }
+            // for the exception not found in method's signature, print ERROR message in server's log.
+            log.error(
+                    "Got unchecked and undeclared exception which called by {}. service: {}, method: {}, exception: {} >> {}",
+                    RpcContext.getServiceContext().getRemoteAddress(),
+                    invoker.getInterface().getName(),
+                    invocation.getMethodName(),
+                    exception.getClass().getName(),
+                    exception.getMessage(),
+                    exception);
 
-                // 业务异常
-                if (className.contains(BUSINESS_EXCEPTION_SIMPLE_CLASS_NAME)) {
-                    return;
-                }
+            // directly throw if exception class and interface class are in the same jar file.
+            String serviceFile = ReflectUtils.getCodeBase(invoker.getInterface());
+            String exceptionFile = ReflectUtils.getCodeBase(exception.getClass());
+            if (serviceFile == null || exceptionFile == null || serviceFile.equals(exceptionFile)) {
+                return;
+            }
+            // directly throw if it's JDK exception
+            String className = exception.getClass().getName();
+            if (className.startsWith("java.") || className.startsWith("javax.")) {
+                return;
+            }
+            // directly throw if it's dubbo exception
+            if (exception instanceof RpcException) {
+                return;
+            }
 
-                // otherwise, wrap with RuntimeException and throw back to the client
-                appResponse.setException(new RuntimeException(StrUtil.toString(exception)));
-            } catch (NoSuchMethodException cause) {
-                // ignored
-            } catch (Throwable cause) {
-                log.warn("Fail to ExceptionFilter when called by {}. service: {}, method: {}, exception: {} >> {}",
-                        RpcContext.getContext().getRemoteHost(),
-                        invoker.getInterface().getName(),
-                        invocation.getMethodName(),
-                        cause.getClass().getName(),
-                        cause.getMessage(),
-                        cause);
+            // 业务异常
+            if (className.contains(BUSINESS_EXCEPTION_SIMPLE_CLASS_NAME)) {
+                return;
             }
-        }
 
-        @Override
-        public void onError(Throwable e, Invoker<?> invoker, Invocation invocation) {
-            log.error(
-                    "Got unchecked and undeclared exception which called by {}. service: {}, method: {}, exception: {} >> {}",
-                    RpcContext.getContext().getRemoteHost(),
+            // otherwise, wrap with RuntimeException and throw back to the client
+            appResponse.setException(new RuntimeException(StrUtil.toString(exception)));
+        } catch (NoSuchMethodException cause) {
+            // ignored
+        } catch (Throwable cause) {
+            log.warn("Fail to ExceptionFilter when called by {}. service: {}, method: {}, exception: {} >> {}",
+                    RpcContext.getServiceContext().getRemoteHost(),
                     invoker.getInterface().getName(),
                     invocation.getMethodName(),
-                    e.getClass().getName(),
-                    e.getMessage(),
-                    e);
+                    cause.getClass().getName(),
+                    cause.getMessage(),
+                    cause);
         }
     }
 
+    @Override
+    public void onError(Throwable e, Invoker<?> invoker, Invocation invocation) {
+        log.error(
+                "Got unchecked and undeclared exception which called by {}. service: {}, method: {}, exception: {} >> {}",
+                RpcContext.getServiceContext().getRemoteHost(),
+                invoker.getInterface().getName(),
+                invocation.getMethodName(),
+                e.getClass().getName(),
+                e.getMessage(),
+                e);
+    }
+
 }

+ 13 - 2
helio-starter-dubbo/src/main/java/cc/uncarbon/framework/dubbo/filter/ProviderContextFilter.java

@@ -1,5 +1,7 @@
 package cc.uncarbon.framework.dubbo.filter;
 
+import cc.uncarbon.framework.core.context.TenantContext;
+import cc.uncarbon.framework.core.context.TenantContextHolder;
 import cc.uncarbon.framework.core.context.UserContext;
 import cc.uncarbon.framework.core.context.UserContextHolder;
 import cn.hutool.core.util.StrUtil;
@@ -26,11 +28,20 @@ public class ProviderContextFilter implements Filter {
 
     @Override
     public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
-        String userContextJson = RpcContext.getContext().getAttachment(UserContext.CAMEL_NAME);
+        // 从 Dubbo 附件中,取出用户上下文、租户上下文等业务字段
+
+        String userContextJson = RpcContext.getServerAttachment().getAttachment(UserContext.CAMEL_NAME);
         if (StrUtil.isNotEmpty(userContextJson)) {
             UserContext userContext = JSONUtil.parseObj(userContextJson).toBean(UserContext.class);
             UserContextHolder.setUserContext(userContext);
-            log.debug("[Dubbo RPC] 获取当前用户上下文 >> {}", userContextJson);
+            log.debug("[Dubbo RPC] 取出当前用户上下文 >> {}", userContextJson);
+        }
+
+        String tenantContextJson = RpcContext.getServerAttachment().getAttachment(TenantContext.CAMEL_NAME);
+        if (StrUtil.isNotEmpty(tenantContextJson)) {
+            TenantContext tenantContext = JSONUtil.parseObj(tenantContextJson).toBean(TenantContext.class);
+            TenantContextHolder.setTenantContext(tenantContext);
+            log.debug("[Dubbo RPC] 取出当前租户上下文 >> {}", tenantContextJson);
         }
 
         return invoker.invoke(invocation);

+ 1 - 1
helio-starter-knife4j/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>helio-starters</artifactId>
         <groupId>cc.uncarbon.framework</groupId>
-        <version>1.4.0</version>
+        <version>1.5.0</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <modelVersion>4.0.0</modelVersion>

+ 6 - 6
helio-starter-knife4j/src/main/java/cc/uncarbon/framework/knife4j/config/HelioKnife4jAutoConfiguration.java

@@ -4,6 +4,7 @@ package cc.uncarbon.framework.knife4j.config;
 import cc.uncarbon.framework.core.props.HelioProperties;
 import cn.hutool.core.collection.CollUtil;
 import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
+import lombok.RequiredArgsConstructor;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
@@ -22,7 +23,6 @@ import springfox.documentation.spi.DocumentationType;
 import springfox.documentation.spi.service.contexts.SecurityContext;
 import springfox.documentation.spring.web.plugins.Docket;
 
-import javax.annotation.Resource;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -34,6 +34,7 @@ import java.util.List;
  * @author Uncarbon
  * @author xiaoymin
  */
+@RequiredArgsConstructor
 @EnableOpenApi
 @EnableKnife4j
 @Import(BeanValidatorPluginsConfiguration.class)
@@ -41,10 +42,9 @@ import java.util.List;
 public class HelioKnife4jAutoConfiguration {
 
     @Value(value = "${sa-token.token-name:Authorization}")
-    private String HEADER_TOKEN_NAME;
+    private String headerTokenName;
 
-    @Resource
-    private HelioProperties helioProperties;
+    private final HelioProperties helioProperties;
 
 
     @Bean
@@ -80,7 +80,7 @@ public class HelioKnife4jAutoConfiguration {
         AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
         authorizationScopes[0] = authorizationScope;
 
-        references.add(new SecurityReference(HEADER_TOKEN_NAME, authorizationScopes));
+        references.add(new SecurityReference(headerTokenName, authorizationScopes));
 
         return SecurityContext.builder()
                 .securityReferences(references)
@@ -88,7 +88,7 @@ public class HelioKnife4jAutoConfiguration {
     }
 
     private ApiKey apiKeyOfToken() {
-        return new ApiKey(HEADER_TOKEN_NAME, HEADER_TOKEN_NAME, "header");
+        return new ApiKey(headerTokenName, headerTokenName, "header");
     }
 
 }

+ 1 - 1
helio-starter-redis/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>helio-starters</artifactId>
         <groupId>cc.uncarbon.framework</groupId>
-        <version>1.4.0</version>
+        <version>1.5.0</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <modelVersion>4.0.0</modelVersion>

+ 24 - 18
helio-starter-redis/src/main/java/cc/uncarbon/framework/redis/config/RedisAutoConfiguration.java → helio-starter-redis/src/main/java/cc/uncarbon/framework/redis/config/HelioRedisAutoConfiguration.java

@@ -2,6 +2,8 @@ package cc.uncarbon.framework.redis.config;
 
 import cc.uncarbon.framework.redis.lock.RedisDistributedLock;
 import cc.uncarbon.framework.redis.lock.impl.RedisDistributedLockImpl;
+import lombok.RequiredArgsConstructor;
+import org.redisson.api.RedissonClient;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.cache.annotation.EnableCaching;
@@ -13,8 +15,6 @@ import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
 import org.springframework.data.redis.serializer.StringRedisSerializer;
 
-import javax.annotation.Resource;
-
 
 /**
  * Redis 配置类
@@ -22,45 +22,51 @@ import javax.annotation.Resource;
  * @author zuihou
  * @author Mark sunlightcs@gmail.com
  */
+@RequiredArgsConstructor
 @EnableCaching
 @ConditionalOnClass(RedisConnectionFactory.class)
 @Configuration
-public class RedisAutoConfiguration {
+public class HelioRedisAutoConfiguration {
 
-    @Resource
-    private RedisConnectionFactory factory;
+    private final RedisConnectionFactory factory;
 
+    private final RedissonClient redissonClient;
 
-    @Bean
-    public RedisTemplate<String, Object> redisTemplate() {
 
-        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
+    @Bean
+    @ConditionalOnMissingBean
+    public RedisTemplate<?, ?> redisTemplate() {
+        RedisTemplate<?, ?> redisTemplate = new RedisTemplate<>();
 
-        redisTemplate.setKeySerializer(new StringRedisSerializer());
-        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
-        redisTemplate.setHashValueSerializer(new StringRedisSerializer());
-        /*
-        还有一种StringRedisSerializer序列化器
-         */
-        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
         redisTemplate.setConnectionFactory(factory);
 
+        // 指定相应的序列化方案
+        StringRedisSerializer keySerializer = new StringRedisSerializer();
+        GenericJackson2JsonRedisSerializer valueSerializer = new GenericJackson2JsonRedisSerializer();
+
+        redisTemplate.setKeySerializer(keySerializer);
+        redisTemplate.setHashKeySerializer(keySerializer);
+
+        redisTemplate.setValueSerializer(valueSerializer);
+        redisTemplate.setHashValueSerializer(valueSerializer);
+
         return redisTemplate;
     }
 
     /**
-     * 基于Redisson的分布式锁
+     * 基于 Redisson 的分布式锁
      */
     @Bean
     @ConditionalOnMissingBean
     public RedisDistributedLock redisDistributedLock() {
-        return new RedisDistributedLockImpl();
+        return new RedisDistributedLockImpl(redissonClient);
     }
 
     /**
-     * Key名生成规则
+     * 缓存键名生成规则
      */
     @Bean
+    @ConditionalOnMissingBean
     public KeyGenerator keyGenerator() {
         return (target, method, objects) -> {
             StringBuilder sb = new StringBuilder();

+ 3 - 3
helio-starter-redis/src/main/java/cc/uncarbon/framework/redis/lock/impl/RedisDistributedLockImpl.java

@@ -1,11 +1,11 @@
 package cc.uncarbon.framework.redis.lock.impl;
 
 import cc.uncarbon.framework.redis.lock.RedisDistributedLock;
+import lombok.AllArgsConstructor;
 import org.redisson.api.RLock;
 import org.redisson.api.RedissonClient;
 import org.springframework.stereotype.Service;
 
-import javax.annotation.Resource;
 import java.util.concurrent.TimeUnit;
 
 /**
@@ -14,6 +14,7 @@ import java.util.concurrent.TimeUnit;
  * @author dcy
  * @author Uncarbon
  */
+@AllArgsConstructor
 @Service
 public class RedisDistributedLockImpl implements RedisDistributedLock {
 
@@ -22,8 +23,7 @@ public class RedisDistributedLockImpl implements RedisDistributedLock {
      */
     private static final String LOCK_KEY_PREFIX = "distributedLock:";
 
-    @Resource
-    private RedissonClient redissonClient;
+    private final RedissonClient redissonClient;
 
 
     @Override

+ 1 - 1
helio-starter-redis/src/main/resources/META-INF/spring.factories

@@ -1,2 +1,2 @@
 org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
-  cc.uncarbon.framework.redis.config.RedisAutoConfiguration
+  cc.uncarbon.framework.redis.config.HelioRedisAutoConfiguration

+ 1 - 1
helio-starter-rocketmq-aliyun/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>helio-starters</artifactId>
         <groupId>cc.uncarbon.framework</groupId>
-        <version>1.4.0</version>
+        <version>1.5.0</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <modelVersion>4.0.0</modelVersion>

+ 19 - 2
helio-starter-rocketmq-aliyun/src/main/java/cc/uncarbon/framework/rocketmq/annotation/RocketListener.java

@@ -17,10 +17,14 @@
 package cc.uncarbon.framework.rocketmq.annotation;
 
 import com.aliyun.openservices.ons.api.PropertyValueConst;
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
 import org.springframework.stereotype.Component;
 
-import java.lang.annotation.*;
-
 /**
  * ClassName: RocketListener
  * Description:
@@ -49,5 +53,18 @@ public @interface RocketListener {
 	 */
 	String messageModel() default PropertyValueConst.CLUSTERING;
 
+	/**
+	 * 消费线程数量,特定需求可以单独指定,默认同配置文件
+	 *
+	 * @return String
+	 */
+	String consumeThreadNums();
+
+	/**
+	 * 一次最大拉取消费数量,特定需求可以单独指定,默认同配置文件
+	 *
+	 * @return String
+	 */
+	String consumeBatchSize();
 
 }

+ 5 - 7
helio-starter-rocketmq-aliyun/src/main/java/cc/uncarbon/framework/rocketmq/config/AliyunRocketAutoConfiguration.java

@@ -22,12 +22,12 @@ import cc.uncarbon.framework.rocketmq.container.RocketProducerContainer;
 import cc.uncarbon.framework.rocketmq.core.serializer.JacksonJsonSerializer;
 import cc.uncarbon.framework.rocketmq.core.serializer.RocketSerializer;
 import cc.uncarbon.framework.rocketmq.props.AliyunRocketProperties;
+import lombok.RequiredArgsConstructor;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.boot.context.properties.EnableConfigurationProperties;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 
-import javax.annotation.Resource;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 
@@ -38,18 +38,16 @@ import java.util.concurrent.ConcurrentHashMap;
  * @author ThierrySquirrel
  * @author Uncarbon
  */
+@RequiredArgsConstructor
 @EnableConfigurationProperties(value = {AliyunRocketProperties.class})
 @Configuration
 public class AliyunRocketAutoConfiguration {
 
-    @Resource
-    private AliyunRocketProperties rocketProperties;
+    private final AliyunRocketProperties rocketProperties;
 
-    @Resource
-    private Map<String, Object> consumerContainer;
+    private final Map<String, Object> consumerContainer;
 
-    @Resource
-    private RocketSerializer rocketSerializer;
+    private final RocketSerializer rocketSerializer;
 
 
     @Bean

+ 15 - 8
helio-starter-rocketmq-aliyun/src/main/java/cc/uncarbon/framework/rocketmq/core/factory/ConsumerPropertiesFactory.java

@@ -19,7 +19,7 @@ package cc.uncarbon.framework.rocketmq.core.factory;
 import cc.uncarbon.framework.rocketmq.annotation.RocketListener;
 import cc.uncarbon.framework.rocketmq.props.AliyunRocketProperties;
 import com.aliyun.openservices.ons.api.PropertyKeyConst;
-
+import java.util.Objects;
 import java.util.Properties;
 
 /**
@@ -39,14 +39,21 @@ public class ConsumerPropertiesFactory {
 
 		Properties properties = PropertiesFactory.createProperties(rocketProperties);
 
-		properties.put(PropertyKeyConst.GROUP_ID, rocketListener.groupID());
-		properties.put(PropertyKeyConst.MessageModel, rocketListener.messageModel());
-		properties.put(PropertyKeyConst.ConsumeThreadNums, rocketProperties.getConsumeThreadNums());
-		properties.put(PropertyKeyConst.MaxReconsumeTimes, rocketProperties.getMaxReconsumeTimes());
-		properties.put(PropertyKeyConst.ConsumeTimeout, rocketProperties.getConsumeTimeout());
+        properties.put(PropertyKeyConst.GROUP_ID, rocketListener.groupID());
+        properties.put(PropertyKeyConst.MessageModel, rocketListener.messageModel());
+        properties.put(PropertyKeyConst.ConsumeThreadNums,
+				// 消费线程数量,特定需求可以单独指定
+                Objects.nonNull(rocketListener.consumeThreadNums()) ? rocketListener.consumeThreadNums()
+                        : rocketProperties.getConsumeThreadNums());
+        properties.put(PropertyKeyConst.MaxReconsumeTimes, rocketProperties.getMaxReconsumeTimes());
+        properties.put(PropertyKeyConst.ConsumeTimeout, rocketProperties.getConsumeTimeout());
 
+		if (Objects.nonNull(rocketListener.consumeBatchSize())) {
+			// 一次最大拉取消费数量,特定需求可以单独指定
+			properties.put(PropertyKeyConst.MAX_BATCH_MESSAGE_COUNT, rocketListener.consumeBatchSize());
+		}
 
-		return properties;
+        return properties;
 
-	}
+    }
 }

+ 1 - 1
helio-starter-satoken/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>helio-starters</artifactId>
         <groupId>cc.uncarbon.framework</groupId>
-        <version>1.4.0</version>
+        <version>1.5.0</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <modelVersion>4.0.0</modelVersion>

+ 22 - 8
helio-starter-satoken/src/main/java/cc/uncarbon/framework/satoken/interceptor/DefaultSaTokenParseInterceptor.java

@@ -1,15 +1,18 @@
 package cc.uncarbon.framework.satoken.interceptor;
 
+import cc.uncarbon.framework.core.context.TenantContext;
+import cc.uncarbon.framework.core.context.TenantContextHolder;
 import cc.uncarbon.framework.core.context.UserContext;
 import cc.uncarbon.framework.core.context.UserContextHolder;
+import cc.uncarbon.framework.satoken.util.IPUtil;
 import cn.dev33.satoken.stp.StpUtil;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import lombok.NonNull;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.web.servlet.AsyncHandlerInterceptor;
 import org.springframework.web.servlet.resource.ResourceHttpRequestHandler;
 
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
 /**
  * 从请求头解析用户信息,并赋值到上下文
  *
@@ -17,20 +20,31 @@ import javax.servlet.http.HttpServletResponse;
  */
 @Slf4j
 public class DefaultSaTokenParseInterceptor implements AsyncHandlerInterceptor {
+
     @Override
-    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
+    public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler) {
         if (handler instanceof ResourceHttpRequestHandler) {
             // 直接放行静态资源
             return true;
         }
 
-        // 从请求头解析用户上下文
+        // SA-Token 会自动从请求头中解析 token,所以这里可以直接拿到对应 session,从而取出业务字段
         if (StpUtil.isLogin()) {
-            UserContext currentUser = (UserContext) StpUtil.getSession().get("userContext");
-            log.debug("[SA-Token] 从请求头解析出用户上下文 >> {}", currentUser);
-            UserContextHolder.setUserContext(currentUser);
+            UserContext userContext = (UserContext) StpUtil.getSession().get(UserContext.CAMEL_NAME);
+
+            // 获取用户公网IP
+            userContext.setClientIP(IPUtil.getClientIPAddress(request));
+            UserContextHolder.setUserContext(userContext);
+
+            log.debug("[SA-Token] 从请求头解析出用户上下文 >> {}", userContext);
+
+            // 赋值租户上下文
+            TenantContext tenantContext = (TenantContext) StpUtil.getSession().get(TenantContext.CAMEL_NAME);
+            TenantContextHolder.setTenantContext(tenantContext);
+
         } else {
             UserContextHolder.setUserContext(null);
+            TenantContextHolder.setTenantContext(null);
         }
 
         return true;

+ 42 - 0
helio-starter-satoken/src/main/java/cc/uncarbon/framework/satoken/util/IPUtil.java

@@ -0,0 +1,42 @@
+package cc.uncarbon.framework.satoken.util;
+
+import javax.servlet.http.HttpServletRequest;
+import lombok.experimental.UtilityClass;
+
+/**
+ * IP地址工具类
+ * 方法来自于互联网
+ * @author Uncarbon
+ */
+@UtilityClass
+public class IPUtil {
+    private static final String[] HEADERS_TO_TRY = {
+            "X-Forwarded-For",
+            "Proxy-Client-IP",
+            "WL-Proxy-Client-IP",
+            "HTTP_X_FORWARDED_FOR",
+            "HTTP_X_FORWARDED",
+            "HTTP_X_CLUSTER_CLIENT_IP",
+            "HTTP_CLIENT_IP",
+            "HTTP_FORWARDED_FOR",
+            "HTTP_FORWARDED",
+            "HTTP_VIA",
+            "REMOTE_ADDR",
+            "X-Real-IP"};
+
+    /***
+     * 获取客户端IP地址(可以穿透代理)
+     * @param request 请求对象
+     * @return IP地址
+     */
+    public String getClientIPAddress(HttpServletRequest request) {
+        for (String header : HEADERS_TO_TRY) {
+            String ip = request.getHeader(header);
+            if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
+                return ip;
+            }
+        }
+        return request.getRemoteAddr();
+    }
+
+}

+ 1 - 1
helio-starter-seata/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>helio-starters</artifactId>
         <groupId>cc.uncarbon.framework</groupId>
-        <version>1.4.0</version>
+        <version>1.5.0</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <modelVersion>4.0.0</modelVersion>

+ 35 - 0
helio-starter-tenant/pom.xml

@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>helio-starters</artifactId>
+        <groupId>cc.uncarbon.framework</groupId>
+        <version>1.5.0</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>helio-starter-tenant</artifactId>
+
+    <properties>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>cc.uncarbon.framework</groupId>
+            <artifactId>helio-core</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>cc.uncarbon.framework</groupId>
+            <artifactId>helio-starter-crud</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
+            <optional>true</optional>
+        </dependency>
+    </dependencies>
+
+</project>

+ 22 - 0
helio-starter-tenant/src/main/java/cc/uncarbon/framework/tenant/annotation/EnableGlobalTenantDataSource.java

@@ -0,0 +1,22 @@
+package cc.uncarbon.framework.tenant.annotation;
+
+import cc.uncarbon.framework.tenant.config.GlobalTenantDataSourceConfiguration;
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import org.springframework.context.annotation.Import;
+
+/**
+ * 启用基于全局 AOP 的数据源级多租户
+ *
+ * @author Uncarbon
+ */
+@Import(value = GlobalTenantDataSourceConfiguration.class)
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface EnableGlobalTenantDataSource {
+
+}

+ 19 - 0
helio-starter-tenant/src/main/java/cc/uncarbon/framework/tenant/annotation/IgnoreTenantDataSource.java

@@ -0,0 +1,19 @@
+package cc.uncarbon.framework.tenant.annotation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 数据源级多租户专用,忽略 AOP 拦截
+ *
+ * @author Uncarbon
+ */
+@Target({ElementType.TYPE, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface IgnoreTenantDataSource {
+
+}

+ 49 - 0
helio-starter-tenant/src/main/java/cc/uncarbon/framework/tenant/config/GlobalTenantDataSourceConfiguration.java

@@ -0,0 +1,49 @@
+package cc.uncarbon.framework.tenant.config;
+
+import cc.uncarbon.framework.crud.dynamicdatasource.HelioDynamicDataSourceRegistry;
+import cc.uncarbon.framework.tenant.tenantdatasource.GlobalTenantDataSourceAdvisor;
+import cc.uncarbon.framework.tenant.tenantdatasource.GlobalTenantDataSourceInterceptor;
+import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
+import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceProperties;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Role;
+
+/**
+ * 基于全局 AOP 的数据源级多租户配置类
+ *
+ * @author Uncarbon
+ */
+public class GlobalTenantDataSourceConfiguration {
+
+    @Bean
+    @ConditionalOnMissingBean
+    public DynamicRoutingDataSource dynamicRoutingDataSource() {
+        return new DynamicRoutingDataSource();
+    }
+
+    @Bean
+    @ConditionalOnMissingBean
+    @Role(value = BeanDefinition.ROLE_INFRASTRUCTURE)
+    public GlobalTenantDataSourceInterceptor interceptor(
+            HelioDynamicDataSourceRegistry dataSourceRegistry
+    ) {
+        return new GlobalTenantDataSourceInterceptor(dataSourceRegistry);
+    }
+
+
+    @Bean
+    @ConditionalOnMissingBean
+    @Role(value = BeanDefinition.ROLE_INFRASTRUCTURE)
+    public GlobalTenantDataSourceAdvisor tenantDataSourceGlobalAdvisor(
+            GlobalTenantDataSourceInterceptor interceptor,
+            DynamicDataSourceProperties properties
+    ) {
+        GlobalTenantDataSourceAdvisor advisor = new GlobalTenantDataSourceAdvisor(interceptor);
+        // 数值越高,优先度越低
+        advisor.setOrder(properties.getAop().getOrder() + 1);
+        return advisor;
+    }
+
+}

+ 43 - 0
helio-starter-tenant/src/main/java/cc/uncarbon/framework/tenant/config/HelioTenantAutoConfiguration.java

@@ -0,0 +1,43 @@
+package cc.uncarbon.framework.tenant.config;
+
+import cc.uncarbon.framework.core.props.HelioProperties;
+import cc.uncarbon.framework.crud.support.TenantSupport;
+import cc.uncarbon.framework.crud.support.impl.DefaultTenantSupport;
+import cc.uncarbon.framework.tenant.support.TenantDataSourceSupport;
+import cc.uncarbon.framework.tenant.support.TenantLineSupport;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.support.GenericApplicationContext;
+
+/**
+ * Helio 多租户自动配置类
+ * 当启用多租户功能,且多租户隔离级别配置正确,则向 IoC 容器注入对应处理 Bean
+ */
+@Configuration
+@RequiredArgsConstructor
+public class HelioTenantAutoConfiguration {
+
+    private final HelioProperties helioProperties;
+
+
+    @Bean
+    public TenantSupport tenantSupport(GenericApplicationContext applicationContext) {
+        if (!Boolean.TRUE.equals(helioProperties.getTenant().getEnabled())) {
+            // 引入了 starter,但未启用多租户
+            return new DefaultTenantSupport();
+        }
+
+        switch (helioProperties.getTenant().getIsolateLevel()) {
+            case LINE:
+                // 行级
+                return new TenantLineSupport();
+            case DATASOURCE:
+                // 数据源级
+                return new TenantDataSourceSupport();
+            default:
+                throw new IllegalArgumentException("启用多租户功能后,请正确配置对应的多租户隔离级别 (helio.tenant.isolate-level) ");
+        }
+    }
+
+}

+ 29 - 0
helio-starter-tenant/src/main/java/cc/uncarbon/framework/tenant/support/TenantDataSourceSupport.java

@@ -0,0 +1,29 @@
+package cc.uncarbon.framework.tenant.support;
+
+import cc.uncarbon.framework.core.props.HelioProperties;
+import cc.uncarbon.framework.crud.support.TenantSupport;
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.ClassUtils;
+
+/**
+ * 多租户支持-数据源级
+ *
+ * @author Uncarbon
+ */
+@Slf4j
+public class TenantDataSourceSupport implements TenantSupport {
+
+    @Override
+    public void support(HelioProperties helioProperties, MybatisPlusInterceptor interceptor) {
+        log.info("\n\n[多租户支持] >> 隔离级别: 数据源级");
+
+        System.err.println("数据源级多租户支持还处于试验阶段,请经过测试后再投入生产使用!");
+
+        try {
+            Class.forName("com.baomidou.dynamic.datasource.DynamicRoutingDataSource", false, ClassUtils.getDefaultClassLoader());
+        } catch (ClassNotFoundException cnfe) {
+            System.err.println("\n\n ERROR: 没有找到 dynamic-datasource-spring-boot-starter 依赖,请检查是否引入");
+        }
+    }
+}

+ 88 - 0
helio-starter-tenant/src/main/java/cc/uncarbon/framework/tenant/support/TenantLineSupport.java

@@ -0,0 +1,88 @@
+package cc.uncarbon.framework.tenant.support;
+
+import cc.uncarbon.framework.core.constant.HelioConstant;
+import cc.uncarbon.framework.core.context.TenantContextHolder;
+import cc.uncarbon.framework.core.props.HelioProperties;
+import cc.uncarbon.framework.crud.support.TenantSupport;
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler;
+import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
+import java.util.Collection;
+import java.util.Objects;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.expression.LongValue;
+
+/**
+ * 多租户支持-行级
+ *
+ * @author Uncarbon
+ */
+@Slf4j
+public class TenantLineSupport implements TenantSupport {
+
+    @Override
+    public void support(HelioProperties helioProperties, MybatisPlusInterceptor interceptor) {
+        Collection<String> ignoredTables = helioProperties.getTenant().getIgnoredTables();
+
+        // 添加行级租户内联拦截器
+        interceptor.addInnerInterceptor(
+                new TenantLineInnerInterceptor(
+                        new HelioLineTenantHandler(
+                                helioProperties.getTenant().getPrivilegedTenantId(),
+                                ignoredTables)
+                )
+        );
+
+        log.info("\n\n[多租户支持] >> 隔离级别: 行级");
+
+        System.err.println("以下数据表不参与租户隔离: " + ignoredTables);
+    }
+
+
+    @AllArgsConstructor
+    public static class HelioLineTenantHandler implements TenantLineHandler {
+
+        /**
+         * 特权租户ID
+         */
+        private final Long privilegedTenantId;
+
+        /**
+         * 忽略租户隔离的表
+         */
+        private final Collection<String> ignoredTables;
+
+
+        @Override
+        public Expression getTenantId() {
+            Long currentTenantId = TenantContextHolder.getTenantId();
+            if (currentTenantId == null) {
+                return null;
+            }
+
+            return new LongValue(currentTenantId);
+        }
+
+        @Override
+        public String getTenantIdColumn() {
+            return HelioConstant.CRUD.COLUMN_TENANT_ID;
+        }
+
+        @Override
+        public boolean ignoreTable(String tableName) {
+            Long currentTenantId = TenantContextHolder.getTenantId();
+
+            if (Objects.nonNull(privilegedTenantId)
+                    && Objects.nonNull(currentTenantId)
+                    && privilegedTenantId.equals(currentTenantId)) {
+                return true;
+            }
+
+            return ignoredTables.contains(tableName);
+        }
+
+    }
+
+}

+ 56 - 0
helio-starter-tenant/src/main/java/cc/uncarbon/framework/tenant/support/TenantTableSupport.java

@@ -0,0 +1,56 @@
+package cc.uncarbon.framework.tenant.support;
+
+import cc.uncarbon.framework.core.context.TenantContextHolder;
+import cc.uncarbon.framework.core.props.HelioProperties;
+import cc.uncarbon.framework.crud.support.TenantSupport;
+import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.handler.TableNameHandler;
+import com.baomidou.mybatisplus.extension.plugins.inner.DynamicTableNameInnerInterceptor;
+import java.util.Collection;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * 多租户支持-表级
+ * @deprecated 后来发现并不是太适合实践使用,仅保留本类
+ *
+ * @author Uncarbon
+ */
+@Slf4j
+@Deprecated
+public class TenantTableSupport implements TenantSupport {
+
+    @Override
+    public void support(HelioProperties helioProperties, MybatisPlusInterceptor interceptor) {
+        Collection<String> ignoredTables = helioProperties.getTenant().getIgnoredTables();
+
+        DynamicTableNameInnerInterceptor innerInterceptor = new DynamicTableNameInnerInterceptor();
+        innerInterceptor.setTableNameHandler(new HelioTableTenantHandler(ignoredTables));
+
+        // 添加表级租户内联拦截器
+        interceptor.addInnerInterceptor(innerInterceptor);
+
+        log.info("\n\n[多租户支持] >> 隔离级别: 表级,以下数据表不参与租户隔离: {}\n",
+                ignoredTables);
+    }
+
+    @AllArgsConstructor
+    public static class HelioTableTenantHandler implements TableNameHandler {
+
+        /**
+         * 忽略租户隔离的表
+         */
+        private Collection<String> ignoredTables;
+
+        @Override
+        public String dynamicTableName(String sql, String tableName) {
+            if (ignoredTables.contains(tableName)) {
+                return tableName;
+            }
+
+            // 拼接新表名
+            return String.format("%s_%s", tableName, TenantContextHolder.getTenantId());
+        }
+
+    }
+}

+ 52 - 0
helio-starter-tenant/src/main/java/cc/uncarbon/framework/tenant/tenantdatasource/GlobalTenantDataSourceAdvisor.java

@@ -0,0 +1,52 @@
+package cc.uncarbon.framework.tenant.tenantdatasource;
+
+import lombok.Getter;
+import lombok.NonNull;
+import lombok.extern.slf4j.Slf4j;
+import org.aopalliance.aop.Advice;
+import org.springframework.aop.Pointcut;
+import org.springframework.aop.aspectj.AspectJExpressionPointcut;
+import org.springframework.aop.support.AbstractPointcutAdvisor;
+import org.springframework.aop.support.ComposablePointcut;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.BeanFactory;
+import org.springframework.beans.factory.BeanFactoryAware;
+
+/**
+ * 数据源级多租户全局 AOP 织入点
+ *
+ * @author chill
+ * @author Uncarbon
+ */
+@Slf4j
+@Getter
+public class GlobalTenantDataSourceAdvisor extends AbstractPointcutAdvisor implements BeanFactoryAware {
+
+    private final Advice advice;
+
+    private final Pointcut pointcut;
+
+    public GlobalTenantDataSourceAdvisor(@NonNull GlobalTenantDataSourceInterceptor globalTenantDataSourceInterceptor) {
+        this.advice = globalTenantDataSourceInterceptor;
+        this.pointcut = buildPointcut();
+    }
+
+    @Override
+    public void setBeanFactory(@NonNull BeanFactory beanFactory) throws BeansException {
+        if (this.advice instanceof BeanFactoryAware) {
+            ((BeanFactoryAware) this.advice).setBeanFactory(beanFactory);
+        }
+    }
+
+    /**
+     * 配置需要拦截的切面
+     */
+    private Pointcut buildPointcut() {
+        AspectJExpressionPointcut cut = new AspectJExpressionPointcut();
+        cut.setExpression(
+                "@within(org.springframework.stereotype.Service)"
+                + " && !@annotation(cc.uncarbon.framework.tenant.annotation.IgnoreTenantDataSource)"
+        );
+        return new ComposablePointcut((Pointcut) cut);
+    }
+}

+ 48 - 0
helio-starter-tenant/src/main/java/cc/uncarbon/framework/tenant/tenantdatasource/GlobalTenantDataSourceInterceptor.java

@@ -0,0 +1,48 @@
+package cc.uncarbon.framework.tenant.tenantdatasource;
+
+import cc.uncarbon.framework.core.constant.HelioConstant.CRUD;
+import cc.uncarbon.framework.core.context.TenantContextHolder;
+import cc.uncarbon.framework.crud.dynamicdatasource.HelioDynamicDataSourceRegistry;
+import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
+import java.util.Objects;
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.aopalliance.intercept.MethodInterceptor;
+import org.aopalliance.intercept.MethodInvocation;
+
+/**
+ * 数据源级多租户全局 AOP 处理过程
+ *
+ * @author chill
+ * @author Uncarbon
+ */
+@RequiredArgsConstructor
+@Slf4j
+public class GlobalTenantDataSourceInterceptor implements MethodInterceptor {
+
+    private final HelioDynamicDataSourceRegistry dataSourceRegistry;
+
+
+    @Override
+    public Object invoke(@NonNull MethodInvocation invocation) throws Throwable {
+        Long currentTenantId = TenantContextHolder.getTenantId();
+        if (Objects.isNull(currentTenantId)) {
+            // 没有租户信息,直接跳过
+            return invocation.proceed();
+        }
+
+        try {
+            // 不适合纯数字作为数据源名称,给他拼个前缀
+            String tenantDataSourceName = CRUD.COLUMN_TENANT_ID + currentTenantId;
+            if (dataSourceRegistry.containsDataSource(tenantDataSourceName, true)) {
+                log.debug("[多租户][数据源级] 使用租户数据源 >> {}", tenantDataSourceName);
+                DynamicDataSourceContextHolder.push(tenantDataSourceName);
+            }
+
+            return invocation.proceed();
+        } finally {
+            DynamicDataSourceContextHolder.poll();
+        }
+    }
+}

+ 2 - 0
helio-starter-tenant/src/main/resources/META-INF/spring.factories

@@ -0,0 +1,2 @@
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
+  cc.uncarbon.framework.tenant.config.HelioTenantAutoConfiguration

+ 1 - 1
helio-starter-test/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>helio-starters</artifactId>
         <groupId>cc.uncarbon.framework</groupId>
-        <version>1.4.0</version>
+        <version>1.5.0</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <modelVersion>4.0.0</modelVersion>

+ 6 - 1
helio-starter-web/pom.xml

@@ -5,7 +5,7 @@
     <parent>
         <artifactId>helio-starters</artifactId>
         <groupId>cc.uncarbon.framework</groupId>
-        <version>1.4.0</version>
+        <version>1.5.0</version>
         <relativePath>../pom.xml</relativePath>
     </parent>
     <modelVersion>4.0.0</modelVersion>
@@ -22,6 +22,11 @@
             <artifactId>helio-core</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>cc.uncarbon.framework</groupId>
+            <artifactId>helio-starter-aop</artifactId>
+        </dependency>
+
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-web</artifactId>

+ 114 - 0
helio-starter-web/src/main/java/cc/uncarbon/framework/web/aspect/WebLoggingAspect.java

@@ -0,0 +1,114 @@
+package cc.uncarbon.framework.web.aspect;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+import javax.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Component;
+import org.springframework.web.context.request.RequestAttributes;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+
+/**
+ * Web 请求及响应日志切面
+ *
+ * @author Uncarbon
+ */
+@Aspect
+@Slf4j
+@Component
+@ConditionalOnProperty(value = "helio.web.logging.enabled", havingValue = "true")
+public class WebLoggingAspect {
+
+    /**
+     * Pointcut that matches all repositories, services and Web REST endpoints.
+     */
+    @Pointcut("within(@org.springframework.web.bind.annotation.RestController *)")
+    public void restControllerPointcut() {
+        // AOP Pointcut
+    }
+
+    /**
+     * Pointcut that matches all Spring beans in the application's main packages.
+     */
+    @Pointcut("within(cc.uncarbon.framework..*) || within(cc.uncarbon.module..*)")
+    public void applicationPackagePointcut() {
+        // AOP Pointcut
+    }
+
+    /**
+     * 日志记录请求体及响应体
+     * 切面一定程度上来说会有性能损失,影响 QPS,不建议在生产环境使用本切面类
+     *
+     * @param point 织入点,由切面引擎自动注入
+     * @return 织入点最终执行结果
+     * @throws Throwable 被抛出的异常
+     */
+    @Around("restControllerPointcut() && applicationPackagePointcut()")
+    public Object restControllerAround(ProceedingJoinPoint point) throws Throwable {
+        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
+        assert requestAttributes != null;
+        HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
+
+        /*
+        请求体记录
+         */
+        StringBuilder reqLog = new StringBuilder(500);
+        List<Object> reqLogParameters = new ArrayList<>();
+
+        reqLog.append("\n\n================  Request Start  ================\n");
+
+        // 打印路由  e.g. POST: /api/v1/doSth
+        reqLog.append("{}: {} \n");
+        reqLogParameters.add(request.getMethod());
+        reqLogParameters.add(request.getRequestURI());
+
+        // 打印入参
+        reqLog.append("Parameters: {} \n");
+        reqLogParameters.add(Arrays.stream(point.getArgs()).map(Object::toString).collect(Collectors.joining("\n")));
+
+        reqLog.append("================   Request End   ================\n");
+
+        log.debug(reqLog.toString(), reqLogParameters.toArray());
+
+        /*
+        响应体记录
+         */
+        StringBuilder repLog = new StringBuilder(200);
+        List<Object> repLogParameters = new ArrayList<>();
+
+        repLog.append("\n\n================  Response Start  ================\n");
+
+        long beginAt = System.nanoTime();
+        try {
+            Object result = point.proceed();
+            repLog.append("result: {} \n");
+            repLogParameters.add(result);
+
+            return result;
+        } catch (IllegalArgumentException e) {
+            log.error("Illegal argument: {} in {}.{}()", Arrays.toString(point.getArgs()),
+                    point.getSignature().getDeclaringTypeName(), point.getSignature().getName());
+
+            throw e;
+        } finally {
+            long duration = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - beginAt);
+            repLog.append("{}: {} (elapsed {} ms)\n");
+            repLogParameters.add(request.getMethod());
+            repLogParameters.add(request.getRequestURI());
+            repLogParameters.add(duration);
+            repLog.append("================   Response End   ================\n");
+
+            log.debug(repLog.toString(), repLogParameters.toArray());
+        }
+    }
+}

+ 6 - 3
helio-starter-web/src/main/java/cc/uncarbon/framework/web/config/JacksonExtendAutoConfiguration.java

@@ -21,6 +21,7 @@ import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
 
 import java.text.SimpleDateFormat;
 import java.time.ZoneId;
+import java.util.Locale;
 import java.util.TimeZone;
 
 /**
@@ -33,12 +34,12 @@ import java.util.TimeZone;
 @AutoConfigureBefore({JacksonAutoConfiguration.class})
 @Configuration
 public class JacksonExtendAutoConfiguration {
+
     @Bean
     public Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder() {
         return new Jackson2ObjectMapperBuilder();
     }
 
-
     /**
      * 全局配置 序列化和反序列化规则
      */
@@ -48,15 +49,17 @@ public class JacksonExtendAutoConfiguration {
         builder.simpleDateFormat(HelioConstant.Jackson.DATE_TIME_FORMAT);
         ObjectMapper objectMapper = builder.createXmlMapper(false).build();
 
+        Locale defaultLocale = Locale.getDefault();
+
         objectMapper
                 // 地点
-                .setLocale(HelioConstant.Jackson.LOCALE)
+                .setLocale(defaultLocale)
                 // 时区
                 .setTimeZone(TimeZone.getTimeZone(ZoneId.systemDefault()))
                 // 去掉默认的时间戳格式
                 .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
                 // Date参数日期格式
-                .setDateFormat(new SimpleDateFormat(HelioConstant.Jackson.DATE_TIME_FORMAT, HelioConstant.Jackson.LOCALE))
+                .setDateFormat(new SimpleDateFormat(HelioConstant.Jackson.DATE_TIME_FORMAT, defaultLocale))
                 // 该特性决定parser是否允许JSON字符串包含非引号控制字符(值小于32的ASCII字符,包含制表符和换行符)。
                 // 如果该属性关闭,则如果遇到这些字符,则会抛出异常。JSON标准说明书要求所有控制符必须使用引号,因此这是一个非标准的特性
                 .configure(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature(), true)

+ 4 - 2
helio-starter-web/src/main/java/cc/uncarbon/framework/web/config/XssAutoConfiguration.java

@@ -1,20 +1,22 @@
 package cc.uncarbon.framework.web.config;
 
 import cc.uncarbon.framework.web.xss.XssFilter;
+import javax.servlet.DispatcherType;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.boot.web.servlet.FilterRegistrationBean;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 
-import javax.servlet.DispatcherType;
-
 /**
  * 反XSS注入
+ *
  * @author Uncarbon
  */
 @Configuration
 public class XssAutoConfiguration {
 
     @Bean
+    @ConditionalOnMissingBean
     public FilterRegistrationBean<?> xssFilterRegistration() {
         FilterRegistrationBean<XssFilter> registration = new FilterRegistrationBean<>();
         registration.setDispatcherTypes(DispatcherType.REQUEST);

+ 0 - 55
helio-starter-web/src/main/java/cc/uncarbon/framework/web/interceptor/DefaultTenantParseInterceptor.java

@@ -1,55 +0,0 @@
-package cc.uncarbon.framework.web.interceptor;
-
-import cc.uncarbon.framework.core.context.TenantContext;
-import cc.uncarbon.framework.core.context.UserContext;
-import cc.uncarbon.framework.core.context.UserContextHolder;
-import cn.hutool.core.util.StrUtil;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.web.servlet.AsyncHandlerInterceptor;
-import org.springframework.web.servlet.resource.ResourceHttpRequestHandler;
-
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-/**
- * 从请求头解析租户信息,并赋值到上下文
- *
- * @author Uncarbon
- */
-@Slf4j
-public class DefaultTenantParseInterceptor implements AsyncHandlerInterceptor {
-    @Override
-    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
-        if (handler instanceof ResourceHttpRequestHandler) {
-            // 直接放行静态资源
-            return true;
-        }
-
-        /*
-        只有未登录的情况下,才从请求头的 X-TenantId 和 X-TenantName 字段读取租户信息
-        实际登录以后,用户上下文被更新,租户上下文随之固定
-
-        注:这里是读取明文,而且未考虑负数、自然对数、科学计数法等情况;出于安全考虑可以合并成一个字段,并密文形式传输,这里编码解密;注意异常处理
-         */
-        if (!UserContextHolder.isActuallyHold()) {
-            // 请求头里的租户ID,默认为-1
-            String headerTenantIdStr = StrUtil.blankToDefault(request.getHeader("X-TenantId"), "-1");
-            // 请求头里的租户名,可选
-            String headerTenantName = request.getHeader("X-TenantName");
-            if (StrUtil.isNumeric(headerTenantIdStr)) {
-                Long headerTenantId = Long.parseLong(headerTenantIdStr);
-                UserContextHolder.setUserContext(
-                        UserContext.builder()
-                                .relationalTenant(
-                                        TenantContext.builder()
-                                                .tenantId(headerTenantId)
-                                                .tenantName(headerTenantName)
-                                                .build()
-                                )
-                                .build()
-                );
-            }
-        }
-        return true;
-    }
-}

+ 3 - 1
helio-starter-web/src/main/java/cc/uncarbon/framework/web/util/IPUtil.java

@@ -1,12 +1,14 @@
 package cc.uncarbon.framework.web.util;
 
 import javax.servlet.http.HttpServletRequest;
+import lombok.experimental.UtilityClass;
 
 /**
  * IP地址工具类
  * 方法来自于互联网
  * @author Uncarbon
  */
+@UtilityClass
 public class IPUtil {
     private static final String[] HEADERS_TO_TRY = {
             "X-Forwarded-For",
@@ -27,7 +29,7 @@ public class IPUtil {
      * @param request 请求对象
      * @return IP地址
      */
-    public static String getClientIPAddress(HttpServletRequest request) {
+    public String getClientIPAddress(HttpServletRequest request) {
         for (String header : HEADERS_TO_TRY) {
             String ip = request.getHeader(header);
             if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {

+ 38 - 11
pom.xml

@@ -15,6 +15,7 @@
         <module>helio-starter-rocketmq-aliyun</module>
         <module>helio-starter-satoken</module>
         <module>helio-starter-seata</module>
+        <module>helio-starter-tenant</module>
         <module>helio-starter-test</module>
         <module>helio-starter-web</module>
     </modules>
@@ -22,45 +23,47 @@
     <parent>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-parent</artifactId>
-        <version>2.5.9</version>
+        <version>2.6.6</version>
         <relativePath/>
     </parent>
 
     <groupId>cc.uncarbon.framework</groupId>
     <artifactId>helio-starters</artifactId>
-    <version>1.4.0</version>
+    <version>1.5.0</version>
 
     <properties>
         <!-- core versions -->
-        <helio-starters.version>1.4.0</helio-starters.version>
+        <helio-starters.version>1.5.0</helio-starters.version>
         <!-- JDK8则改为'1.8'  JDK11则改为'11' -->
         <java.version>1.8</java.version>
         <!-- 若变更Spring Boot版本, 上面parent依赖版本也要变 -->
-        <spring-boot.version>2.5.9</spring-boot.version>
+        <spring-boot.version>2.6.6</spring-boot.version>
 
 
         <!-- Spring Cloud versions -->
         <!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-dependencies -->
-        <spring-cloud.version>2020.0.5</spring-cloud.version>
+        <spring-cloud.version>2021.0.1</spring-cloud.version>
         <spring-cloud-starter-bootstrap.version>3.0.3</spring-cloud-starter-bootstrap.version>
 
         <!-- Spring Cloud Alibaba versions -->
-        <spring-cloud-alibaba.version>2021.1</spring-cloud-alibaba.version>
-        <!-- DON'T UPGRADE TO 3.X -->
-        <dubbo.version>2.7.12</dubbo.version>
+        <spring-cloud-alibaba.version>2021.0.1.0</spring-cloud-alibaba.version>
+        <dubbo.version>3.0.6</dubbo.version>
         <seata.vesion>1.4.2</seata.vesion>
-        <nacos-client.vesion>2.0.3</nacos-client.vesion>
+        <nacos-client.vesion>2.0.4</nacos-client.vesion>
 
 
         <!-- 3rd-party versions -->
-        <hutool.version>5.7.20</hutool.version>
-        <sa-token.version>1.28.0</sa-token.version>
+        <hutool.version>5.7.22</hutool.version>
+        <sa-token.version>1.29.0</sa-token.version>
         <transmittable-thread-local.version>2.12.1</transmittable-thread-local.version>
+        <!-- 3.5.1 版本根据 issue 显示,有多租户、枚举类等兼容性问题,暂缓升级 -->
         <mybatis-plus.version>3.4.3.4</mybatis-plus.version>
         <knife4j.version>3.0.3</knife4j.version>
         <redisson.version>3.16.7</redisson.version>
         <aspectj.version>1.9.6</aspectj.version>
         <ons-client.version>1.8.8.1.Final</ons-client.version>
+        <guava.version>31.1-jre</guava.version>
+        <mybatis-plus-dynamic-datasource.version>3.5.1</mybatis-plus-dynamic-datasource.version>
 
 
         <!-- DB Driver versions -->
@@ -160,6 +163,12 @@
                 <version>${helio-starters.version}</version>
             </dependency>
 
+            <dependency>
+                <groupId>cc.uncarbon.framework</groupId>
+                <artifactId>helio-starter-tenant</artifactId>
+                <version>${helio-starters.version}</version>
+            </dependency>
+
             <dependency>
                 <groupId>cc.uncarbon.framework</groupId>
                 <artifactId>helio-starter-test</artifactId>
@@ -252,9 +261,21 @@
                         <groupId>commons-lang</groupId>
                         <artifactId>commons-lang</artifactId>
                     </exclusion>
+
+                    <!-- 默认引入的 guava 版本过低,存在安全风险,手动指定新版本 -->
+                    <exclusion>
+                        <groupId>com.google.guava</groupId>
+                        <artifactId>guava</artifactId>
+                    </exclusion>
                 </exclusions>
             </dependency>
 
+            <dependency>
+                <groupId>com.google.guava</groupId>
+                <artifactId>guava</artifactId>
+                <version>${guava.version}</version>
+            </dependency>
+
 
             <!-- CRUD dependencies -->
             <!-- 常用数据库驱动 -->
@@ -294,6 +315,12 @@
                 <version>${mybatis-plus.version}</version>
             </dependency>
 
+            <dependency>
+                <groupId>com.baomidou</groupId>
+                <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
+                <version>${mybatis-plus-dynamic-datasource.version}</version>
+            </dependency>
+
 
             <!-- DUBBO dependencies -->
             <dependency>