­

Java變數命名前倆個字母僅含有一個大寫字母的坑

背景

前幾周在做項目fetch切換,即將HttpUtils調用改成使用Feign調用。大概程式碼如下:

// 原程式碼
String resultJson = HttpUtil.get(url + "/fin/test?code=" + code, null);
RespDTO<Result> respDTO = JSON.parseObject(resultJson, new TypeReference<RespDTO<Result>>() {});

// 現程式碼如下
RespDTO<Result> respDTO = urlClient.getTest(code);

程式碼上線後,出現了異常。表現為:respDTO的某個欄位為null,但是第三方是有傳過來這個值的。

問題復現

public static void main(String[] args) throws JsonProcessingException, IntrospectionException {
	String data = "{\"aFiled\":10,\"normalFiled\":20}";

  // 原方式
	Domain domain = JSON.parseObject(data, Domain.class);
	System.out.println("domain = " + domain.getAFiled());

	// feign方式
	ObjectMapper mapper = new ObjectMapper();
	Domain domain1 = mapper.readValue(data, Domain.class);
	System.out.println("domain1 = " + domain1.getAFiled());
}

//github.com/wangjie-fourth/jackson01

問題分析

既然請求返回數據,但接收對象沒有對應的值,那就說明字元串沒有反序列化到指定的變數名上。改之前是使用FastJson反序列化數據,改之後的Feign默認是採用Jackson反序列化數據。那麼為什麼FastJson可以反序列化aFiledJackson不可以呢?

FastJson是根據變數名稱來反序列化的,也就是說它接收到aFiled數據,就會到對象找aFiled變數名,然後附上值;而Jackson默認是根據JavaBean規範找到對應屬性後再賦值,也就是說Jackson並沒有在這個對象找到aFiled屬性。

JavaBean屬性名稱跟變數名稱是不一定相同的。JavaBean是通過對象的getset方法來確定對象屬性,其屬性名稱是由其對象變數來決定的,通常的邏輯是:將屬性首字母轉成大寫。但也有例外就是,前倆個字母都是大寫的情況:

8.8 Capitalization of inferred names.
When we use design patterns to infer a property or event name, we need to decide what rulesto follow for capitalizing the inferred name. If we extract the name from the middle of a normalmixedCase style Java name then the name will, by default, begin with a capital letter.
Java programmers are accustomed to having normal identifiers start with lower case letters.Vigorous reviewer input has convinced us that we should follow this same conventional rulefor property and event names.
Thus when we extract a property or event name from the middle of an existing Java name, wenormally convert the first character to lower case. However to support the occasional use of allupper-case names, we check if the first two characters of the name are both upper case and ifso leave it alone. So for example,
「FooBah」 becomes 「fooBah」
「Z」 becomes 「z」
「URL」 becomes 「URL」
We provide a method Introspector.decapitalize which implements this conversion rule.

Jackson根據getset方法來確定屬性的名稱。而變數前倆個字母含有一個大寫字母對應的屬性名稱會很怪。如下:

我們項目上使用的是lombok,其生成的getset方法是不遵循JavaBean規範的,只是將變數名的首字母大寫而已,所以它生成aFiled的方法是getAFiled
所以,Jackson在接收到aFiled屬性值,它會到對象找setaFiled方法,自然這個對象是沒有這個方法的,所以就沒映射到aFiled欄位上。

解決辦法

jackson可以使用@JsonProperty註解來指定屬性名稱。

總結

出現這個就是因為JavaBean規範對前倆個字母含有大寫字母的變數名做了特殊處理。 Jackson遵循JavaBean規範來反序列化,而項目使用的Lombok插件是不遵循JavaBean規範。

擴展

JavaBean規範對前倆個字母含有大寫字母的處理不全

針對變數名前倆個字母做個窮舉,就是如下對象:

public class Domain {
    private Integer aFiled;
    private Integer afiled;
    private Integer AFiled;
    private Integer Afiled;

    public Integer getaFiled() {
        return aFiled;
    }

    public void setaFiled(Integer aFiled) {
        this.aFiled = aFiled;
    }

    public Integer getAfiled() {
        return afiled;
    }

    public void setAfiled(Integer afiled) {
        this.afiled = afiled;
    }

    public Integer getAFiled() {
        return AFiled;
    }

    public void setAFiled(Integer AFiled) {
        this.AFiled = AFiled;
    }
}

你會發現afiledAfiled生成的屬性是一致的!不過好在項目中沒有人只將首字母大寫這種命名風格。

謹慎用Lombok替換舊項目的getset方法

舊項目一般都是用Idea來生成getset方法,而Idea是遵循JavaBean規範來生成屬性的。所以,舊項目如果含有前倆個僅含有一個大寫字母時,盡量不用藥lombok去替換。

參考鏈接

java bean規範文檔
jackson 序列化問題