支付宝沙箱支付出现 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),也要确认这段代码实际执行过:
- 初始化类是否添加了
@Component,或者通过@Bean注册。 - 初始化类是否位于 Spring Boot 启动类的包扫描范围内。
@PostConstruct方法是否因 Bean 创建失败而没有执行。- 当前激活的 Profile 是否加载了对应配置,例如
application-dev.yml。 - 配置前缀与字段名称是否一致。
- 支付代码是否在 Spring 容器初始化完成前被调用。
- 单元测试是否绕过 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 是否可能为空。
建议的排查顺序
- 在
PayService.pay()调用前确认Factory.setOptions(config)已经执行。 - 为
appId、网关、私钥和支付宝公钥添加非空校验。 - 确认 Spring 配置类已被扫描,并且当前 Profile 的配置文件已经加载。
- 确认使用沙箱网关
openapi.alipaydev.com。 - 检查是否手动创建了 EasySDK 客户端并传入空配置。
- 查看 Maven 或 Gradle 依赖树,排除重复或旧版内核依赖。
- 清理并重新构建应用,避免部署包继续携带旧 JAR。
- 初始化异常消失后,再排查签名错误、应用权限或支付参数等业务问题。
根据当前堆栈,应该先检查全局配置是否完成初始化,以及依赖版本是否一致。Spring MVC 和 Tomcat 的后续调用记录只是异常向上传播的路径,并非空指针真正产生的位置。
备注:内容仅供参考。
