12 November 2017

ว่าด้วยเรื่อง @SerializedName ใน Gson และ ProGuard

Updated on


        ถ้านึกจะเขียนแอปฯที่เอาไว้เรียกข้อมูลจาก Web Service ก็คงไม่พ้น Retrofit ยอดนิยมที่คอมโบคู่กับ Gson เพื่อแปลงข้อมูลจาก JSON ให้กลายเป็น Object (Model Class)  และในบทความนี้ก็จะมาพูดถึง @SerializedName ของ Gson เมื่อต้องใช้ ProGuard กันครับ

        เจ้าของบล็อกก็เป็นคนหนึ่งที่ประทับใจใน @SerializedName มากๆ เพราะว่ามันแก้ปัญหาเรื่อง Key ใน JSON ไม่ตรงกับชื่อของตัวแปรใน Object เพราะว่าวิธีการตั้งชื่อของทั้งสองอย่างนั้นไม่เหมือนกันหรืออยากจะเปลี่ยนเป็นชื่ออื่นเลยก็ทำได้เช่นกัน

public class AwesomeProfile implements Parcelable {
    @SerializedName("html_url")
    private String githubUrl;
    ...
}

         และเจ้าของบล็อกก็เจอบ่อยที่นักพัฒนาใช้ @SerializedName บ้างหรือไม่ใช้บ้างแบบนี้

public class AwesomeProfile implements Parcelable {
    private String login;
    private String id;
    @SerializedName("avatar_url")
    private String avatarUrl;
    @SerializedName("html_url")
    private String githubUrl;
    private String bio;
    private String email;
    private String blog;
    private String company;
    private String name;
    @SerializedName("public_repos")
    private int publicRepos;
    @SerializedName("public_gists")
    private int publicGists;
    private int followers;
    private int following;

    ...
}

        ถ้าดูเผินๆก็คงไม่คิดอะไรมาก เพราะว่าจะใช้ @SerializedName ไปทำไม ในเมื่อชื่อตัวแปรมันก็ตรงกับ Key ใน JSON อยู่แล้ว และมันก็สามารถทำงานได้ปกติสุข

จนกระทั่งใส่ ProGuard ตอน Release ขึ้น Production...

        ปัญหาจะผุดขึ้นมาทันทีเมื่อต้องใส่ ProGuard ตอนจะเอาขึ้น Production เพราะว่าจะมีขั้นตอน Obfuscate ที่แปลงชื่อตัวแปรและชื่อคลาสให้อ่านยากขึ้น ดังนั้นเมื่อเอาไฟล์ Release APK มา Decompile ดู ก็จะเห็นโค้ดที่เปลี่ยนไปดังนี้

public class a implements Parcelable {
    private String a;
    private String b;
    @c(a = "avatar_url")
    private String c;
    @c(a = "html_url")
    private String d;
    private String e;
    private String f;
    private String g;
    private String h;
    private String i;
    @c(a = "public_repos")
    private int j;
    @c(a = "public_gists")
    private int k;
    private int l;
    private int m;
    ...
}

         จะเห็นว่าชื่อต่างๆถูกแปลงให้เป็นตัวอักษรสั้นๆทั้งหมดเลย ขนาด @SerializedName ยังถูกแปลงเป็น @c เลย

         และนั่นหมายความว่า Gson จะแปลงข้อมูลผิดทันที เพราะว่าชื่อตัวแปรไม่ตรงกับ Key ใน JSON มีแค่อันที่ใส่ @SerializedName เท่านั้นที่ยังถูกต้อง เพราะชื่อ Key ที่กำหนดไว้ในนั้นเป็น String ธรรมดาๆ

        เมื่อแปลงข้อมูลไม่ได้ ค่าที่ไม่ได้กำหนด @SerializedName ก็จะมีค่าเป็น Null ไปโดยปริยาย และทำให้แอปฯเกิด NullPointerException ได้ทันที

        แต่ทว่าก็มีนักพัฒนาบางคนใช้วิธีกำหนดค่าในไฟล์ proguard-rules.pro เพื่อบอก ProGuard ว่า "อย่ามายุ่งกับคลาสนี้นะ!!"

// เฉพาะ AwesomeProfile.java
-keep class com.akexorcist.awesomeapp.AwesomeProfile { *; }

// ทุกคลาสที่อยู่ใน com.akexorcist.awesomeapp แม่มเลย
-keep class com.akexorcist.awesomeapp.** { *; }

        พอลอง Decompile ใหม่อีกครั้งก็จะเห็นว่าชื่อตัวแปรกลับมาตรงกับ JSON แล้ว แต่ @SerializedName ก็ถูกย่อเป็น @c เหมือนเดิม แต่ก็ไม่มีปัญหาอะไร ยังคงทำงานได้ปกติเหมือนเดิม

public class AwesomeProfile implements Parcelable {
    @c(a = "avatar_url")
    private String avatarUrl;
    private String bio;
    private String blog;
    private String company;
    private String email;
    private int followers;
    private int following;
    @c(a = "html_url")
    private String githubUrl;
    private String id;
    private String login;
    private String name;
    @c(a = "public_gists")
    private int publicGists;
    @c(a = "public_repos")
    private int publicRepos;
    ...
}

แต่นั่นใช่วิธีที่ถูกต้องหรือ?

        การที่นักพัฒนาใส่ ProGuard สาเหตุหลักก็คือป้องกันการถูก Decompile ด้วยการทำให้โค้ดนั้นอ่านได้ยาก แต่ถ้าไปใส่คำสั่งไว้ใน proguard-rules.pro เพื่อบอกว่าไม่ต้องยุ่งกับคลาสนั้นๆ

        แล้วจะใส่ ProGuard ไปเพื่ออะไรล่ะ?

        ดังนั้นวิธีที่ดีกว่าก็คือ

ใส่ @SerializedName ให้กับตัวแปรทุกตัวเสมอ

         ถึงแม้ว่าชื่อตัวแปรจะตรงกับ Key ใน JSON อยู่แล้ว แต่ว่าผู้ที่หลงเข้ามาอ่านก็ควรใส่ @SerializedName เสมอครับ เพื่อให้ทำงานได้ปกติเมื่อมีการใส่ ProGuard

public class AwesomeProfile implements Parcelable {
    @SerializedName("login")
    private String login;
    @SerializedName("id")
    private String id;
    @SerializedName("avatar_url")
    private String avatarUrl;
    @SerializedName("html_url")
    private String githubUrl;
    @SerializedName("bio")
    private String bio;
    @SerializedName("email")
    private String email;
    @SerializedName("blog")
    private String blog;
    @SerializedName("company")
    private String company;
    @SerializedName("name")
    private String name;
    @SerializedName("public_repos")
    private int publicRepos;
    @SerializedName("public_gists")
    private int publicGists;
    @SerializedName("followers")
    private int followers;
    @SerializedName("following")
    private int following;
    ...
}

        ใส่เถอะครับ ถึงแม้ว่าจะดูเยอะ แต่ทว่าในความเป็นจริง Gson ก็ช่วยลดโค้ดได้เยอะแล้ว ดังนั้นการใส่ @SerializedName ไว้ทุกตัวก็ไม่ได้ทำให้ชีวิตลำบากขึ้นซักเท่าไรนัก แถมคลาสนั้นๆของผู้ที่หลงเข้ามาอ่านก็สามารถ Obfuscate เพื่อทำให้อ่านโค้ดตอน Decompile ได้ยากอีกด้วย