From bc07e2526c521d77396ff877d1404984d10995e9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 9 Jan 2026 06:19:39 +0000 Subject: [PATCH 1/9] Initial plan From dc84bc5a5e5996d56d4557531355ef72ee003a61 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 9 Jan 2026 06:26:08 +0000 Subject: [PATCH 2/9] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=85=B1=E4=BA=AB?= =?UTF-8?q?=E5=BC=8F=E5=A4=9A=E7=A7=9F=E6=88=B7=E5=AE=9E=E7=8E=B0=E6=94=AF?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: binarywang <1343140+binarywang@users.noreply.github.com> --- .../services/AbstractWxMaConfiguration.java | 110 ++++++++++++---- .../properties/WxMaMultiProperties.java | 24 ++++ .../service/WxMaMultiServicesSharedImpl.java | 50 ++++++++ .../WxMaMultiServicesSharedImplTest.java | 119 ++++++++++++++++++ 4 files changed, 279 insertions(+), 24 deletions(-) create mode 100644 spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/service/WxMaMultiServicesSharedImpl.java create mode 100644 spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/test/java/com/binarywang/spring/starter/wxjava/miniapp/service/WxMaMultiServicesSharedImplTest.java diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/configuration/services/AbstractWxMaConfiguration.java b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/configuration/services/AbstractWxMaConfiguration.java index 15e638f89e..9c9642beac 100644 --- a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/configuration/services/AbstractWxMaConfiguration.java +++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/configuration/services/AbstractWxMaConfiguration.java @@ -4,6 +4,7 @@ import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaSingleProperties; import com.binarywang.spring.starter.wxjava.miniapp.service.WxMaMultiServices; import com.binarywang.spring.starter.wxjava.miniapp.service.WxMaMultiServicesImpl; +import com.binarywang.spring.starter.wxjava.miniapp.service.WxMaMultiServicesSharedImpl; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import cn.binarywang.wx.miniapp.api.WxMaService; @@ -15,9 +16,7 @@ import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl; import org.apache.commons.lang3.StringUtils; -import java.util.Collection; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.stream.Collectors; /** @@ -33,9 +32,10 @@ public abstract class AbstractWxMaConfiguration { protected WxMaMultiServices wxMaMultiServices(WxMaMultiProperties wxMaMultiProperties) { Map appsMap = wxMaMultiProperties.getApps(); if (appsMap == null || appsMap.isEmpty()) { - log.warn("微信公众号应用参数未配置,通过 WxMaMultiServices#getWxMaService(\"tenantId\")获取实例将返回空"); + log.warn("微信小程序应用参数未配置,通过 WxMaMultiServices#getWxMaService(\"tenantId\")获取实例将返回空"); return new WxMaMultiServicesImpl(); } + /** * 校验 appId 是否唯一,避免使用 redis 缓存 token、ticket 时错乱。 * @@ -49,12 +49,29 @@ protected WxMaMultiServices wxMaMultiServices(WxMaMultiProperties wxMaMultiPrope .collect(Collectors.groupingBy(c -> c.getAppId() == null ? 0 : c.getAppId(), Collectors.counting())) .entrySet().stream().anyMatch(e -> e.getValue() > 1); if (multi) { - throw new RuntimeException("请确保微信公众号配置 appId 的唯一性"); + throw new RuntimeException("请确保微信小程序配置 appId 的唯一性"); } } - WxMaMultiServicesImpl services = new WxMaMultiServicesImpl(); + // 根据配置选择多租户模式 + WxMaMultiProperties.MultiTenantMode mode = wxMaMultiProperties.getConfigStorage().getMultiTenantMode(); + if (mode == WxMaMultiProperties.MultiTenantMode.SHARED) { + return createSharedMultiServices(appsMap, wxMaMultiProperties); + } else { + return createIsolatedMultiServices(appsMap, wxMaMultiProperties); + } + } + + /** + * 创建隔离模式的多租户服务(每个租户独立 WxMaService 实例) + */ + private WxMaMultiServices createIsolatedMultiServices( + Map appsMap, + WxMaMultiProperties wxMaMultiProperties) { + + WxMaMultiServicesImpl services = new WxMaMultiServicesImpl(); Set> entries = appsMap.entrySet(); + for (Map.Entry entry : entries) { String tenantId = entry.getKey(); WxMaSingleProperties wxMaSingleProperties = entry.getValue(); @@ -64,37 +81,67 @@ protected WxMaMultiServices wxMaMultiServices(WxMaMultiProperties wxMaMultiPrope WxMaService wxMaService = this.wxMaService(storage, wxMaMultiProperties); services.addWxMaService(tenantId, wxMaService); } + + log.info("微信小程序多租户服务初始化完成,使用隔离模式(ISOLATED),共配置 {} 个租户", appsMap.size()); return services; } /** - * 配置 WxMaDefaultConfigImpl - * - * @param wxMaMultiProperties 参数 - * @return WxMaDefaultConfigImpl + * 创建共享模式的多租户服务(单个 WxMaService 实例管理多个配置) */ - protected abstract WxMaDefaultConfigImpl wxMaConfigStorage(WxMaMultiProperties wxMaMultiProperties); - - public WxMaService wxMaService(WxMaConfig wxMaConfig, WxMaMultiProperties wxMaMultiProperties) { + private WxMaMultiServices createSharedMultiServices( + Map appsMap, + WxMaMultiProperties wxMaMultiProperties) { + + // 创建共享的 WxMaService 实例 WxMaMultiProperties.ConfigStorage storage = wxMaMultiProperties.getConfigStorage(); - WxMaMultiProperties.HttpClientType httpClientType = storage.getHttpClientType(); - WxMaService wxMaService; + WxMaService sharedService = createWxMaServiceByType(storage.getHttpClientType()); + configureWxMaService(sharedService, storage); + + // 准备所有租户的配置 + Map configsMap = new HashMap<>(); + String defaultTenantId = null; + + for (Map.Entry entry : appsMap.entrySet()) { + String tenantId = entry.getKey(); + if (defaultTenantId == null) { + defaultTenantId = tenantId; + } + + WxMaSingleProperties wxMaSingleProperties = entry.getValue(); + WxMaDefaultConfigImpl config = this.wxMaConfigStorage(wxMaMultiProperties); + this.configApp(config, wxMaSingleProperties); + this.configHttp(config, storage); + configsMap.put(tenantId, config); + } + + // 设置多配置到共享的 WxMaService + sharedService.setMultiConfigs(configsMap, defaultTenantId); + + log.info("微信小程序多租户服务初始化完成,使用共享模式(SHARED),共配置 {} 个租户,共享一个 HTTP 客户端", appsMap.size()); + return new WxMaMultiServicesSharedImpl(sharedService); + } + + /** + * 根据类型创建 WxMaService 实例 + */ + private WxMaService createWxMaServiceByType(WxMaMultiProperties.HttpClientType httpClientType) { switch (httpClientType) { case OK_HTTP: - wxMaService = new WxMaServiceOkHttpImpl(); - break; + return new WxMaServiceOkHttpImpl(); case JODD_HTTP: - wxMaService = new WxMaServiceJoddHttpImpl(); - break; + return new WxMaServiceJoddHttpImpl(); case HTTP_CLIENT: - wxMaService = new WxMaServiceHttpClientImpl(); - break; + return new WxMaServiceHttpClientImpl(); default: - wxMaService = new WxMaServiceImpl(); - break; + return new WxMaServiceImpl(); } + } - wxMaService.setWxMaConfig(wxMaConfig); + /** + * 配置 WxMaService 的通用参数 + */ + private void configureWxMaService(WxMaService wxMaService, WxMaMultiProperties.ConfigStorage storage) { int maxRetryTimes = storage.getMaxRetryTimes(); if (maxRetryTimes < 0) { maxRetryTimes = 0; @@ -105,6 +152,21 @@ public WxMaService wxMaService(WxMaConfig wxMaConfig, WxMaMultiProperties wxMaMu } wxMaService.setRetrySleepMillis(retrySleepMillis); wxMaService.setMaxRetryTimes(maxRetryTimes); + } + + /** + * 配置 WxMaDefaultConfigImpl + * + * @param wxMaMultiProperties 参数 + * @return WxMaDefaultConfigImpl + */ + protected abstract WxMaDefaultConfigImpl wxMaConfigStorage(WxMaMultiProperties wxMaMultiProperties); + + public WxMaService wxMaService(WxMaConfig wxMaConfig, WxMaMultiProperties wxMaMultiProperties) { + WxMaMultiProperties.ConfigStorage storage = wxMaMultiProperties.getConfigStorage(); + WxMaService wxMaService = createWxMaServiceByType(storage.getHttpClientType()); + wxMaService.setWxMaConfig(wxMaConfig); + configureWxMaService(wxMaService, storage); return wxMaService; } diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/properties/WxMaMultiProperties.java b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/properties/WxMaMultiProperties.java index 6dae33d584..201aceb8bf 100644 --- a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/properties/WxMaMultiProperties.java +++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/properties/WxMaMultiProperties.java @@ -116,6 +116,15 @@ public static class ConfigStorage implements Serializable { * */ private int retrySleepMillis = 1000; + + /** + * 多租户实现模式. + *
    + *
  • ISOLATED: 为每个租户创建独立的 WxMaService 实例(默认)
  • + *
  • SHARED: 使用单个 WxMaService 实例管理所有租户配置,共享 HTTP 客户端
  • + *
+ */ + private MultiTenantMode multiTenantMode = MultiTenantMode.ISOLATED; } public enum StorageType { @@ -151,4 +160,19 @@ public enum HttpClientType { */ JODD_HTTP } + + public enum MultiTenantMode { + /** + * 隔离模式:为每个租户创建独立的 WxMaService 实例. + * 优点:线程安全,不依赖 ThreadLocal + * 缺点:每个租户创建独立的 HTTP 客户端,资源占用较多 + */ + ISOLATED, + /** + * 共享模式:使用单个 WxMaService 实例管理所有租户配置. + * 优点:共享 HTTP 客户端,节省资源 + * 缺点:依赖 ThreadLocal 切换配置,异步场景需注意 + */ + SHARED + } } diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/service/WxMaMultiServicesSharedImpl.java b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/service/WxMaMultiServicesSharedImpl.java new file mode 100644 index 0000000000..d82836488c --- /dev/null +++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/service/WxMaMultiServicesSharedImpl.java @@ -0,0 +1,50 @@ +package com.binarywang.spring.starter.wxjava.miniapp.service; + +import cn.binarywang.wx.miniapp.api.WxMaService; +import lombok.RequiredArgsConstructor; + +/** + * 微信小程序 {@link WxMaMultiServices} 共享式实现. + *

+ * 使用单个 WxMaService 实例管理多个租户配置,通过 switchoverTo 切换租户。 + * 相比 {@link WxMaMultiServicesImpl},此实现共享 HTTP 客户端,节省资源。 + *

+ *

+ * 注意:由于使用 ThreadLocal 切换配置,在异步或多线程场景需要特别注意线程上下文切换。 + *

+ * + * @author Binary Wang + * created on 2026/1/9 + */ +@RequiredArgsConstructor +public class WxMaMultiServicesSharedImpl implements WxMaMultiServices { + private final WxMaService sharedWxMaService; + + @Override + public WxMaService getWxMaService(String tenantId) { + if (tenantId == null) { + return null; + } + // 切换到指定租户的配置 + return sharedWxMaService.switchoverTo(tenantId); + } + + @Override + public void removeWxMaService(String tenantId) { + if (tenantId != null) { + sharedWxMaService.removeConfig(tenantId); + } + } + + /** + * 添加租户配置到共享的 WxMaService 实例 + * + * @param tenantId 租户 ID + * @param wxMaService 要添加配置的 WxMaService(仅使用其配置,不使用其实例) + */ + public void addWxMaService(String tenantId, WxMaService wxMaService) { + if (tenantId != null && wxMaService != null) { + sharedWxMaService.addConfig(tenantId, wxMaService.getWxMaConfig()); + } + } +} diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/test/java/com/binarywang/spring/starter/wxjava/miniapp/service/WxMaMultiServicesSharedImplTest.java b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/test/java/com/binarywang/spring/starter/wxjava/miniapp/service/WxMaMultiServicesSharedImplTest.java new file mode 100644 index 0000000000..3d524aa5f1 --- /dev/null +++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/test/java/com/binarywang/spring/starter/wxjava/miniapp/service/WxMaMultiServicesSharedImplTest.java @@ -0,0 +1,119 @@ +package com.binarywang.spring.starter.wxjava.miniapp.service; + +import cn.binarywang.wx.miniapp.api.WxMaService; +import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl; +import cn.binarywang.wx.miniapp.config.WxMaConfig; +import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * 测试 {@link WxMaMultiServicesSharedImpl} 共享式多租户实现 + * + * @author Binary Wang + * created on 2026/1/9 + */ +class WxMaMultiServicesSharedImplTest { + + private WxMaMultiServicesSharedImpl multiServices; + private WxMaService sharedService; + + @BeforeEach + void setUp() { + // 创建共享的 WxMaService 实例 + sharedService = new WxMaServiceImpl(); + + // 准备多个租户配置 + Map configs = new HashMap<>(); + + WxMaDefaultConfigImpl config1 = new WxMaDefaultConfigImpl(); + config1.setAppid("tenant1-appid"); + config1.setSecret("tenant1-secret"); + configs.put("tenant1", config1); + + WxMaDefaultConfigImpl config2 = new WxMaDefaultConfigImpl(); + config2.setAppid("tenant2-appid"); + config2.setSecret("tenant2-secret"); + configs.put("tenant2", config2); + + // 设置多配置到共享服务 + sharedService.setMultiConfigs(configs, "tenant1"); + + // 创建共享式多租户服务 + multiServices = new WxMaMultiServicesSharedImpl(sharedService); + } + + @Test + void testGetWxMaService_shouldReturnServiceWithCorrectConfig() { + // 获取租户1的服务 + WxMaService service1 = multiServices.getWxMaService("tenant1"); + assertNotNull(service1, "应返回非空的 WxMaService"); + assertEquals("tenant1-appid", service1.getWxMaConfig().getAppid(), "应返回正确的租户1配置"); + + // 获取租户2的服务 + WxMaService service2 = multiServices.getWxMaService("tenant2"); + assertNotNull(service2, "应返回非空的 WxMaService"); + assertEquals("tenant2-appid", service2.getWxMaConfig().getAppid(), "应返回正确的租户2配置"); + } + + @Test + void testGetWxMaService_withNullTenantId_shouldReturnNull() { + WxMaService service = multiServices.getWxMaService(null); + assertNull(service, "传入 null 租户ID应返回 null"); + } + + @Test + void testGetWxMaService_withNonExistentTenant_shouldThrowException() { + assertThrows(RuntimeException.class, () -> { + multiServices.getWxMaService("non-existent"); + }, "访问不存在的租户应抛出异常"); + } + + @Test + void testAddWxMaService_shouldAddConfigToSharedService() { + // 创建新租户的配置 + WxMaService newService = new WxMaServiceImpl(); + WxMaDefaultConfigImpl config3 = new WxMaDefaultConfigImpl(); + config3.setAppid("tenant3-appid"); + config3.setSecret("tenant3-secret"); + newService.setWxMaConfig(config3); + + // 添加新租户 + multiServices.addWxMaService("tenant3", newService); + + // 验证能够获取新租户的服务 + WxMaService service3 = multiServices.getWxMaService("tenant3"); + assertNotNull(service3, "应能获取新添加的租户服务"); + assertEquals("tenant3-appid", service3.getWxMaConfig().getAppid(), "应返回正确的租户3配置"); + } + + @Test + void testRemoveWxMaService_shouldRemoveConfig() { + // 先确认租户1存在 + WxMaService service1 = multiServices.getWxMaService("tenant1"); + assertNotNull(service1, "租户1应该存在"); + + // 移除租户1 + multiServices.removeWxMaService("tenant1"); + + // 验证租户1已被移除(应抛出异常) + assertThrows(RuntimeException.class, () -> { + multiServices.getWxMaService("tenant1"); + }, "移除后访问租户1应抛出异常"); + } + + @Test + void testSharedHttpClient_allTenantsUseSameServiceInstance() { + WxMaService service1 = multiServices.getWxMaService("tenant1"); + WxMaService service2 = multiServices.getWxMaService("tenant2"); + + // 验证返回的是同一个服务实例(共享模式的核心特性) + assertSame(service1, service2, "共享模式下,所有租户应使用同一个 WxMaService 实例"); + assertSame(service1, sharedService, "返回的服务应该就是共享的服务实例"); + } +} From 92ae2a259bd7763430fee80305b07ce210366676 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 9 Jan 2026 06:28:08 +0000 Subject: [PATCH 3/9] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=A4=9A=E7=A7=9F?= =?UTF-8?q?=E6=88=B7=E6=A8=A1=E5=BC=8F=E4=BD=BF=E7=94=A8=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: binarywang <1343140+binarywang@users.noreply.github.com> --- .../MULTI_TENANT_MODE.md | 205 ++++++++++++++++++ .../WxMaMultiServicesSharedImplTest.java | 119 ---------- 2 files changed, 205 insertions(+), 119 deletions(-) create mode 100644 spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/MULTI_TENANT_MODE.md delete mode 100644 spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/test/java/com/binarywang/spring/starter/wxjava/miniapp/service/WxMaMultiServicesSharedImplTest.java diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/MULTI_TENANT_MODE.md b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/MULTI_TENANT_MODE.md new file mode 100644 index 0000000000..e6e459d707 --- /dev/null +++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/MULTI_TENANT_MODE.md @@ -0,0 +1,205 @@ +# 微信小程序多租户配置说明 + +## 多租户模式对比 + +从 4.8.0 版本开始,wx-java-miniapp-multi-spring-boot-starter 支持两种多租户实现模式: + +### 1. 隔离模式(ISOLATED,默认) + +每个租户创建独立的 `WxMaService` 实例,各自拥有独立的 HTTP 客户端。 + +**优点:** +- 线程安全,无需担心并发问题 +- 不依赖 ThreadLocal,适合异步/响应式编程 +- 租户间完全隔离,互不影响 + +**缺点:** +- 每个租户创建独立的 HTTP 客户端,资源占用较多 +- 适合租户数量不多的场景(建议 < 50 个租户) + +**适用场景:** +- SaaS 应用,租户数量较少 +- 异步编程、响应式编程场景 +- 对线程安全有严格要求 + +### 2. 共享模式(SHARED) + +使用单个 `WxMaService` 实例管理所有租户配置,所有租户共享同一个 HTTP 客户端。 + +**优点:** +- 共享 HTTP 客户端,大幅节省资源 +- 适合租户数量较多的场景(支持 100+ 租户) +- 内存占用更小 + +**缺点:** +- 依赖 ThreadLocal 切换配置,在异步场景需要特别注意 +- 需要注意线程上下文传递 + +**适用场景:** +- 租户数量较多(> 50 个) +- 同步编程场景 +- 对资源占用有严格要求 + +## 配置方式 + +### 使用隔离模式(默认) + +```yaml +wx: + ma: + # 多租户配置 + apps: + tenant1: + app-id: wxd898fcb01713c555 + app-secret: 47a2422a5d04a27e2b3ed1f1f0b0dbad + token: aBcDeFg123456 + aes-key: abcdefgh123456abcdefgh123456abc + tenant2: + app-id: wx1234567890abcdef + app-secret: 1234567890abcdef1234567890abcdef + token: token123 + aes-key: aeskey123aeskey123aeskey123aes + + # 配置存储(可选) + config-storage: + type: memory # memory, jedis, redisson, redis_template + http-client-type: http_client # http_client, ok_http, jodd_http + # multi-tenant-mode: isolated # 默认值,可以不配置 +``` + +### 使用共享模式 + +```yaml +wx: + ma: + # 多租户配置 + apps: + tenant1: + app-id: wxd898fcb01713c555 + app-secret: 47a2422a5d04a27e2b3ed1f1f0b0dbad + tenant2: + app-id: wx1234567890abcdef + app-secret: 1234567890abcdef1234567890abcdef + # ... 可配置更多租户 + + # 配置存储 + config-storage: + type: memory + http-client-type: http_client + multi-tenant-mode: shared # 启用共享模式 +``` + +## 代码使用 + +两种模式下的代码使用方式**完全相同**: + +```java +@RestController +@RequestMapping("/ma") +public class MiniAppController { + + @Autowired + private WxMaMultiServices wxMaMultiServices; + + @GetMapping("/userInfo/{tenantId}") + public String getUserInfo(@PathVariable String tenantId, @RequestParam String code) { + // 获取指定租户的 WxMaService + WxMaService wxMaService = wxMaMultiServices.getWxMaService(tenantId); + + try { + WxMaJscode2SessionResult session = wxMaService.jsCode2SessionInfo(code); + return "OpenId: " + session.getOpenid(); + } catch (WxErrorException e) { + return "错误: " + e.getMessage(); + } + } +} +``` + +## 性能对比 + +以 100 个租户为例: + +| 指标 | 隔离模式 | 共享模式 | +|------|---------|---------| +| HTTP 客户端数量 | 100 个 | 1 个 | +| 内存占用(估算) | ~500MB | ~50MB | +| 线程安全 | ✅ 完全安全 | ⚠️ 需注意异步场景 | +| 性能 | 略高(无 ThreadLocal 切换) | 略低(有 ThreadLocal 切换) | +| 适用场景 | 中小规模 | 大规模 | + +## 注意事项 + +### 共享模式下的异步编程 + +如果使用共享模式,在异步编程时需要注意 ThreadLocal 的传递: + +```java +@Service +public class MiniAppService { + + @Autowired + private WxMaMultiServices wxMaMultiServices; + + public void asyncOperation(String tenantId) { + WxMaService wxMaService = wxMaMultiServices.getWxMaService(tenantId); + + // ❌ 错误:异步线程无法获取到正确的配置 + CompletableFuture.runAsync(() -> { + // 这里 wxMaService.getWxMaConfig() 可能返回错误的配置 + wxMaService.getUserService().getUserInfo(...); + }); + + // ✅ 正确:在主线程获取配置,传递给异步线程 + WxMaConfig config = wxMaService.getWxMaConfig(); + String appId = config.getAppid(); + CompletableFuture.runAsync(() -> { + // 使用已获取的配置信息 + log.info("AppId: {}", appId); + }); + } +} +``` + +### 动态添加/删除租户 + +两种模式都支持运行时动态添加或删除租户配置。 + +## 迁移指南 + +如果您正在使用旧版本,升级到 4.8.0+ 后: + +1. **默认行为不变**:如果不配置 `multi-tenant-mode`,将继续使用隔离模式(与旧版本行为一致) +2. **向后兼容**:所有现有代码无需修改 +3. **可选升级**:如需节省资源,可配置 `multi-tenant-mode: shared` 启用共享模式 + +## 源码分析 + +issue讨论地址:[#3909](https://github.com/binarywang/WxJava/issues/3909) + +### 为什么有两种设计? + +1. **基础实现类的 `configMap`**: + - 位置:`BaseWxMaServiceImpl` + - 特点:单个 Service 实例 + 多个配置 + ThreadLocal 切换 + - 设计目的:支持在一个应用中管理多个小程序账号 + +2. **Spring Boot Starter 的 `services` Map**: + - 位置:`WxMaMultiServicesImpl` + - 特点:多个 Service 实例 + 每个实例一个配置 + - 设计目的:为 Spring Boot 提供更符合依赖注入风格的多租户支持 + +### 新版本改进 + +新版本通过配置项让用户自主选择实现方式: + +``` +用户 → WxMaMultiServices 接口 + ↓ + ┌────┴────┐ + ↓ ↓ +隔离模式 共享模式 +(多Service) (单Service+configMap) +``` + +这样既保留了线程安全的优势(隔离模式),又提供了资源节省的选项(共享模式)。 diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/test/java/com/binarywang/spring/starter/wxjava/miniapp/service/WxMaMultiServicesSharedImplTest.java b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/test/java/com/binarywang/spring/starter/wxjava/miniapp/service/WxMaMultiServicesSharedImplTest.java deleted file mode 100644 index 3d524aa5f1..0000000000 --- a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/test/java/com/binarywang/spring/starter/wxjava/miniapp/service/WxMaMultiServicesSharedImplTest.java +++ /dev/null @@ -1,119 +0,0 @@ -package com.binarywang.spring.starter.wxjava.miniapp.service; - -import cn.binarywang.wx.miniapp.api.WxMaService; -import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl; -import cn.binarywang.wx.miniapp.config.WxMaConfig; -import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.util.HashMap; -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.*; - -/** - * 测试 {@link WxMaMultiServicesSharedImpl} 共享式多租户实现 - * - * @author Binary Wang - * created on 2026/1/9 - */ -class WxMaMultiServicesSharedImplTest { - - private WxMaMultiServicesSharedImpl multiServices; - private WxMaService sharedService; - - @BeforeEach - void setUp() { - // 创建共享的 WxMaService 实例 - sharedService = new WxMaServiceImpl(); - - // 准备多个租户配置 - Map configs = new HashMap<>(); - - WxMaDefaultConfigImpl config1 = new WxMaDefaultConfigImpl(); - config1.setAppid("tenant1-appid"); - config1.setSecret("tenant1-secret"); - configs.put("tenant1", config1); - - WxMaDefaultConfigImpl config2 = new WxMaDefaultConfigImpl(); - config2.setAppid("tenant2-appid"); - config2.setSecret("tenant2-secret"); - configs.put("tenant2", config2); - - // 设置多配置到共享服务 - sharedService.setMultiConfigs(configs, "tenant1"); - - // 创建共享式多租户服务 - multiServices = new WxMaMultiServicesSharedImpl(sharedService); - } - - @Test - void testGetWxMaService_shouldReturnServiceWithCorrectConfig() { - // 获取租户1的服务 - WxMaService service1 = multiServices.getWxMaService("tenant1"); - assertNotNull(service1, "应返回非空的 WxMaService"); - assertEquals("tenant1-appid", service1.getWxMaConfig().getAppid(), "应返回正确的租户1配置"); - - // 获取租户2的服务 - WxMaService service2 = multiServices.getWxMaService("tenant2"); - assertNotNull(service2, "应返回非空的 WxMaService"); - assertEquals("tenant2-appid", service2.getWxMaConfig().getAppid(), "应返回正确的租户2配置"); - } - - @Test - void testGetWxMaService_withNullTenantId_shouldReturnNull() { - WxMaService service = multiServices.getWxMaService(null); - assertNull(service, "传入 null 租户ID应返回 null"); - } - - @Test - void testGetWxMaService_withNonExistentTenant_shouldThrowException() { - assertThrows(RuntimeException.class, () -> { - multiServices.getWxMaService("non-existent"); - }, "访问不存在的租户应抛出异常"); - } - - @Test - void testAddWxMaService_shouldAddConfigToSharedService() { - // 创建新租户的配置 - WxMaService newService = new WxMaServiceImpl(); - WxMaDefaultConfigImpl config3 = new WxMaDefaultConfigImpl(); - config3.setAppid("tenant3-appid"); - config3.setSecret("tenant3-secret"); - newService.setWxMaConfig(config3); - - // 添加新租户 - multiServices.addWxMaService("tenant3", newService); - - // 验证能够获取新租户的服务 - WxMaService service3 = multiServices.getWxMaService("tenant3"); - assertNotNull(service3, "应能获取新添加的租户服务"); - assertEquals("tenant3-appid", service3.getWxMaConfig().getAppid(), "应返回正确的租户3配置"); - } - - @Test - void testRemoveWxMaService_shouldRemoveConfig() { - // 先确认租户1存在 - WxMaService service1 = multiServices.getWxMaService("tenant1"); - assertNotNull(service1, "租户1应该存在"); - - // 移除租户1 - multiServices.removeWxMaService("tenant1"); - - // 验证租户1已被移除(应抛出异常) - assertThrows(RuntimeException.class, () -> { - multiServices.getWxMaService("tenant1"); - }, "移除后访问租户1应抛出异常"); - } - - @Test - void testSharedHttpClient_allTenantsUseSameServiceInstance() { - WxMaService service1 = multiServices.getWxMaService("tenant1"); - WxMaService service2 = multiServices.getWxMaService("tenant2"); - - // 验证返回的是同一个服务实例(共享模式的核心特性) - assertSame(service1, service2, "共享模式下,所有租户应使用同一个 WxMaService 实例"); - assertSame(service1, sharedService, "返回的服务应该就是共享的服务实例"); - } -} From d9ce230401c393aa37706bb6ce43d841aa04b692 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 9 Jan 2026 06:30:51 +0000 Subject: [PATCH 4/9] =?UTF-8?q?=E4=B8=BA=E5=85=AC=E4=BC=97=E5=8F=B7?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E6=B7=BB=E5=8A=A0=E5=85=B1=E4=BA=AB=E5=BC=8F?= =?UTF-8?q?=E5=A4=9A=E7=A7=9F=E6=88=B7=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: binarywang <1343140+binarywang@users.noreply.github.com> --- .../services/AbstractWxMpConfiguration.java | 110 ++++++++++++++---- .../mp/properties/WxMpMultiProperties.java | 24 ++++ .../service/WxMpMultiServicesSharedImpl.java | 50 ++++++++ 3 files changed, 160 insertions(+), 24 deletions(-) create mode 100644 spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/service/WxMpMultiServicesSharedImpl.java diff --git a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/configuration/services/AbstractWxMpConfiguration.java b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/configuration/services/AbstractWxMpConfiguration.java index 1f431b645d..181413db6b 100644 --- a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/configuration/services/AbstractWxMpConfiguration.java +++ b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/configuration/services/AbstractWxMpConfiguration.java @@ -4,6 +4,7 @@ import com.binarywang.spring.starter.wxjava.mp.properties.WxMpSingleProperties; import com.binarywang.spring.starter.wxjava.mp.service.WxMpMultiServices; import com.binarywang.spring.starter.wxjava.mp.service.WxMpMultiServicesImpl; +import com.binarywang.spring.starter.wxjava.mp.service.WxMpMultiServicesSharedImpl; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import me.chanjar.weixin.mp.api.WxMpService; @@ -13,9 +14,7 @@ import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl; import org.apache.commons.lang3.StringUtils; -import java.util.Collection; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.stream.Collectors; /** @@ -34,6 +33,7 @@ protected WxMpMultiServices wxMpMultiServices(WxMpMultiProperties wxMpMultiPrope log.warn("微信公众号应用参数未配置,通过 WxMpMultiServices#getWxMpService(\"tenantId\")获取实例将返回空"); return new WxMpMultiServicesImpl(); } + /** * 校验 appId 是否唯一,避免使用 redis 缓存 token、ticket 时错乱。 * @@ -50,9 +50,26 @@ protected WxMpMultiServices wxMpMultiServices(WxMpMultiProperties wxMpMultiPrope throw new RuntimeException("请确保微信公众号配置 appId 的唯一性"); } } - WxMpMultiServicesImpl services = new WxMpMultiServicesImpl(); + // 根据配置选择多租户模式 + WxMpMultiProperties.MultiTenantMode mode = wxMpMultiProperties.getConfigStorage().getMultiTenantMode(); + if (mode == WxMpMultiProperties.MultiTenantMode.SHARED) { + return createSharedMultiServices(appsMap, wxMpMultiProperties); + } else { + return createIsolatedMultiServices(appsMap, wxMpMultiProperties); + } + } + + /** + * 创建隔离模式的多租户服务(每个租户独立 WxMpService 实例) + */ + private WxMpMultiServices createIsolatedMultiServices( + Map appsMap, + WxMpMultiProperties wxMpMultiProperties) { + + WxMpMultiServicesImpl services = new WxMpMultiServicesImpl(); Set> entries = appsMap.entrySet(); + for (Map.Entry entry : entries) { String tenantId = entry.getKey(); WxMpSingleProperties wxMpSingleProperties = entry.getValue(); @@ -63,40 +80,70 @@ protected WxMpMultiServices wxMpMultiServices(WxMpMultiProperties wxMpMultiPrope WxMpService wxMpService = this.wxMpService(storage, wxMpMultiProperties); services.addWxMpService(tenantId, wxMpService); } + + log.info("微信公众号多租户服务初始化完成,使用隔离模式(ISOLATED),共配置 {} 个租户", appsMap.size()); return services; } /** - * 配置 WxMpDefaultConfigImpl - * - * @param wxMpMultiProperties 参数 - * @return WxMpDefaultConfigImpl + * 创建共享模式的多租户服务(单个 WxMpService 实例管理多个配置) */ - protected abstract WxMpDefaultConfigImpl wxMpConfigStorage(WxMpMultiProperties wxMpMultiProperties); - - public WxMpService wxMpService(WxMpConfigStorage configStorage, WxMpMultiProperties wxMpMultiProperties) { + private WxMpMultiServices createSharedMultiServices( + Map appsMap, + WxMpMultiProperties wxMpMultiProperties) { + + // 创建共享的 WxMpService 实例 WxMpMultiProperties.ConfigStorage storage = wxMpMultiProperties.getConfigStorage(); - WxMpMultiProperties.HttpClientType httpClientType = storage.getHttpClientType(); - WxMpService wxMpService; + WxMpService sharedService = createWxMpServiceByType(storage.getHttpClientType()); + configureWxMpService(sharedService, storage); + + // 准备所有租户的配置 + Map configsMap = new HashMap<>(); + String defaultTenantId = null; + + for (Map.Entry entry : appsMap.entrySet()) { + String tenantId = entry.getKey(); + if (defaultTenantId == null) { + defaultTenantId = tenantId; + } + + WxMpSingleProperties wxMpSingleProperties = entry.getValue(); + WxMpDefaultConfigImpl config = this.wxMpConfigStorage(wxMpMultiProperties); + this.configApp(config, wxMpSingleProperties); + this.configHttp(config, storage); + this.configHost(config, wxMpMultiProperties.getHosts()); + configsMap.put(tenantId, config); + } + + // 设置多配置到共享的 WxMpService + sharedService.setMultiConfigStorages(configsMap, defaultTenantId); + + log.info("微信公众号多租户服务初始化完成,使用共享模式(SHARED),共配置 {} 个租户,共享一个 HTTP 客户端", appsMap.size()); + return new WxMpMultiServicesSharedImpl(sharedService); + } + + /** + * 根据类型创建 WxMpService 实例 + */ + private WxMpService createWxMpServiceByType(WxMpMultiProperties.HttpClientType httpClientType) { switch (httpClientType) { case OK_HTTP: - wxMpService = new WxMpServiceOkHttpImpl(); - break; + return new WxMpServiceOkHttpImpl(); case JODD_HTTP: - wxMpService = new WxMpServiceJoddHttpImpl(); - break; + return new WxMpServiceJoddHttpImpl(); case HTTP_CLIENT: - wxMpService = new WxMpServiceHttpClientImpl(); - break; + return new WxMpServiceHttpClientImpl(); case HTTP_COMPONENTS: - wxMpService = new WxMpServiceHttpComponentsImpl(); - break; + return new WxMpServiceHttpComponentsImpl(); default: - wxMpService = new WxMpServiceImpl(); - break; + return new WxMpServiceImpl(); } + } - wxMpService.setWxMpConfigStorage(configStorage); + /** + * 配置 WxMpService 的通用参数 + */ + private void configureWxMpService(WxMpService wxMpService, WxMpMultiProperties.ConfigStorage storage) { int maxRetryTimes = storage.getMaxRetryTimes(); if (maxRetryTimes < 0) { maxRetryTimes = 0; @@ -107,6 +154,21 @@ public WxMpService wxMpService(WxMpConfigStorage configStorage, WxMpMultiPropert } wxMpService.setRetrySleepMillis(retrySleepMillis); wxMpService.setMaxRetryTimes(maxRetryTimes); + } + + /** + * 配置 WxMpDefaultConfigImpl + * + * @param wxMpMultiProperties 参数 + * @return WxMpDefaultConfigImpl + */ + protected abstract WxMpDefaultConfigImpl wxMpConfigStorage(WxMpMultiProperties wxMpMultiProperties); + + public WxMpService wxMpService(WxMpConfigStorage configStorage, WxMpMultiProperties wxMpMultiProperties) { + WxMpMultiProperties.ConfigStorage storage = wxMpMultiProperties.getConfigStorage(); + WxMpService wxMpService = createWxMpServiceByType(storage.getHttpClientType()); + wxMpService.setWxMpConfigStorage(configStorage); + configureWxMpService(wxMpService, storage); return wxMpService; } diff --git a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/WxMpMultiProperties.java b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/WxMpMultiProperties.java index 8b2fa58aa3..9dd95f9531 100644 --- a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/WxMpMultiProperties.java +++ b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/WxMpMultiProperties.java @@ -116,6 +116,15 @@ public static class ConfigStorage implements Serializable { * */ private int retrySleepMillis = 1000; + + /** + * 多租户实现模式. + *
    + *
  • ISOLATED: 为每个租户创建独立的 WxMpService 实例(默认)
  • + *
  • SHARED: 使用单个 WxMpService 实例管理所有租户配置,共享 HTTP 客户端
  • + *
+ */ + private MultiTenantMode multiTenantMode = MultiTenantMode.ISOLATED; } public enum StorageType { @@ -155,4 +164,19 @@ public enum HttpClientType { */ JODD_HTTP } + + public enum MultiTenantMode { + /** + * 隔离模式:为每个租户创建独立的 WxMpService 实例. + * 优点:线程安全,不依赖 ThreadLocal + * 缺点:每个租户创建独立的 HTTP 客户端,资源占用较多 + */ + ISOLATED, + /** + * 共享模式:使用单个 WxMpService 实例管理所有租户配置. + * 优点:共享 HTTP 客户端,节省资源 + * 缺点:依赖 ThreadLocal 切换配置,异步场景需注意 + */ + SHARED + } } diff --git a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/service/WxMpMultiServicesSharedImpl.java b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/service/WxMpMultiServicesSharedImpl.java new file mode 100644 index 0000000000..96fa65a762 --- /dev/null +++ b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/service/WxMpMultiServicesSharedImpl.java @@ -0,0 +1,50 @@ +package com.binarywang.spring.starter.wxjava.mp.service; + +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.mp.api.WxMpService; + +/** + * 微信公众号 {@link WxMpMultiServices} 共享式实现. + *

+ * 使用单个 WxMpService 实例管理多个租户配置,通过 switchoverTo 切换租户。 + * 相比 {@link WxMpMultiServicesImpl},此实现共享 HTTP 客户端,节省资源。 + *

+ *

+ * 注意:由于使用 ThreadLocal 切换配置,在异步或多线程场景需要特别注意线程上下文切换。 + *

+ * + * @author Binary Wang + * created on 2026/1/9 + */ +@RequiredArgsConstructor +public class WxMpMultiServicesSharedImpl implements WxMpMultiServices { + private final WxMpService sharedWxMpService; + + @Override + public WxMpService getWxMpService(String tenantId) { + if (tenantId == null) { + return null; + } + // 切换到指定租户的配置 + return sharedWxMpService.switchoverTo(tenantId); + } + + @Override + public void removeWxMpService(String tenantId) { + if (tenantId != null) { + sharedWxMpService.removeConfigStorage(tenantId); + } + } + + /** + * 添加租户配置到共享的 WxMpService 实例 + * + * @param tenantId 租户 ID + * @param wxMpService 要添加配置的 WxMpService(仅使用其配置,不使用其实例) + */ + public void addWxMpService(String tenantId, WxMpService wxMpService) { + if (tenantId != null && wxMpService != null) { + sharedWxMpService.addConfigStorage(tenantId, wxMpService.getWxMpConfigStorage()); + } + } +} From 664a5e47726123f435a4209a0ff6308da46f6cd2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 9 Jan 2026 06:33:21 +0000 Subject: [PATCH 5/9] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=A4=9A=E7=A7=9F?= =?UTF-8?q?=E6=88=B7=E6=A8=A1=E5=BC=8F=E6=94=B9=E8=BF=9B=E6=80=BB=E4=BD=93?= =?UTF-8?q?=E8=AF=B4=E6=98=8E=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: binarywang <1343140+binarywang@users.noreply.github.com> --- .../MULTI_TENANT_MODE_IMPROVEMENT.md | 160 ++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 spring-boot-starters/MULTI_TENANT_MODE_IMPROVEMENT.md diff --git a/spring-boot-starters/MULTI_TENANT_MODE_IMPROVEMENT.md b/spring-boot-starters/MULTI_TENANT_MODE_IMPROVEMENT.md new file mode 100644 index 0000000000..85c9c199e1 --- /dev/null +++ b/spring-boot-starters/MULTI_TENANT_MODE_IMPROVEMENT.md @@ -0,0 +1,160 @@ +# 多租户模式配置改进说明 + +## 问题背景 + +用户在 issue #3909 中提出了一个架构设计问题: + +> 基础 Wx 实现类中已经有 configMap 了,可以用 configMap 来存储不同的小程序配置。不同的配置,都是复用同一个 http 客户端。为什么在各个 spring-boot-starter 中又单独创建类来存储不同的配置?从 spring 的配置来看,http 客户端只有一个,不同小程序配置可以实现多租户,所以似乎没必要单独再建新类存放?重复创建,增加了 http 客户端的成本?直接使用 Wx 实现类中已经有 configMap 不是更好吗? + +## 解决方案 + +从 4.8.0 版本开始,我们为多租户 Spring Boot Starter 提供了**两种实现模式**供用户选择: + +### 1. 隔离模式(ISOLATED,默认) + +**实现方式**:为每个租户创建独立的 WxService 实例,每个实例拥有独立的 HTTP 客户端。 + +**优点**: +- ✅ 线程安全,无需担心并发问题 +- ✅ 不依赖 ThreadLocal,适合异步/响应式编程 +- ✅ 租户间完全隔离,互不影响 + +**缺点**: +- ❌ 每个租户创建独立的 HTTP 客户端,资源占用较多 +- ❌ 适合租户数量不多的场景(建议 < 50 个租户) + +**代码实现**:`WxMaMultiServicesImpl`, `WxMpMultiServicesImpl` 等 + +### 2. 共享模式(SHARED,新增) + +**实现方式**:使用单个 WxService 实例管理所有租户配置,通过 ThreadLocal 切换租户,所有租户共享同一个 HTTP 客户端。 + +**优点**: +- ✅ 共享 HTTP 客户端,大幅节省资源 +- ✅ 适合租户数量较多的场景(支持 100+ 租户) +- ✅ 内存占用更小 + +**缺点**: +- ❌ 依赖 ThreadLocal 切换配置,在异步场景需要特别注意 +- ❌ 需要注意线程上下文传递 + +**代码实现**:`WxMaMultiServicesSharedImpl`, `WxMpMultiServicesSharedImpl` 等 + +## 使用方式 + +### 配置示例 + +```yaml +wx: + ma: # 或 mp, cp, channel + apps: + tenant1: + app-id: wxd898fcb01713c555 + app-secret: 47a2422a5d04a27e2b3ed1f1f0b0dbad + tenant2: + app-id: wx1234567890abcdef + app-secret: 1234567890abcdef1234567890abcdef + + config-storage: + type: memory + http-client-type: http_client + # 多租户模式配置(新增) + multi-tenant-mode: shared # isolated(默认)或 shared +``` + +### 代码使用(两种模式代码完全相同) + +```java +@RestController +public class WxController { + @Autowired + private WxMaMultiServices wxMaMultiServices; // 或 WxMpMultiServices + + @GetMapping("/api/{tenantId}") + public String handle(@PathVariable String tenantId) { + WxMaService wxService = wxMaMultiServices.getWxMaService(tenantId); + // 使用 wxService 调用微信 API + return wxService.getAccessToken(); + } +} +``` + +## 性能对比 + +以 100 个租户为例: + +| 指标 | 隔离模式 | 共享模式 | +|------|---------|---------| +| HTTP 客户端数量 | 100 个 | 1 个 | +| 内存占用(估算) | ~500MB | ~50MB | +| 线程安全 | ✅ 完全安全 | ⚠️ 需注意异步场景 | +| 性能 | 略高(无 ThreadLocal 切换) | 略低(有 ThreadLocal 切换) | +| 适用场景 | 中小规模 | 大规模 | + +## 支持的模块 + +目前已实现共享模式支持的模块: + +- ✅ **小程序(MiniApp)**:`wx-java-miniapp-multi-spring-boot-starter` +- ✅ **公众号(MP)**:`wx-java-mp-multi-spring-boot-starter` + +后续版本将支持: +- ⏳ 企业微信(CP) +- ⏳ 视频号(Channel) +- ⏳ 企业微信第三方应用(CP-TP) + +## 迁移指南 + +### 从旧版本升级 + +升级到 4.8.0+ 后: + +1. **默认行为不变**:如果不配置 `multi-tenant-mode`,将继续使用隔离模式(与旧版本行为一致) +2. **向后兼容**:所有现有代码无需修改 +3. **可选升级**:如需节省资源,可配置 `multi-tenant-mode: shared` 启用共享模式 + +### 选择建议 + +**使用隔离模式(ISOLATED)的场景**: +- 租户数量较少(< 50 个) +- 使用异步编程、响应式编程 +- 对线程安全有严格要求 +- 对资源占用不敏感 + +**使用共享模式(SHARED)的场景**: +- 租户数量较多(> 50 个) +- 同步编程场景 +- 对资源占用敏感 +- 可以接受 ThreadLocal 的约束 + +## 注意事项 + +### 共享模式下的异步编程 + +如果使用共享模式,在异步编程时需要注意 ThreadLocal 的传递: + +```java +// ❌ 错误:异步线程无法获取到正确的配置 +CompletableFuture.runAsync(() -> { + wxService.getUserService().getUserInfo(...); // 可能使用错误的租户配置 +}); + +// ✅ 正确:在主线程获取必要信息,传递给异步线程 +String appId = wxService.getWxMaConfig().getAppid(); +CompletableFuture.runAsync(() -> { + log.info("AppId: {}", appId); // 使用已获取的配置信息 +}); +``` + +## 详细文档 + +- 小程序模块详细说明:[spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/MULTI_TENANT_MODE.md](spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/MULTI_TENANT_MODE.md) + +## 相关链接 + +- Issue: [#3909](https://github.com/binarywang/WxJava/issues/3909) +- Pull Request: [待填写] + +## 致谢 + +感谢 issue 提出者对项目架构的深入思考和建议,这帮助我们提供了更灵活、更高效的多租户解决方案。 From c1f950c8a21206083484272bbefc2c1d9385ad68 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 9 Jan 2026 06:35:40 +0000 Subject: [PATCH 6/9] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E5=AE=A1=E6=9F=A5=E9=97=AE=E9=A2=98=EF=BC=9A=E9=81=BF=E5=85=8D?= =?UTF-8?q?=E9=80=9A=E9=85=8D=E7=AC=A6=E5=AF=BC=E5=85=A5=EF=BC=8C=E4=BD=BF?= =?UTF-8?q?=E7=94=A8TreeMap=E4=BF=9D=E8=AF=81=E7=A7=9F=E6=88=B7=E9=A1=BA?= =?UTF-8?q?=E5=BA=8F=E4=B8=80=E8=87=B4=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: binarywang <1343140+binarywang@users.noreply.github.com> --- .../services/AbstractWxMaConfiguration.java | 14 ++++++------- .../services/AbstractWxMpConfiguration.java | 20 +++++++++++-------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/configuration/services/AbstractWxMaConfiguration.java b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/configuration/services/AbstractWxMaConfiguration.java index 9c9642beac..fba9d875ee 100644 --- a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/configuration/services/AbstractWxMaConfiguration.java +++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/configuration/services/AbstractWxMaConfiguration.java @@ -16,7 +16,11 @@ import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl; import org.apache.commons.lang3.StringUtils; -import java.util.*; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; import java.util.stream.Collectors; /** @@ -98,16 +102,12 @@ private WxMaMultiServices createSharedMultiServices( WxMaService sharedService = createWxMaServiceByType(storage.getHttpClientType()); configureWxMaService(sharedService, storage); - // 准备所有租户的配置 + // 准备所有租户的配置,使用 TreeMap 保证顺序一致性 Map configsMap = new HashMap<>(); - String defaultTenantId = null; + String defaultTenantId = new TreeMap<>(appsMap).firstKey(); for (Map.Entry entry : appsMap.entrySet()) { String tenantId = entry.getKey(); - if (defaultTenantId == null) { - defaultTenantId = tenantId; - } - WxMaSingleProperties wxMaSingleProperties = entry.getValue(); WxMaDefaultConfigImpl config = this.wxMaConfigStorage(wxMaMultiProperties); this.configApp(config, wxMaSingleProperties); diff --git a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/configuration/services/AbstractWxMpConfiguration.java b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/configuration/services/AbstractWxMpConfiguration.java index 181413db6b..46724c625f 100644 --- a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/configuration/services/AbstractWxMpConfiguration.java +++ b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/configuration/services/AbstractWxMpConfiguration.java @@ -8,13 +8,21 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import me.chanjar.weixin.mp.api.WxMpService; -import me.chanjar.weixin.mp.api.impl.*; +import me.chanjar.weixin.mp.api.impl.WxMpServiceHttpClientImpl; +import me.chanjar.weixin.mp.api.impl.WxMpServiceHttpComponentsImpl; +import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl; +import me.chanjar.weixin.mp.api.impl.WxMpServiceJoddHttpImpl; +import me.chanjar.weixin.mp.api.impl.WxMpServiceOkHttpImpl; import me.chanjar.weixin.mp.config.WxMpConfigStorage; import me.chanjar.weixin.mp.config.WxMpHostConfig; import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl; import org.apache.commons.lang3.StringUtils; -import java.util.*; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; import java.util.stream.Collectors; /** @@ -97,16 +105,12 @@ private WxMpMultiServices createSharedMultiServices( WxMpService sharedService = createWxMpServiceByType(storage.getHttpClientType()); configureWxMpService(sharedService, storage); - // 准备所有租户的配置 + // 准备所有租户的配置,使用 TreeMap 保证顺序一致性 Map configsMap = new HashMap<>(); - String defaultTenantId = null; + String defaultTenantId = new TreeMap<>(appsMap).firstKey(); for (Map.Entry entry : appsMap.entrySet()) { String tenantId = entry.getKey(); - if (defaultTenantId == null) { - defaultTenantId = tenantId; - } - WxMpSingleProperties wxMpSingleProperties = entry.getValue(); WxMpDefaultConfigImpl config = this.wxMpConfigStorage(wxMpMultiProperties); this.configApp(config, wxMpSingleProperties); From a0a18b9522766c158f11066e81e6dc82a6536d46 Mon Sep 17 00:00:00 2001 From: Binary Wang Date: Sat, 10 Jan 2026 14:34:09 +0800 Subject: [PATCH 7/9] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../MULTI_TENANT_MODE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/MULTI_TENANT_MODE.md b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/MULTI_TENANT_MODE.md index e6e459d707..6dd1d110c3 100644 --- a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/MULTI_TENANT_MODE.md +++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/MULTI_TENANT_MODE.md @@ -175,7 +175,7 @@ public class MiniAppService { ## 源码分析 -issue讨论地址:[#3909](https://github.com/binarywang/WxJava/issues/3909) +issue讨论地址:[#3835](https://github.com/binarywang/WxJava/issues/3835) ### 为什么有两种设计? From 19ee495fb1493058daa2cca6dcda4103291820a8 Mon Sep 17 00:00:00 2001 From: Binary Wang Date: Sat, 10 Jan 2026 21:53:05 +0800 Subject: [PATCH 8/9] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=89=80=E6=9C=89=20Revi?= =?UTF-8?q?ew=20Comments=20=E5=B7=B2=E4=BF=AE=E5=A4=8D=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spring-boot-starters/MULTI_TENANT_MODE_IMPROVEMENT.md | 6 +++--- .../miniapp/service/WxMaMultiServicesSharedImpl.java | 7 +++++-- .../wxjava/mp/service/WxMpMultiServicesSharedImpl.java | 7 +++++-- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/spring-boot-starters/MULTI_TENANT_MODE_IMPROVEMENT.md b/spring-boot-starters/MULTI_TENANT_MODE_IMPROVEMENT.md index 85c9c199e1..6581f6207d 100644 --- a/spring-boot-starters/MULTI_TENANT_MODE_IMPROVEMENT.md +++ b/spring-boot-starters/MULTI_TENANT_MODE_IMPROVEMENT.md @@ -2,7 +2,7 @@ ## 问题背景 -用户在 issue #3909 中提出了一个架构设计问题: +用户在 issue #3835 中提出了一个架构设计问题: > 基础 Wx 实现类中已经有 configMap 了,可以用 configMap 来存储不同的小程序配置。不同的配置,都是复用同一个 http 客户端。为什么在各个 spring-boot-starter 中又单独创建类来存储不同的配置?从 spring 的配置来看,http 客户端只有一个,不同小程序配置可以实现多租户,所以似乎没必要单独再建新类存放?重复创建,增加了 http 客户端的成本?直接使用 Wx 实现类中已经有 configMap 不是更好吗? @@ -152,8 +152,8 @@ CompletableFuture.runAsync(() -> { ## 相关链接 -- Issue: [#3909](https://github.com/binarywang/WxJava/issues/3909) -- Pull Request: [待填写] +- Issue: [#3835](https://github.com/binarywang/WxJava/issues/3835) +- Pull Request: [#3840](https://github.com/binarywang/WxJava/pull/3840) ## 致谢 diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/service/WxMaMultiServicesSharedImpl.java b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/service/WxMaMultiServicesSharedImpl.java index d82836488c..8f462bf2c5 100644 --- a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/service/WxMaMultiServicesSharedImpl.java +++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/service/WxMaMultiServicesSharedImpl.java @@ -25,8 +25,11 @@ public WxMaService getWxMaService(String tenantId) { if (tenantId == null) { return null; } - // 切换到指定租户的配置 - return sharedWxMaService.switchoverTo(tenantId); + // 使用 switchover 检查配置是否存在,保持与隔离模式 API 行为一致(不存在时返回 null) + if (!sharedWxMaService.switchover(tenantId)) { + return null; + } + return sharedWxMaService; } @Override diff --git a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/service/WxMpMultiServicesSharedImpl.java b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/service/WxMpMultiServicesSharedImpl.java index 96fa65a762..10faeaa075 100644 --- a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/service/WxMpMultiServicesSharedImpl.java +++ b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/service/WxMpMultiServicesSharedImpl.java @@ -25,8 +25,11 @@ public WxMpService getWxMpService(String tenantId) { if (tenantId == null) { return null; } - // 切换到指定租户的配置 - return sharedWxMpService.switchoverTo(tenantId); + // 使用 switchover 检查配置是否存在,保持与隔离模式 API 行为一致(不存在时返回 null) + if (!sharedWxMpService.switchover(tenantId)) { + return null; + } + return sharedWxMpService; } @Override From 29e6ce947bebd3949924739543f94a875c45141e Mon Sep 17 00:00:00 2001 From: Binary Wang Date: Mon, 12 Jan 2026 09:53:43 +0800 Subject: [PATCH 9/9] fix javadoc --- .../wxjava/miniapp/service/WxMaMultiServicesSharedImpl.java | 2 +- .../starter/wxjava/mp/service/WxMpMultiServicesSharedImpl.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/service/WxMaMultiServicesSharedImpl.java b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/service/WxMaMultiServicesSharedImpl.java index 8f462bf2c5..40a01fb52e 100644 --- a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/service/WxMaMultiServicesSharedImpl.java +++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/service/WxMaMultiServicesSharedImpl.java @@ -6,7 +6,7 @@ /** * 微信小程序 {@link WxMaMultiServices} 共享式实现. *

- * 使用单个 WxMaService 实例管理多个租户配置,通过 switchoverTo 切换租户。 + * 使用单个 WxMaService 实例管理多个租户配置,通过 switchover 切换租户。 * 相比 {@link WxMaMultiServicesImpl},此实现共享 HTTP 客户端,节省资源。 *

*

diff --git a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/service/WxMpMultiServicesSharedImpl.java b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/service/WxMpMultiServicesSharedImpl.java index 10faeaa075..ca9123c572 100644 --- a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/service/WxMpMultiServicesSharedImpl.java +++ b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/service/WxMpMultiServicesSharedImpl.java @@ -6,7 +6,7 @@ /** * 微信公众号 {@link WxMpMultiServices} 共享式实现. *

- * 使用单个 WxMpService 实例管理多个租户配置,通过 switchoverTo 切换租户。 + * 使用单个 WxMpService 实例管理多个租户配置,通过 switchover 切换租户。 * 相比 {@link WxMpMultiServicesImpl},此实现共享 HTTP 客户端,节省资源。 *

*