ถ้านึกจะเขียนแอปฯที่เอาไว้เรียกข้อมูลจาก 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 เสมอครับ เพื่อให้ทำงานได้ปกติเมื่อมีการใส่ ProGuardpublic 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 ได้ยากอีกด้วย