Spring Boot靜態成員注入致使的NullPointerException(NPE) 問題

問題場景

有一個工具類, 用於對支付參數進行簽名, 其中使用了 @ConfigurationProperties 配置類. 簽名工具類以下: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 的輸出爲 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,
而後, 建立這個代理類, 並使用PostConstructSignUtil 工具類的 PayProperties 成員進行手工注入:spring

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.javaapi

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();
    }
}
相關文章
相關標籤/搜索