一個Getter引發的血案
本文轉載自微信公眾號「你呀不牛」,作者不牛。轉載本文請聯系你呀不牛公眾號。
1需求
最近做一了個需求,調用其他服務的REST接口,感覺很簡單,于是迅速就搞起來了
構造Request類
- public class User {
- private String name;
- private Integer age;
- public User(String name, Integer age) {
- this.name = name;
- this.age = age;
- }
- }
啪,我上來就一new
- service.sendRequest(new User("niu", 18));
打完,收工,又是努力工作(摸魚)的一天。
2定位
但是,某天晚上8點,測試人員突然給我打電話,說調用失敗,同時本身又缺少打印,沒有辦法具體哪出問題了。
我是不會認為這么簡單的代碼自己會出錯的,不可能!!
經過網絡抓包后發現,收到的參數都是null,但是我這邊明明調用構造器傳入參數了
難道出現靈異事件了?
經過分析,整體數據流為:
能出現問題的地方只能是序列化JSON地方,于是本地測試驗證了這一結論:
- public static void main(String[] args) throws IOException {
- ObjectMapper objectMapper = new ObjectMapper();
- String request = objectMapper.writeValueAsString(new User("niu", 18));
- System.out.println(request);
- }
雖然是出問題了,但是序列化并沒有轉為屬性為null的對象,而是直接拋出異常
- Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class online.jvm.bean.User and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)
- at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:77)
通過查詢異常資料,解決掉這種異常需要在增加Jackson的序列化配置FAIL_ON_EMPTY_BEANS,FAIL_ON_EMPTY_BEANS這個配置表示如果某個bean序列化為空時不會異常失敗
- public static void main(String[] args) throws IOException {
- ObjectMapper objectMapper = new ObjectMapper();
- objectMapper.configure(FAIL_ON_EMPTY_BEANS, false);
- String request = objectMapper.writeValueAsString(new User("niu", 18));
- System.out.println(request);
- }
這種就不會報錯,而是返回序列化成空串,也就導致接受方為屬性都為null
通過看自研RPC框架看到是有該FAIL_ON_EMPTY_BEANS的配置
3解決
再來分析一下原因,Jackson序列化時需要調用bean的getter方法
1、寫上getter后再看下結果:
- public class User {
- private String name;
- private Integer age;
- public User(String name, Integer age) {
- this.name = name;
- this.age = age;
- }
- public String getName() {
- return name;
- }
- public Integer getAge() {
- return age;
- }
- public static void main(String[] args) throws IOException {
- ObjectMapper objectMapper = new ObjectMapper();
- String request = objectMapper.writeValueAsString(new User("niu", 18));
- System.out.println(request);
- // 輸出正常 : {"name":"niu","age":18}
- }
- }
2、或者把屬性訪問權限改為public
- public class User {
- public String name;
- public Integer age;
- public User(String name, Integer age) {
- this.name = name;
- this.age = age;
- }
- public static void main(String[] args) throws IOException {
- ObjectMapper objectMapper = new ObjectMapper();
- String request = objectMapper.writeValueAsString(new User("niu", 18));
- System.out.println(request);
- // 輸出正常 : {"name":"niu","age":18}
- }
- }
但是如果要求不能暴露bean的屬性即使是getter也不行呢?
3、注解 @JsonProperty
這是就需要使用Jackson提供的注解 @JsonProperty
- public class User {
- @JsonProperty("userName")
- private String name;
- @JsonProperty
- private Integer age;
- public User(String name, Integer age) {
- this.name = name;
- this.age = age;
- }
- public static void main(String[] args) throws IOException {
- ObjectMapper objectMapper = new ObjectMapper();
- String request = objectMapper.writeValueAsString(new User("niu", 18));
- System.out.println(request);
- // {"userName":"niu","age":18}
- }
- }
來看下注解@JsonProperty的源碼注釋
- Marker annotation that can be used to define a non-static method as a "setter" or "getter" for a logical property (depending on its signature), or non-static object field to be used (serialized, deserialized) as a logical property.
大體意思是注解如果用在屬性上相當于為該屬性定義getter和setter。
那如果既有getter又有@JsonProperty注解,以哪個為準呢?
- public class User {
- @JsonProperty("userName")
- private String name;
- @JsonProperty
- private Integer age;
- public User(String name, Integer age) {
- this.name = name;
- this.age = age;
- }
- public String getName() {
- return name;
- }
- public static void main(String[] args) throws IOException {
- ObjectMapper objectMapper = new ObjectMapper();
- String request = objectMapper.writeValueAsString(new User("niu", 18));
- System.out.println(request);
- // {"age":18,"userName":"niu"}
- }
- }
如果getter一個沒有的屬性,效果如何呢?
- public class User {
- @JsonProperty("userName")
- private String name;
- @JsonProperty
- private Integer age;
- public User(String name, Integer age) {
- this.name = name;
- this.age = age;
- }
- public String getName2() {
- return name;
- }
- public static void main(String[] args) throws IOException {
- ObjectMapper objectMapper = new ObjectMapper();
- String request = objectMapper.writeValueAsString(new User("niu", 18));
- System.out.println(request);
- // {"age":18,"name2":"niu","userName":"niu"}
- }
- }
這說明如果有@JsonProperty注解,先以注解為準
然后利用反射找到對象類的所有get方法,接下來去get,然后小寫化,作為json的每個key值,而get方法的返回值作為value。接下來再反射field,添加到json中。
4、特殊情況
還有一種比較特殊的情況, getter方法由lombok生成,且屬性的次首字母是大寫:
- @Getter
- public class User {
- @JsonProperty
- private String nAme;
- @JsonProperty
- private Integer age;
- public User(String name, Integer age) {
- this.nAme = name;
- this.age = age;
- }
- public static void main(String[] args) throws IOException {
- ObjectMapper objectMapper = new ObjectMapper();
- String request = objectMapper.writeValueAsString(new User("niu", 18));
- System.out.println(request);
- // {"nAme":"niu","age":18,"name":"niu"}
- }
- }
這是因為lombok生成的getter會把屬性的第一個字母變成大寫,
序列化時會把get后與小寫字母中間的大寫變成小寫,也就是會把NA變成小寫
所以序列化結果會有name(getter獲取)和nAme(注解獲取)兩個屬性
- public String getNAme() {
- return this.nAme;
- }
如果我們自己用idea快捷鍵生成getter,
此時之后序列化nAme
- public String getnAme() {
- return nAme;
- }
4小結
許多bug都是在自以為沒有問題的地方產生,看似簡單,更需要小心,同時也需要多注意序列化原理,整體感覺序列化還是用Gson更省心,完全不用關心Getter和Setter方法,會完全按照屬性名來序列化。
本文的涉及的bug過程和解決方式希望對你也有所幫助,再見。