alipay-easysdk 如何动态切换不同收单商户的支付配置?
结论
不要在每次请求中通过 Factory.setOptions() 切换配置。Factory 保存的是全局配置,并发请求可能互相覆盖,因此无法保证某次支付调用使用的是当前商户的配置。
多商户场景下,应为每套支付配置创建独立的 Config,并直接使用对应业务模块的 Client,不再经过全局 Factory。如果当前使用的 alipay-easysdk 版本没有公开 Client(Config) 构造方式,可以升级到支持该方式的版本,或者在业务层对整段“设置配置并调用接口”的过程加锁。只在调用前切换 Factory 配置,无法解决并发问题。
为什么会发生配置混用
下面的写法存在竞态条件:
Factory.setOptions(merchantAConfig);
// 此处可能切换到另一个线程
Factory.Payment.FaceToFace().preCreate(
subject,
outTradeNo,
totalAmount
);
如果线程切换后,另一个请求执行了:
Factory.setOptions(merchantBConfig);
第一个请求后续创建支付客户端时,就可能读到商户 B 的配置。只有在配置的整个读取链路都可控时,ThreadLocal 才能起作用。它无法改变 Factory 保存全局状态这一事实,因此不建议用它包装 Factory.setOptions()。
推荐实现:按商户创建客户端
先在业务层保存各商户对应的 Config:
import com.alipay.easysdk.kernel.Config;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class AlipayConfigRegistry {
private final Map<String, Config> configs = new ConcurrentHashMap<>();
public void register(String merchantId, Config config) {
configs.put(merchantId, config);
}
public Config get(String merchantId) {
Config config = configs.get(merchantId);
if (config == null) {
throw new IllegalArgumentException(
"未找到支付宝配置,merchantId=" + merchantId
);
}
return config;
}
}
分别初始化不同商户的配置:
Config merchantA = new Config();
merchantA.protocol = "https";
merchantA.gatewayHost = "openapi.alipay.com";
merchantA.signType = "RSA2";
merchantA.appId = "商户A的应用ID";
merchantA.merchantPrivateKey = "商户A的应用私钥";
merchantA.alipayPublicKey = "商户A对应的支付宝公钥";
merchantA.notifyUrl = "https://example.com/alipay/notify/a";
Config merchantB = new Config();
merchantB.protocol = "https";
merchantB.gatewayHost = "openapi.alipay.com";
merchantB.signType = "RSA2";
merchantB.appId = "商户B的应用ID";
merchantB.merchantPrivateKey = "商户B的应用私钥";
merchantB.alipayPublicKey = "商户B对应的支付宝公钥";
merchantB.notifyUrl = "https://example.com/alipay/notify/b";
AlipayConfigRegistry registry = new AlipayConfigRegistry();
registry.register("merchant-a", merchantA);
registry.register("merchant-b", merchantB);
调用支付接口时,先根据商户标识取得配置,再创建本次调用所需的客户端:
import com.alipay.easysdk.kernel.Config;
import com.alipay.easysdk.payment.facetoface.models.AlipayTradePrecreateResponse;
public class AlipayPaymentService {
private final AlipayConfigRegistry registry;
public AlipayPaymentService(AlipayConfigRegistry registry) {
this.registry = registry;
}
public AlipayTradePrecreateResponse preCreate(
String merchantId,
String subject,
String outTradeNo,
String totalAmount
) throws Exception {
Config config = registry.get(merchantId);
com.alipay.easysdk.payment.facetoface.Client client =
new com.alipay.easysdk.payment.facetoface.Client(config);
return client.preCreate(subject, outTradeNo, totalAmount);
}
}
每个请求的客户端都持有自己的配置,其他请求即使在处理不同商户,也不会覆盖这份配置。
不同版本的 EasySDK 在包名、构造方法可见性和响应类型上可能有所不同,实际使用时应以项目当前依赖中 Client 类的签名为准。原则是让每次调用明确绑定自己的 Config,不要依赖全局 Factory 动态切换配置。
如果暂时只能使用 Factory
旧版本如果无法直接创建业务 Client,可以把“设置配置”和“完成接口调用”放在同一把锁中:
private final Object alipayFactoryLock = new Object();
public Object preCreate(
Config config,
String subject,
String outTradeNo,
String totalAmount
) throws Exception {
synchronized (alipayFactoryLock) {
Factory.setOptions(config);
return Factory.Payment.FaceToFace().preCreate(
subject,
outTradeNo,
totalAmount
);
}
}
锁必须覆盖完整的调用过程,不能只锁住 Factory.setOptions():
// 错误:释放锁后,其他线程仍可覆盖配置
synchronized (alipayFactoryLock) {
Factory.setOptions(config);
}
return Factory.Payment.FaceToFace().preCreate(
subject,
outTradeNo,
totalAmount
);
使用全局锁后,所有商户的支付宝请求都会串行执行。这种方式只适合兼容旧版本,不适合高并发支付链路。
ISV 代调用场景需要区别处理
如果所谓的“不同收单商户”并非使用不同的 appId、应用私钥和支付宝公钥,而是由同一个服务商应用代多个授权商户调用接口,通常不需要切换整套 Config。
这种情况下,应保持服务商应用配置不变,并为每个授权商户传入对应的 app_auth_token。部分 EasySDK 版本提供类似 agent(appAuthToken) 的调用方式。该方法是否存在、适用于哪些接口,需要以当前依赖版本的 API 为准。
不要混用以下两种场景:
appId、私钥或支付宝公钥不同:分别使用独立的Config和Client。- 同一个服务商应用代不同授权商户调用:使用同一套应用配置,并在请求中传入对应的
app_auth_token。
注意事项
- 使用明确的
merchantId、tenantId等业务标识索引商户配置,不要通过请求参数直接接收任意私钥或网关地址。 Config注册后尽量不要原地修改。更新配置时,应创建新对象并原子替换,以免请求读到尚未更新完成的配置。- 不要在日志中输出应用私钥、AES 密钥、完整授权令牌等敏感信息。
- 处理异步通知时,必须根据可靠信息找到原支付商户,再使用对应的支付宝公钥验签,不能始终使用默认商户配置。
- 如果客户端提供的
optional()、agent()等链式方法会修改内部状态,不要让多个线程共享同一个客户端实例。更稳妥的做法是缓存配置,并按请求创建客户端。 - 订单号最好能够映射到商户标识,否则在退款、查询、关单和通知验签时,可能无法确定应使用哪套配置。
备注:内容仅供参考。
