@NonNull導致無法序列化的問題及解決

@NonNull導致無法序列化的問題

以上這個代碼在接參的時候報瞭一個缺少無參構造函數無法序列化的錯誤

將.class反編譯

可以看到編譯後的源碼中生成瞭一個有參構造 明顯是 用來判空的 假設那麼這個構造函數應該就是根據@NonNull生成的

實際上我們治理應該添加的註解是NotNull才對

上面因為lombook根據@NonNull生成瞭一個有參構造函數,導致jdk不會添加默認的無參構造函數,沒有無參構造函數的話 序列化就會失敗.

@NonNull修飾Field反序列化部分值為空

一般Http接口,為瞭參數統一管理,定義一個VO用來接收POST過來的字段,常規做法是把參數解析成map,然後反序列化到VO中,早期定義的接口字段都非空,所以VO中都加瞭@NonNull註解;一直很和諧;

因為需求變化,接口字段需要增加兩個,為瞭版本兼容,新加的兩個字段需要可空;於是在VO中增加兩個字段,不用@NonNull修飾,但是反序列化後發現這兩個字段一直為空!怎麼都不能從map中獲取到這兩個值!

分析

版本:

  • JDK:1.8
  • lombok:1.18.12
  • fastjson:1.2.60

原代碼

package com.example.demo;

import lombok.Data;
import lombok.NonNull;

@Data
public class DemoRequestVO {

    @NonNull
    private String firstParam;

    private String SecondParam;

    private String thirdParam;

}
 public static void testDemo(){

        Map<String, String> params = new HashMap<>();
        params.put("firstParam","lllllll");
        params.put("secondParam","45454645");
        params.put("thirdParam","xx公司");
        
        DemoRequestVO request = JSON.parseObject(JSON.toJSONString(params), DemoRequestVO.class);
        System.out.println(request);
    }

分析原因

做兩方面猜測:

1: 註解提供者問題

  • 2: Json反序列化問題
  • 1: 先看下: 註解提供者 @NonNull

發現其是作用於RetentionPolicy.CLASS的

package lombok;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.LOCAL_VARIABLE, ElementType.TYPE_USE})
@Retention(RetentionPolicy.CLASS)
@Documented
public @interface NonNull {
}

查看lombok源碼可以看到,NonNull註解提供者一共這麼多

static {
        NONNULL_ANNOTATIONS = Collections.unmodifiableList(Arrays.asList(new String[] {
            "androidx.annotation.NonNull",
            "android.support.annotation.NonNull",
            "com.sun.istack.internal.NotNull",
            "edu.umd.cs.findbugs.annotations.NonNull",
            "javax.annotation.Nonnull",
            // "javax.validation.constraints.NotNull", // The field might contain a null value until it is persisted.
            "lombok.NonNull",
            "org.checkerframework.checker.nullness.qual.NonNull",
            "org.eclipse.jdt.annotation.NonNull",
            "org.eclipse.jgit.annotations.NonNull",
            "org.jetbrains.annotations.NotNull",
            "org.jmlspecs.annotation.NonNull",
            "org.netbeans.api.annotations.common.NonNull",
            "org.springframework.lang.NonNull",
        }));

再看下經過註解後編譯的CLASS

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.example.demo;

import lombok.NonNull;

public class DemoRequestVO {
    @NonNull
    private String firstParam;
    private String SecondParam;
    private String thirdParam;

    public DemoRequestVO(@NonNull final String firstParam) {
        if (firstParam == null) {
            throw new NullPointerException("firstParam is marked non-null but is null");
        } else {
            this.firstParam = firstParam;
        }
    }

    @NonNull
    public String getFirstParam() {
        return this.firstParam;
    }

    public String getSecondParam() {
        return this.SecondParam;
    }

    public String getThirdParam() {
        return this.thirdParam;
    }

    public void setFirstParam(@NonNull final String firstParam) {
        if (firstParam == null) {
            throw new NullPointerException("firstParam is marked non-null but is null");
        } else {
            this.firstParam = firstParam;
        }
    }

    public void setSecondParam(final String SecondParam) {
        this.SecondParam = SecondParam;
    }

    public void setThirdParam(final String thirdParam) {
        this.thirdParam = thirdParam;
    }

    public boolean equals(final Object o) {
        if (o == this) {
            return true;
        } else if (!(o instanceof DemoRequestVO)) {
            return false;
        } else {
            DemoRequestVO other = (DemoRequestVO)o;
            if (!other.canEqual(this)) {
                return false;
            } else {
                label47: {
                    Object this$firstParam = this.getFirstParam();
                    Object other$firstParam = other.getFirstParam();
                    if (this$firstParam == null) {
                        if (other$firstParam == null) {
                            break label47;
                        }
                    } else if (this$firstParam.equals(other$firstParam)) {
                        break label47;
                    }

                    return false;
                }

                Object this$SecondParam = this.getSecondParam();
                Object other$SecondParam = other.getSecondParam();
                if (this$SecondParam == null) {
                    if (other$SecondParam != null) {
                        return false;
                    }
                } else if (!this$SecondParam.equals(other$SecondParam)) {
                    return false;
                }

                Object this$thirdParam = this.getThirdParam();
                Object other$thirdParam = other.getThirdParam();
                if (this$thirdParam == null) {
                    if (other$thirdParam != null) {
                        return false;
                    }
                } else if (!this$thirdParam.equals(other$thirdParam)) {
                    return false;
                }

                return true;
            }
        }
    }

    protected boolean canEqual(final Object other) {
        return other instanceof DemoRequestVO;
    }

    public int hashCode() {
        int PRIME = true;
        int result = 1;
        Object $firstParam = this.getFirstParam();
        int result = result * 59 + ($firstParam == null ? 43 : $firstParam.hashCode());
        Object $SecondParam = this.getSecondParam();
        result = result * 59 + ($SecondParam == null ? 43 : $SecondParam.hashCode());
        Object $thirdParam = this.getThirdParam();
        result = result * 59 + ($thirdParam == null ? 43 : $thirdParam.hashCode());
        return result;
    }

    public String toString() {
        return "DemoRequestVO(firstParam=" + this.getFirstParam() + ", SecondParam=" + this.getSecondParam() + ", thirdParam=" + this.getThirdParam() + ")";
    }
}

重點是看這個編譯後的class的構造方法:隻有一個帶@NonNull註解參數的構造方法!!!

一般到這裡都能想到反序列化後的為啥另外兩個未註解NonNull的為啥空值瞭;如果沒想到,也沒關系,咱們再來看看JSON反序列化的過程

2: json反序列化;

一系列遞進過程不再描述,重點看JavaBeanInfo類中的build方法,這個方法是真正把map反序化到javaBean的過程

public static JavaBeanInfo build(Class<?> clazz, Type type, PropertyNamingStrategy propertyNamingStrategy, boolean fieldBased, boolean compatibleWithJavaBean, boolean jacksonCompatible) 

挑幾處重要的開看下:

取構造方法list

        Constructor[] constructors = clazz.getDeclaredConstructors();
        Constructor<?> defaultConstructor = null;
        if (!kotlin || constructors.length == 1) {
            if (builderClass == null) {
                defaultConstructor = getDefaultConstructor(clazz, constructors);
            } else {
                defaultConstructor = getDefaultConstructor(builderClass, builderClass.getDeclaredConstructors());
            }
        }

賦值創建javaBean的構造

   boolean is_public = (constructor.getModifiers() & 1) != 0;
                                if (is_public) {
                                    String[] lookupParameterNames = ASMUtils.lookupParameterNames(constructor);
                                    if (lookupParameterNames != null && lookupParameterNames.length != 0 && (creatorConstructor == null || paramNames == null || lookupParameterNames.length > paramNames.length)) {
                                        paramNames = lookupParameterNames;
                                        creatorConstructor = constructor;
                                    }
                                }

創建javaBean,看傳參; 隻有一個構造方法;

 if (!kotlin && !clazz.getName().equals("javax.servlet.http.Cookie")) {
                        return new JavaBeanInfo(clazz, builderClass, (Constructor)null, creatorConstructor, (Method)null, (Method)null, jsonType, fieldList);
                    }

結論:

使用@NonNull註解,編譯後生成的CLASS構造方法隻有一個,且隻有被註解的字段才能構造時候賦值;此種做法是保證在編譯期可以判斷非空;

反序列化時候使用瞭這個構造方法,其他的值沒有被賦值;

建議改進

1: 使用@NotNull代替

2: 如果修飾的是String類型,推薦使用@NotBlank,好處是可以判斷空字符串

3: 在自定義的VO中增加一個無參構造方法;

總結

以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。

推薦閱讀: