服务器数据解析篇
在客户端开发过程中一个重点内容就是解析服务器数据,关于这个话题也许大家首先会去思考的问题是用哪个json解析库。是的,目前通过json格式进行数据传输是主流的方式,确实不同的json解析库在性能方面也有一些差异。以上问题固然重要,但是在开发过程中经常遇到的往往并不是性能相关的问题,笔者从数据使用的角度给大家分享一些经验。
数据成员设为private,默认不提供set方法
解析得到的数据在产品中被修改的几率极低,数据的修改很容易导致bug的产生。另外,数据对象解析的过程中往往是通过反射直接构建对象,基于以上两点思考,我们提倡对数据的修改遵循关闭原则,即默认不提供set方法,只有极少数情况允许对数据进行修改。
- 提供友好的数据访问接口
读取数据时往往是带着具体的场景使用的目的而来,最原始的数据通常并不能够直接满足使用,经常需要做一些简单处理。因此,通过get方法提供更友好的数据获取方式。在get方法中不只是返回最原始的数据,而是叠加上一些简单的处理或者数据的拼装。
例如:
public class LearnInfo {
// 原始数据
private int learnStatus;
// 简单封装后的方法
public boolean isLearned() {
return learnStatus == 2;
}
}
public class Example {
private void doSomething1(LearnInfo learnInfo) {
// 通过获取原始数据做业务处理
if (learnInfo.getLearnStatus() == 2) {
// dosomething
}
}
private void doSomething2(LearnInfo learnInfo) {
// 通过简单封装过的接口获取状态信息
if (learnInfo.isLearned()) {
// do something
}
}
}
- 隔离json字符串与代码调用之间的耦合
数据解析库往往通过反射调用成员字段,导致成员变量的命名与json串中的字符存在严格的对应关系,通过get方法可以屏蔽掉这种强耦合依赖。
例如:版本1.0
public class Course {
private description;
public String getDescription() {
if (description == null) {
return "";
}
return description;
}
}
public class Example {
private void doSomething(Course course) {
mTextView.setText(course.getDescription());
}
}
版本1.1,由于某些原因在版本迭代过程中后端字段可能会发生更改
public class Course {
// 此处被修改了
private desc;
// 接口不变
public String getDescription() {
if (desc == null) {
return "";
}
return desc;
}
}
public class Example {
// 调用方无需修改
private void doSomething(Course course) {
mTextView.setText(course.getDescription());
}
}
- 调试与日志
Android Studio中针对成员变量可以通过Field Watchpoint进行断点调试,而引入get方法之后,增加了常规断点调试的方式。在有必要的情况下,还可以在get方法中增加日志。
- 引入数据合法性检查机制
一旦非法数据进入到系统以后,会带来很多异常的情况,在代码设计时异常逻辑分支处理不够充分的话,很容易导致系统异常,甚至应用程序崩溃。因此,比较建议尽量在源头堵截住异常数据的进入,越早处理对后期的影响越小。一般来说从服务器获取数据在将其解析为系统中真正有含义的对象时,视为检查数据合法性的第一时间比较恰当。常见的非法数据
- 不允许空的字段返回了空值
java语言对空指针的处理并不友好,NullPointerException是导致崩溃的主要原因之一。在系统迭代过程中经常出现数据结构中该返回值的地方却返回了空值。针对这一问题有多种解决方案,有一种做法是做系统全局的异常捕获,这种做法简单粗暴,没有从根源上解决问题,不够优雅。另一种做法在用到数据的地方全部加上判空,这种做法繁琐,容易遗漏,开发人员比较痛苦。笔者在项目开发过程中尝试了从框架层面解决这一问题的技术方案。引入Nullable与NotNull注解的支持,在定义字段时通过注解注明,在解析时校验字段值是否符合注解描述。注解方式一般来说只适合解决与后端约定不允许空的值的校验。事实上为了满足更好的用户体验,更多的字段在设计时会倾向于允许空值。因此在允许空值进入的情况下,在设计get方法时要求不能返回空值,做第二层防护。
例如:
public class Course {
@NotNull
private long id; // id 不允许空
private List<Unit> units; // 列表可以为空
private String description; // 描述信息可以为空
// 不允许直接返回null值,降低调用方的使用成本
public List<Unit> getUnits() {
if (units == null) {
return new ArrayList();
}
return units;
}
// 不允许返回null值,降低调用方的使用成本
public String getDescription() {
if (description == null) {
return "";
}
return description;
}
}
- 返回的数据不符合逻辑
在某些情况下,解析得到的数据不符合逻辑,例如:
public class PageInfo {
private totalCount;
private currentPage;
}
totalCount : 0, currentPage : 11
由于服务器上异常处理不当,客户端解析得到totalCount为0,currentPage为11,显然不符合逻辑。若将此数据继续往下传递很有可能引发数组越界的异常,从而导致应用程序崩溃。
因此,我们还引入了第三层防护机制,定义LegalModel接口,代码如下:
public interface LegalModel {
boolean check();
}
public class PageInfo implements LeagalModel {
private totalCount;
private currentPage;
@Override
public boolean check() {
return totalCount > currentPage;
}
}
public class Example {
private void doSomething(PageInfo pageInfo) {
if (!pageinfo.check()) {
// 判断数据不合法
}
}
}
- 关于混淆
当json解析库通过反射构建数据对象时,数据对象类不能参与混淆,否则就无法找到对应的字段名。避免混淆通常的方法是将数据模型类集中放置在相同的包名下面,在混淆配置中通过配置路径来避免混淆。这种方式有一个比较明显的局限性,即当文件换一个路径以后,则要在配置文件中追加相应的路径。这种限制对开发人员操作过程中非常不友好,比较繁琐,而且特别容易遗漏。因此,在此笔者推荐大家一种通过空接口的方式,来解决避免混淆问题。
代码如下:
// 定义防止混淆空接口
public interface NoProguard {
}
// 不需要混淆的类去实现空接口
public class Course implements NoProguard {
private String description;
// 内部类同样适用
public static class LearnInfo implements NoProguard {
private int learnStatus;
}
}
// 配置代码
-keep interface com.example.NoProguard {*;}
-keep class * implements com.example.NoProguard {*;}
- 建议使用基本类型取代包装类
包装类的默认值为null,非常容易引起空指针异常。而基本类型自带默认值,省去了大量的判空代码。以int与Integer为例,某些情况下对于int默认值0会有一定的含义,此种场景的机率不高,一般来说在约定时可以尽量避免使用0值来简单规避。