PAYATHON 2026

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() 等链式方法会修改内部状态,不要让多个线程共享同一个客户端实例。更稳妥的做法是缓存配置,并按请求创建客户端。
  • 订单号最好能够映射到商户标识,否则在退款、查询、关单和通知验签时,可能无法确定应使用哪套配置。

备注:内容仅供参考。