问题场景
有一个工具类, 用于对支付参数进行签名, 其中使用了 @ConfigurationProperties 配置类. 签名工具类如下:
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.DigestUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
@Slf4j
public class SignUtil {
private static PayProperties payProperties;
@Autowired
public static void setPayProperties(PayProperties payProperties) {
SignUtil.payProperties = payProperties;
}
public static String signParams(PayOrderRequest payOrderRequest, String apiKey) {
////////////////////////////////////////////////////////////////////
// 这里 payProperties 的输出为 null, 访问它将会导致 NullPointerException
////////////////////////////////////////////////////////////////////
log.info("============================: {}", payProperties);
// 排序
Map<String, String> map = new TreeMap<>();
map.put("merchantOrderId", payOrderRequest.getMerchantOrderId());
map.put("createdAt", payOrderRequest.getCreatedAt());
...
...
...
List<String> params = new ArrayList<>();
for (String key : map.keySet()) {
log.debug("参数: {}", String.format("%s=%s", key, map.get(key)));
params.add(map.get(key));
}
params.add(apiKey);
// 拼接
String plainTextParams = String.join("|", params);
log.info("支付签名参数拼接: {}", plainTextParams);
// 签名并返回
return DigestUtils.md5DigestAsHex(plainTextParams.getBytes()).toUpperCase();
}
}
这样是不行的,
PayProperties
是一个静态成员, Spring 容器在初始化过程中如果看到这是一个静态的成员, 它会直接跳过这个成员字段, 处理下一个字段.
解决办法
使用一个代理Bean类对静态成员进行初始化, 这个代理Bean可以叫做
StaticContextInitializer
(静态山下文初始化器)
首先, 这个代理类需要使用 @Component
进行注解
其次, 删除上述代码中静态方法 setPayProperties
的注解 @Autowired
,
然后, 创建这个代理类, 并使用PostConstruct
对 SignUtil
工具类的 PayProperties
成员进行手工注入:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@Component
public class StaticContextInitializer {
private PayProperties anyiProperties;
@Autowired
public void setAnyiProperties(PayProperties anyiProperties) {
this.anyiProperties = anyiProperties;
}
@PostConstruct
public void init() {
SignUtil.setPayProperties(anyiProperties);
}
}
可测试的示例代码
StaticContextInitializer.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
@Component
public class StaticContextInitializer {
private PayProperties anyiProperties;
@Autowired
public void setAnyiProperties(PayProperties anyiProperties) {
this.anyiProperties = anyiProperties;
}
@PostConstruct
public void init() {
SignUtil.setPayProperties(anyiProperties);
}
}
SignUtil.java
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.DigestUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
@Slf4j
public class SignUtil {
private static PayProperties payProperties;
@Autowired
public static void setPayProperties(PayProperties payProperties) {
SignUtil.payProperties = payProperties;
}
public static String signParams(PayOrderRequest payOrderRequest, String apiKey) {
////////////////////////////////////////////////////////////////////
// payProperties 字段已经通 StaticContextInitializer.init() 进行注入
// 不会再出现 NPE 问题
////////////////////////////////////////////////////////////////////
log.info("============================: {}", payProperties);
// 排序
Map<String, String> map = new TreeMap<>();
map.put("merchantOrderId", payOrderRequest.getMerchantOrderId());
map.put("createdAt", payOrderRequest.getCreatedAt());
List<String> params = new ArrayList<>();
for (String key : map.keySet()) {
log.debug("参数: {}", String.format("%s=%s", key, map.get(key)));
params.add(map.get(key));
}
params.add(apiKey);
// 拼接
String plainTextParams = String.join("|", params);
log.info("支付签名参数拼接: {}", plainTextParams);
// 签名并返回
return DigestUtils.md5DigestAsHex(plainTextParams.getBytes()).toUpperCase();
}
}