PAYATHON 2026

支付宝沙箱支付出现 java.lang.NullPointerException

支付阿杰

结论

异常发生在支付宝 EasySDK 发起网络请求之前。调用栈中最早出现的是:

com.alipay.easysdk.kernel.Client.getConfig(Client.java:97)

最可能的原因是 EasySDK 尚未完成全局配置初始化,内部 Config 对象仍为 null。通常有以下几种情况:

  • 调用 Factory.Payment.Page().pay(...) 前没有执行 Factory.setOptions(config)。
  • Spring 没有扫描到配置类,导致初始化方法并未执行。
  • 支付请求早于配置初始化,例如在错误的 Bean 生命周期阶段调用。
  • 手动创建 EasySDK Client 时传入了 null 配置。
  • alipay-easysdk 与 easysdk-kernel 版本不一致,引发依赖冲突。
  • 配置文件没有加载、属性名写错或环境变量缺失,导致有效配置未能创建。

堆栈显示 alipay-easysdk-2.2.0.jar 和 easysdk-kernel-1.0.8.jar 同时存在,需要重点确认它们是否由同一套 EasySDK 依赖关系管理。

正确初始化 EasySDK

首次调用支付接口前,必须先创建 Config 并交给 Factory:

import com.alipay.easysdk.factory.Factory;
import com.alipay.easysdk.kernel.Config;

public class AlipayInitializer {

    public static void init() {
        Config config = new Config();

        config.protocol = "https";
        config.gatewayHost = "openapi.alipaydev.com";
        config.signType = "RSA2";

        config.appId = "你的沙箱应用AppId";
        config.merchantPrivateKey = "你的应用私钥";
        config.alipayPublicKey = "沙箱支付宝公钥";

        config.notifyUrl = "https://example.com/alipay/notify";

        Factory.setOptions(config);
    }
}

沙箱环境应使用以下网关:

openapi.alipaydev.com

不要误配为正式环境网关:

openapi.alipay.com

页面支付可以这样调用:

import com.alipay.easysdk.factory.Factory;
import com.alipay.easysdk.payment.page.models.AlipayTradePagePayResponse;

public AlipayTradePagePayResponse pay() throws Exception {
    return Factory.Payment.Page().pay(
        "测试订单",
        "ORDER_202609090001",
        "0.01",
        "https://example.com/alipay/return"
    );
}

必须先完成初始化,再调用 Factory.Payment.Page()。

Spring Boot 中的推荐写法

可以把支付宝参数放入配置文件,并通过 Spring Bean 生命周期完成初始化。

alipay:
  protocol: https
  gateway-host: openapi.alipaydev.com
  sign-type: RSA2
  app-id: 你的沙箱应用AppId
  merchant-private-key: 你的应用私钥
  alipay-public-key: 你的沙箱支付宝公钥
  notify-url: https://example.com/alipay/notify

配置属性类:

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "alipay")
public class AlipayProperties {

    private String protocol;
    private String gatewayHost;
    private String signType;
    private String appId;
    private String merchantPrivateKey;
    private String alipayPublicKey;
    private String notifyUrl;

    // getter 和 setter
}

初始化类:

import com.alipay.easysdk.factory.Factory;
import com.alipay.easysdk.kernel.Config;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

@Component
public class AlipayConfigInitializer {

    private final AlipayProperties properties;

    public AlipayConfigInitializer(AlipayProperties properties) {
        this.properties = properties;
    }

    @PostConstruct
    public void init() {
        validate(properties);

        Config config = new Config();
        config.protocol = properties.getProtocol();
        config.gatewayHost = properties.getGatewayHost();
        config.signType = properties.getSignType();
        config.appId = properties.getAppId();
        config.merchantPrivateKey = properties.getMerchantPrivateKey();
        config.alipayPublicKey = properties.getAlipayPublicKey();
        config.notifyUrl = properties.getNotifyUrl();

        Factory.setOptions(config);
    }

    private void validate(AlipayProperties properties) {
        requireText(properties.getProtocol(), "alipay.protocol");
        requireText(properties.getGatewayHost(), "alipay.gateway-host");
        requireText(properties.getSignType(), "alipay.sign-type");
        requireText(properties.getAppId(), "alipay.app-id");
        requireText(properties.getMerchantPrivateKey(),
            "alipay.merchant-private-key");
        requireText(properties.getAlipayPublicKey(),
            "alipay.alipay-public-key");
    }

    private void requireText(String value, String name) {
        if (value == null || value.trim().isEmpty()) {
            throw new IllegalStateException(name + " 未配置");
        }
    }
}

这样,应用会在启动时直接报告缺少的参数,不必等到用户发起支付后才暴露难以定位的空指针异常。

检查 Spring 初始化是否生效

即使已经写了 Factory.setOptions(config),也要确认这段代码实际执行过:

  1. 初始化类是否添加了 @Component,或者通过 @Bean 注册。
  2. 初始化类是否位于 Spring Boot 启动类的包扫描范围内。
  3. @PostConstruct 方法是否因 Bean 创建失败而没有执行。
  4. 当前激活的 Profile 是否加载了对应配置,例如 application-dev.yml。
  5. 配置前缀与字段名称是否一致。
  6. 支付代码是否在 Spring 容器初始化完成前被调用。
  7. 单元测试是否绕过 Spring 容器,直接实例化了 PayService。

不要只打印一句”配置已加载”。日志应确认非敏感字段,同时避免输出完整私钥:

log.info(
    "Alipay initialized: appId={}, gatewayHost={}, privateKeyConfigured={}, alipayPublicKeyConfigured={}",
    properties.getAppId(),
    properties.getGatewayHost(),
    hasText(properties.getMerchantPrivateKey()),
    hasText(properties.getAlipayPublicKey())
);

排查依赖版本冲突

当前调用栈包含:

alipay-easysdk-2.2.0.jar
easysdk-kernel-1.0.8.jar

仅凭这两行堆栈还不能断定存在冲突,因为上层 SDK 和内核组件可能采用不同的版本号。但如果项目显式声明了 easysdk-kernel,或者其他依赖传递引入了另一版本,就可能导致运行时类结构与调用方预期不一致。

先检查完整依赖树:

mvn dependency:tree -Dincludes=com.alipay

Gradle 项目可以执行:

./gradlew dependencies --configuration runtimeClasspath

通常只需声明顶层 EasySDK 依赖,无须另外指定内核版本:

<dependency>
    <groupId>com.alipay.sdk</groupId>
    <artifactId>alipay-easysdk</artifactId>
    <version>2.2.0</version>
</dependency>

如果依赖树中有多个 easysdk-kernel 版本,需要排除不符合预期的传递依赖,并以当前 EasySDK 版本声明的依赖关系为准。修改后重新构建,确认运行包中没有残留旧版本 JAR。

检查密钥与应用是否匹配

解决初始化问题后,还要确认沙箱参数来自同一个应用:

  • appId 必须是沙箱应用的 AppId。
  • merchantPrivateKey 是应用私钥,不是支付宝公钥或应用公钥。
  • alipayPublicKey 是沙箱控制台提供的支付宝公钥。
  • 应用公钥模式与证书模式不能混用。
  • 从配置文件或环境变量读取私钥时,必须保留完整内容。
  • YAML 中的多行密钥要注意缩进和换行,必要时可以使用单行形式保存。
  • 沙箱账号、沙箱网关和沙箱应用配置需要配套使用。

密钥错误一般会在请求签名或支付宝响应阶段触发明确的业务异常,而不是直接在 getConfig() 中产生空指针。不过,如果密钥字段本身为 null,具体表现还会受到 SDK 版本和执行路径的影响,因此仍需在应用启动时主动校验。

不要直接创建未配置的客户端

应通过已经完成初始化的 Factory 发起调用:

Factory.Payment.Page().pay(
    subject,
    outTradeNo,
    totalAmount,
    returnUrl
);

不要传入空配置手动构造客户端:

// 错误示例
com.alipay.easysdk.payment.page.Client client =
    new com.alipay.easysdk.payment.page.Client(null);

如果项目封装了自己的支付宝客户端工厂,也需要检查工厂返回值,以及传入的 Config 是否可能为空。

建议的排查顺序

  1. 在 PayService.pay() 调用前确认 Factory.setOptions(config) 已经执行。
  2. 为 appId、网关、私钥和支付宝公钥添加非空校验。
  3. 确认 Spring 配置类已被扫描,并且当前 Profile 的配置文件已经加载。
  4. 确认使用沙箱网关 openapi.alipaydev.com。
  5. 检查是否手动创建了 EasySDK 客户端并传入空配置。
  6. 查看 Maven 或 Gradle 依赖树,排除重复或旧版内核依赖。
  7. 清理并重新构建应用,避免部署包继续携带旧 JAR。
  8. 初始化异常消失后,再排查签名错误、应用权限或支付参数等业务问题。

根据当前堆栈,应该先检查全局配置是否完成初始化,以及依赖版本是否一致。Spring MVC 和 Tomcat 的后续调用记录只是异常向上传播的路径,并非空指针真正产生的位置。

备注:内容仅供参考。