在一般业务开发流程中,会对需求进行评审,而评审之后就是,总是会有这样的一种情况,服务端和客户端都需要根据需求或者UI开始进行联调前开发,而之后才会进入前后端的接口联调阶段。而这个阶段中,服务端一般只会给到一个API文档,用于定义各个模块中的数据结构。作为Android开发,我们需要根据UI页面或者API文档去进行Mock,以便于我们查看大致的UI效果。一般会有以下几种方式。
1,通过手动创建将数据传递到UI层中实现临时的数据mock。
val nameItems = {"张三","王五","李四"}
val adapter = Adapter(…,nameItems)
RecycleView.setAdapter(adapter)
优点:最简单,省事儿。
缺点:写法不规范,交互链路不完整,有一个地方就得复制粘贴一次,修改成本高。
2,NodeJS搭建mock服务器。
优点:数据编写灵活,可以实时根据不同的路径配置不同的返回数据json文件,客户端可以根据接口示例文档完善网络交互逻辑,联调时切换服务器地址即可。
缺点:每个请求地址都需要配置,且数据结构无法复用,修改数据操作成本高。
3,使用Apifox等三方工具。
优点:数据编写灵活,可自定义mock规则,自定义各类相应状态,数据结构可以复用、导出等。
缺点:更加适合服务端,对于客户端来说编写每个接口及其数据结构略显麻烦。
其实可以看到第三种已经是比较符合我们想要的一种方式了,只需要按照正常网络的请求流程去请求该mock服务器对应的地址就能根据你自行创建的接口数据进行返回,但是它最大的问题主要还是不够灵活,像数据实体其实我们本身在使用网络之前就会在编码中定义好了,然后又需要在那边再定义一次,如果接口比较多的话,这也是个体力活,又要定义数据类,又要创建接口,还要建立关联等,如果后续数据结构发生了变化,还需要去修改数据结构。那有没有一种方式可以规范网络请求的流程,有比较省事儿的呢,其实是有的。
这里分为2点来说:
1,规范网络请求流程。
为了能够不影响执行我们的网络请求,在实际的场景中可以使用本地服务器方式来响应网络请求。大致交互如下:
前面我们说了,这个阶段后端远程服务器的内容还没有准备好,所以我们需要将请求指向我们自行创建的Mock服务器,从而使流程规范化,而创建本地服务器的方法有很多,像AndroidAsync,AndServer、httpd等,我们只需要使用我们自己定义的拦截器将请求重新定向到本地服务器,再将其响应数据返回到请求链中。到此,交互是解决了,但是还缺少最重要的东西,也就是我们的响应数据,接着看下面第2点。
2,省事儿。
以我们常用的Retrofit为例,大多数情况下,我们都会在一个类似ApiService的类中去定义我们的接口声明,比如下面这个:
public interface ApiService {
@GET("/user/info")
Call<HttpResult<UserInfo>> getUserInfo(@Body Request req);
@GET("/user/info")
Observable<HttpResult<UserInfo>> getUserInfo(@Body Request request);
}
这里例举了使用RxJava3/2CallAdapterFactory和不使用的情况,在大多数情况下,其实我们希望得到的都是返回值中泛型参数一HttpResult<UserInfo>,HttpResult是我们定义的常用的统一返回数据类,它的常用结构如下:
data class HttpResult<T>(
val data: T?,
val code: Int,
val msg: String
)
而UserInfo则是用户的一些信息,头像、昵称什么的,也就是说,我们完全可以自行创建一个这样的对象,然后再将这个对象传递给我们第1步中的本地服务器,由它去管理这个数据,当路径为/user/info的请求过来时,直接将HttpResult<UserInfo>对应的数据返回回去就好了。但是如果只是这种程度的话,和我们之前说的通过ApiFox等工具一样的麻烦,所以我们需要使用我们最大的优势,那就是我们拥有和源码的关联性,可以利用Java中的反射来帮我们做很多事情。当然,这里也需要考虑几个问题。
如何将Path与Bean类数据相关联?
如何生成想要的数据?
如何对数据进行控制?
那么接下来就针对这几个问题去做处理,
如何将Path与Bean类数据相关联?
如果Retrofit这种形式的网络请求,可以通过获取到这个接口的Class对象,ApiService.class,通过它可以获取所有的成员函数Method,然后通过Method就可以获取到上面的注解以及注解中的值,比如上例中的@GET("/user/info") ,常用的请求方式基本上就4种,PUT/GET/DELETE/POST,所以通过判断其类型就可以做区分,然后拿到注解的中的value,也就是/user/info,这就得到了Path。然后,通过Method还能拿到它的返回值,Call<HttpResult<UserInfo>>,而最终拿到HttpResult<UserInfo>,此时Path就和Bean类数据相关联了,/user/info->HttpResult<UserInfo>,我们可以使用Map将其进行存储。
如果是非Retrofit这种形式,我们只能借助自行传递的Json文件或者类似与List<MockData>在MockData定义好它的请求方式、path、以及具体的json数据,然后传递到上文中提到的Mock服务器那边,最终也可以通过Map将其进行存储。
如何生成想要的数据?
上一个问题中解决了它的关联问题,接下来则是数据生成的问题,类似于HttpResult<UserInfo>这样的一个Class我们拿到了,那么要如何去生成我们想要的Json数据呢?我们都知道Gson.toJson(),FastJson.toJSONString都能做到将对象转换为Json数据,那么问题就只剩下了对象的创建。
对象的创建常见以下方式:
构造器:一个ApiService中定义的Bean类会有很多,不可能每一个对象都通过构造器去做创建,直接就放弃了。
反射:通过Class得到构造器对象,通过构造器调用newInstance(...)得到对象。但是在实际开发中,尤其是现在很多Android开发基本上都使用Kotlin,也就会用到data class这种带参的构造器声明方式,空参构造器被替代,也就不能直接创建了,因为即便是反射,它的参数对于一个通用的反射创建对象的方式来说是不确定的,所以无法使用这种方式。
Gson中的UnsafeAllocator:既然走反射也走不通,那么为什么Gson可以反序列化data class(带参构造器)呢?其实看源码就知道了,UnsafeAllocator中,有个newInstance函数,通过它就可以直接创建出一个对象,而其原理则是使用了sun.misc.Unsafe中的allocateInstance函数,对此函数的反射调用即可以不通过构造器创建出对象实例,具体原理不再细说。
通过上面的方法得到了一个对象,然后将这个对象传递给Gson等序列化的框架就可以转换为Json数据,然后传递给服务器使用了。至此,一个大致的交互流程就出来了,
在请求网络之前,创建本地Mock服务器,然后解析ApiService对象得到path->bean关联关系的数据并将数据提供给服务器。
在请求时,通过拦截器中的Path重定向到本地Mock服务器,本地Mock服务器匹配Path对应的数据内容并将其返回,最后拦截器将返回数据继续在链上(OkHttp的拦截器链)返回完成请求。
流程是有了,但是还缺少一些细节,对象是有了,对象中字段的数据怎么来的呢?所以也就有了下面这个问题。
如何对数据进行控制?
一般来说可以通过干涉对象的创建过程来做到,比如前面提到的HttpResult<UserInfo>对象,HttpResult中code和msg这种都是基本类型比较简单,大部分的Bean类基本上都是由基本类型构成的,我们在解析过程中通过判断它的类型返回对应的值,如果需要随机则随机生成,如果需要固定值或者说需要在范围内选取等,都可以通过注解去标记该字段,然后再在解析过程中通过不同的注解类型得到注解的值然后将其赋值给该字段,而data这类对象类型的,无非就是深层次的遍历,这样下来,就能得到一个比较完整的数据源,能够满足日常所需了。
可以看到为了省事儿还是很不省事儿的,但是这个流程一旦建立后基本上也没有太多需要额外的工作了,接下来的事情则是在后端完成开发以后,涉及到的地方做一下Bean类的部分修改,然后将其服务器地址切换到后端服务器上就好了。以下为我自己的一个实现,如果说你还有更好的方式和想法,欢迎一起来讨论。