🎨 #3840 小程序和公众号的多租户starter添加多租户共享模式以优化资源使用

This commit is contained in:
Copilot
2026-01-19 11:28:05 +08:00
committed by GitHub
parent 12db287ae0
commit 373d9fa5f1
8 changed files with 709 additions and 62 deletions

View File

@@ -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讨论地址[#3835](https://github.com/binarywang/WxJava/issues/3835)
### 为什么有两种设计?
1. **基础实现类的 `configMap`**
- 位置:`BaseWxMaServiceImpl`
- 特点:单个 Service 实例 + 多个配置 + ThreadLocal 切换
- 设计目的:支持在一个应用中管理多个小程序账号
2. **Spring Boot Starter 的 `services` Map**
- 位置:`WxMaMultiServicesImpl`
- 特点:多个 Service 实例 + 每个实例一个配置
- 设计目的:为 Spring Boot 提供更符合依赖注入风格的多租户支持
### 新版本改进
新版本通过配置项让用户自主选择实现方式:
```
用户 → WxMaMultiServices 接口
┌────┴────┐
↓ ↓
隔离模式 共享模式
(多Service) (单Service+configMap)
```
这样既保留了线程安全的优势(隔离模式),又提供了资源节省的选项(共享模式)。

View File

@@ -4,6 +4,7 @@ import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaMultiProperti
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;
@@ -16,8 +17,10 @@ import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl;
import org.apache.commons.lang3.StringUtils;
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;
/**
@@ -33,9 +36,10 @@ public abstract class AbstractWxMaConfiguration {
protected WxMaMultiServices wxMaMultiServices(WxMaMultiProperties wxMaMultiProperties) {
Map<String, WxMaSingleProperties> 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 +53,29 @@ public abstract class AbstractWxMaConfiguration {
.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<String, WxMaSingleProperties> appsMap,
WxMaMultiProperties wxMaMultiProperties) {
WxMaMultiServicesImpl services = new WxMaMultiServicesImpl();
Set<Map.Entry<String, WxMaSingleProperties>> entries = appsMap.entrySet();
for (Map.Entry<String, WxMaSingleProperties> entry : entries) {
String tenantId = entry.getKey();
WxMaSingleProperties wxMaSingleProperties = entry.getValue();
@@ -64,9 +85,75 @@ public abstract class AbstractWxMaConfiguration {
WxMaService wxMaService = this.wxMaService(storage, wxMaMultiProperties);
services.addWxMaService(tenantId, wxMaService);
}
log.info("微信小程序多租户服务初始化完成使用隔离模式ISOLATED共配置 {} 个租户", appsMap.size());
return services;
}
/**
* 创建共享模式的多租户服务(单个 WxMaService 实例管理多个配置)
*/
private WxMaMultiServices createSharedMultiServices(
Map<String, WxMaSingleProperties> appsMap,
WxMaMultiProperties wxMaMultiProperties) {
// 创建共享的 WxMaService 实例
WxMaMultiProperties.ConfigStorage storage = wxMaMultiProperties.getConfigStorage();
WxMaService sharedService = createWxMaServiceByType(storage.getHttpClientType());
configureWxMaService(sharedService, storage);
// 准备所有租户的配置,使用 TreeMap 保证顺序一致性
Map<String, WxMaConfig> configsMap = new HashMap<>();
String defaultTenantId = new TreeMap<>(appsMap).firstKey();
for (Map.Entry<String, WxMaSingleProperties> entry : appsMap.entrySet()) {
String tenantId = entry.getKey();
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:
return new WxMaServiceOkHttpImpl();
case JODD_HTTP:
return new WxMaServiceJoddHttpImpl();
case HTTP_CLIENT:
return new WxMaServiceHttpClientImpl();
default:
return new WxMaServiceImpl();
}
}
/**
* 配置 WxMaService 的通用参数
*/
private void configureWxMaService(WxMaService wxMaService, WxMaMultiProperties.ConfigStorage storage) {
int maxRetryTimes = storage.getMaxRetryTimes();
if (maxRetryTimes < 0) {
maxRetryTimes = 0;
}
int retrySleepMillis = storage.getRetrySleepMillis();
if (retrySleepMillis < 0) {
retrySleepMillis = 1000;
}
wxMaService.setRetrySleepMillis(retrySleepMillis);
wxMaService.setMaxRetryTimes(maxRetryTimes);
}
/**
* 配置 WxMaDefaultConfigImpl
*
@@ -77,34 +164,9 @@ public abstract class AbstractWxMaConfiguration {
public WxMaService wxMaService(WxMaConfig wxMaConfig, WxMaMultiProperties wxMaMultiProperties) {
WxMaMultiProperties.ConfigStorage storage = wxMaMultiProperties.getConfigStorage();
WxMaMultiProperties.HttpClientType httpClientType = storage.getHttpClientType();
WxMaService wxMaService;
switch (httpClientType) {
case OK_HTTP:
wxMaService = new WxMaServiceOkHttpImpl();
break;
case JODD_HTTP:
wxMaService = new WxMaServiceJoddHttpImpl();
break;
case HTTP_CLIENT:
wxMaService = new WxMaServiceHttpClientImpl();
break;
default:
wxMaService = new WxMaServiceImpl();
break;
}
WxMaService wxMaService = createWxMaServiceByType(storage.getHttpClientType());
wxMaService.setWxMaConfig(wxMaConfig);
int maxRetryTimes = storage.getMaxRetryTimes();
if (maxRetryTimes < 0) {
maxRetryTimes = 0;
}
int retrySleepMillis = storage.getRetrySleepMillis();
if (retrySleepMillis < 0) {
retrySleepMillis = 1000;
}
wxMaService.setRetrySleepMillis(retrySleepMillis);
wxMaService.setMaxRetryTimes(maxRetryTimes);
configureWxMaService(wxMaService, storage);
return wxMaService;
}

View File

@@ -116,6 +116,15 @@ public class WxMaMultiProperties implements Serializable {
* </pre>
*/
private int retrySleepMillis = 1000;
/**
* 多租户实现模式.
* <ul>
* <li>ISOLATED: 为每个租户创建独立的 WxMaService 实例(默认)</li>
* <li>SHARED: 使用单个 WxMaService 实例管理所有租户配置,共享 HTTP 客户端</li>
* </ul>
*/
private MultiTenantMode multiTenantMode = MultiTenantMode.ISOLATED;
}
public enum StorageType {
@@ -151,4 +160,19 @@ public class WxMaMultiProperties implements Serializable {
*/
JODD_HTTP
}
public enum MultiTenantMode {
/**
* 隔离模式:为每个租户创建独立的 WxMaService 实例.
* 优点:线程安全,不依赖 ThreadLocal
* 缺点:每个租户创建独立的 HTTP 客户端,资源占用较多
*/
ISOLATED,
/**
* 共享模式:使用单个 WxMaService 实例管理所有租户配置.
* 优点:共享 HTTP 客户端,节省资源
* 缺点:依赖 ThreadLocal 切换配置,异步场景需注意
*/
SHARED
}
}

View File

@@ -0,0 +1,53 @@
package com.binarywang.spring.starter.wxjava.miniapp.service;
import cn.binarywang.wx.miniapp.api.WxMaService;
import lombok.RequiredArgsConstructor;
/**
* 微信小程序 {@link WxMaMultiServices} 共享式实现.
* <p>
* 使用单个 WxMaService 实例管理多个租户配置,通过 switchover 切换租户。
* 相比 {@link WxMaMultiServicesImpl},此实现共享 HTTP 客户端,节省资源。
* </p>
* <p>
* 注意:由于使用 ThreadLocal 切换配置,在异步或多线程场景需要特别注意线程上下文切换。
* </p>
*
* @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;
}
// 使用 switchover 检查配置是否存在,保持与隔离模式 API 行为一致(不存在时返回 null
if (!sharedWxMaService.switchover(tenantId)) {
return null;
}
return sharedWxMaService;
}
@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());
}
}
}