[关闭]
@SmartDengg 2016-03-29T12:35:13.000000Z 字数 9562 阅读 1238

Make architecture more stronger

我最近一直在思考这样的一个问题:如何在保证质量的前提下,写出简短并且易读易维护的程序。我想每一个优秀的工程师都是乐于偷懒的。不想做重复冗余的工作,而且能编写出高质量的嗲吗。

随着OKhttp、Retrofit、RxJava等优秀类库的引入,我们的网络层放生了天翻地覆的变化,不仅变得更加健壮,而且使用更加方便,并且OkHttp已得到了官方的认可,4.4以上的底层调用已经采用了Okhttp。好吧,其实我这一次并不是来讲解这些类库的,我想简短的总结一下,我们遇到了哪些问题,甚至将会遇到哪些问题,以及如何用最低的成本搞定这些问题。

标准Retrofit调用

目前,我们是这样使用它的,首先定义一个service接口,然后在接口中使用注解标注请求参数的key,我在此冒昧借用了沛荣老师代码片段中的一小段,AddHouseSourceApi接口,代码如下:

  1. public interface AddHouseSourceApi{
  2. @GET(UriUtil.URI_HOUSE_HOLDERS_SEARCH)
  3. LinkCall<RecommendHolderResponse> getHouseRecommendHolder(
  4. @Query("houseId") String houseId, @Query("delType") int delType,
  5. @Query("keyWord") String keyWord);
  6. }

然后在Activity或者Fragment中调用,我假设你已经熟悉了如何使用Retrofit:

  1. AddHouseSourceApi service = ServiceGenerator
  2. .createService(AddHouseSourceApi.class);
  3. LinkCall<RecommendHolderResponse> call = service.getHouseRecommendHolder(mHouseId, mDelType, mKeyword);
  4. call.enqueue(new LinkCallbackAdapter<RecommendHolderResponse>() {
  5. @Override public void onResponse(RecommendHolderResponse entity, Response<?> response, Throwable throwable) {
  6. /*处理网络接口回调结果*/
  7. }
  8. });

调用非常简单,但是我相信,聪明的你,一定看出了一些问题。首先我们的这种类似于AddHouseSourceApi的接口会很多,而且也不能保证一个AddHouseSourceApi中只有一个函数,我们更不能保证,一个接口的请求参数只有少数的几个,当请求参数增长的时候,我们要写好多个@Query()或者@Field(),我们要把所有的硬编码都写在一个接口的函数中。首先,这看起来不是很美,其次,你要时刻保证清醒,千万不能写错一个参数值得的key,当然,人不是机器,难免会犯错,因此我抽空完善了Log打印效果,能够显而易见的看到,“请求行”,“请求头”,“请求体”,“状态行”,“响应头”和json格式的“响应体”。但是,由于在实际生产中,这些Service接口足够分散,即便他们在同一个package下,而且,有时候我们不但要维护Response还要维护请求参,倘若遇到一次接口变更或者迁移,首先要定位到正确的Service然后去修改恰当的@Query或者@Field(),比如添加一个参数andOne,那么AddHouseSourceApi将会变成这样:

  1. @GET(UriUtil.URI_HOUSE_HOLDERS_SEARCH)
  2. LinkCall<RecommendHolderResponse> getHouseRecommendHolder(
  3. @Query("houseId") String houseId, @Query("delType") int delType,
  4. @Query("keyWord") String keyWord, @Query("andOne") String andOne);

其次我们还找到调用ServiceActivity或者Fragment,添加一个请求参:

  1. LinkCall<RecommendHolderResponse> call = service.getHouseRecommendHolder(mHouseId, mDelType, mKeyword, andOne);

这不仅破坏了应有的结构,而且也没有体现出封装性,再者说这本应属于Request的逻辑变化,为什么要修改Service

Retrofit的另一种调用方式

当然,我们能够想到的问题,Square公司早已料到,因此他们为我们提供了另外两个注解@QueryMap@FieldMap,以供使用,我们可以直接丢进去一个Map<String,String>,这里面持有我们的请求参数:

  1. @GET(UriUtil.URI_HOUSE_HOLDERS_SEARCH)
  2. LinkCall<RecommendHolderResponse> getHouseRecommendHolder(@QueryMap Map params);

然后,调用方式也有会所变化:

  1. Map<String,String> params = new HashMap<>();
  2. params.put("houseId",mHouseId);
  3. params.put("delType",mDelType);
  4. params.put("keyWord",mKeyword);
  5. LinkCall<RecommendHolderResponse> call = service.getHouseRecommendHolder(params);
  6. call.enqueue(new LinkCallbackAdapter<RecommendHolderResponse>() {
  7. @Override public void onResponse(RecommendHolderResponse entity, Response<?> response, Throwable throwable) {
  8. /*处理网络接口回调结果*/
  9. }
  10. });

这看起来,当需要添加或者修改请求参的时候,我们不再需要破坏原有的Service结构了。但是这样的书写,还是不够美观,因为它存在于Activity中,这会让它变得臃肿。而且一个Response居然对应了一个Map,这算什么?从编码规范来讲Response至少应该对应一个Request才对,这样写简直颠覆了我的编程思想。我想最难的应该就是CodeReview了吧,因为封装性的缺失,这无疑又增加了维护成本,当然很多人认为可以通过添加注释来缓解状况,但是回过头来仔细想想,这些注释出现在Activity或者Fragment中真的合适吗。

没有万能的银弹

Ultrafit!这就是为什么我要创造它的原因,遇到问题,解决问题,才能打造一款高质量的应用和健壮的程序,当然,它也不够完美,因为他是基于注解的,这就意味着在运行时需要使用反射去读取注解,这在Android上可是一大禁忌,因为反射意味着在性能损耗,尤其是Android这种寸土寸金的地方。但又考虑到我们的编译时间已经够长了,所以我还是放弃了,在编译时通过读取注解从而生成辅助类的方案。

让我们先来看看如何使用它,商机目前新增网络网络请求都是这样的,从目前的情况来看,它简直表现良好。而Link这边只有系统消息反馈在用。

首先编写一个请求实体类FeedBackRequest:

  1. @HttpPost(stringUrl = UriUtil.FeedbackUri) public class FeedBackRequest {
  2. /**
  3. * fbType 反馈类型: 2 已阅读 3 已转发
  4. * fbTime 时间戳:fbType=2时可指定时间批量阅读消息(此时oaId不能为空)毫秒为单位
  5. * oaId 系统号业务Id:501Link小信鸽 502链家研究院 503品牌活动 504房产指南
  6. * messageUrl 卡片链接: 阅读或者分享卡片时必须
  7. */
  8. public static final int TYPE_READ = 2;
  9. public static final int TYPE_TRANSPOND = 3;
  10. @Retention(RetentionPolicy.SOURCE) @Target(value = { ElementType.FIELD, ElementType.PARAMETER })
  11. @IntDef(value = { TYPE_READ, TYPE_TRANSPOND }) private @interface FeedType {}
  12. @Argument(parameter = "fbType") private Integer feedType;
  13. @Argument(parameter = "fbTime") private String feedTime;
  14. @Argument(parameter = "oaId") private Integer id;
  15. @Argument(parameter = "messageUrl") private String messageUrl;
  16. public FeedBackRequest(@FeedType @NonNull int feedType, String feedTime, Integer id, String messageUrl) {
  17. this.feedType = feedType;
  18. this.feedTime = feedTime;
  19. this.id = id;
  20. this.messageUrl = messageUrl;
  21. }
  22. @Override public String toString() {
  23. return "FeedBackRequest{" +
  24. "feedType='" + feedType + '\'' +
  25. ", feedTime='" + feedTime + '\'' +
  26. ", id='" + id + '\'' +
  27. ", messageUrl='" + messageUrl + '\'' +
  28. '}';
  29. }
  30. }

注解@HttpPost用来标注请求地址,说明是Post请求。当然也可以使用@HttpGet来标注一个Get请求。@Argument用来标注请求参数的key,它将会被解析,并添加到网络请求中,而最终的value,便是对应字段的值,如feedType最终所被赋的值,便是请求参数key所对应的value。如果这个字段为null,那么它将不会被解析。

那么再来看一看FeedbackService接口:

  1. public class FeedbackService {
  2. @FormUrlEncoded @POST LinkCall<BaseResultInfo> requestFeedBack(@Url String url,
  3. @FieldMap Map<String, String> params);
  4. }

假设你已经知道了在retrofit中,post请求需要使用@FieldMap注解来进行标注。

最后来看看,我是如何使用哒:

  1. FeedbackService service = ServiceGenerator.createService(FeedbackService.class)
  2. FeedBackRequest feedBackRequest =
  3. new FeedBackRequest(FeedBackRequest.TYPE_TRANSPOND, String.valueOf(System.currentTimeMillis()),
  4. eyeEntity.getFromUserId().bizType, mUrl);
  5. RequestEntity requestEntity = UltraParserFactory.createParser(feedBackRequest).parseRequestEntity();
  6. String url = requestEntity.getUrl();
  7. Map<String, String> paramMap = requestEntity.getParamMap();
  8. LinkCall<BaseResultInfo> call = service.requestFeedBack(url,params);
  9. call.enqueue(new LinkCallbackAdapter<BaseResultInfo>(){
  10. @Override public void onResponse(BaseResultInfo entity, Response<?> response, Throwable throwable) {
  11. /*处理网络接口回调结果*/
  12. }
  13. });

UltraParserFactory这个只有不到300行的万能类负责解析FeedBackRequest中所有被标记的注解,最终会返回一个RequestEntity,我们可以从中拿到Url和参数Map集合。接下来的事情就和Retrofit的一般用法一样了。至此,所有的Request都被封装到一个javabean中了,并且每一个Response对应一个Request的,不仅封装性能够得到体现,而且也降低了维护成本,并且请求实体类的出现让逻辑变得更加直观,也更透明。另外值得一提的是,如果多个存在参数服用的场景,比如分页加载等,可以抽取出来一个BaseRequest,让所有用到的子类去继承它,这样也可以少些很多参数,因为UltraParserFactory的解析支持父类继承,比如:

  1. public class BaseRequest {
  2. @Argument(parameter = "key") private String appKey = Constants.APP_KEY;
  3. @Argument(parameter = "dtype") private String dtype = "json";
  4. }
  5. @HttpGet(stringUrl = Constants.MOVIE_TODAY_URL) public class MovieIdRequest extends BaseRequest {
  6. @Argument(parameter = "cityid") private int cityId;
  7. public MovieIdRequest(int cityId) {
  8. this.cityId = cityId;
  9. }
  10. }

keydtype也能够被UltraParserFactory解析到。为此我们可以避免写重复的请求参数。

最后一点,但不是最后

又回到了性能调优的问题上来,为了让反射带来的性能损耗降低最低,我取消Java的语言访问检查,也就是说,你可以使用private来修饰这些参数字段,而且,我认为这些请求实体类不应该,也不能够暴露细节,他应该是细节隐藏的,是不透明的。最后,我们退一步来讲联网请求本身属于耗时操作,反射的解析时间远不及远端接口的请求更耗时,所以,这在用户体验上并没有明显反应。

集成RxJava

如果你熟悉RxJava,那么非常庆幸,我们又能将性能损耗降低一大截,我们甚至可以把注解的解析丢到工作线程中,这样它就影响不到UI线程了,我们View的刷新又会流畅很多,离掉帧又远了一步,这简直不能更棒了。

  1. Observable.fromCallable(new Func0<RequestEntity>() {
  2. @Override public RequestEntity call() {
  3. FeedBackRequest feedBackRequest =
  4. new FeedBackRequest(FeedBackRequest.TYPE_TRANSPOND, String.valueOf(System.currentTimeMillis()),eyeEntity.getFromUserId().bizType, mUrl);
  5. RequestEntity requestEntity =
  6. UltraParserFactory.createParser(feedBackRequest).parseRequestEntity();
  7. UltraParserFactory.outputs(requestEntity);
  8. return requestEntity;
  9. }
  10. }).concatMap(new Func1<RequestEntity, Observable<BaseResultInfo>>() {
  11. @Override public Observable<BaseResultInfo> call(RequestEntity requestEntity) {
  12. return service.requestFeedBack(requestEntity.getUrl(), requestEntity.getParamMap());
  13. }
  14. }).subscribeOn(Schedulers.newThread())
  15. .observeOn(AndroidSchedulers.mainThread())
  16. .subscribe(new Subscriber<BaseResultInfo>() {
  17. @Override public void onCompleted(
  18. @Override public void onError(Throwable e
  19. @Override public void onNext(BaseResultInfo baseResultInfo
  20. /*处理网络接口回调结果*/
  21. }
  22. });

UltraParserFactory.outputs(requestEntity);只是一句打印而已:

  1. public static void outputs(@NonNull RequestEntity requestEntity) {
  2. Logger.d("Request entity !!!!" +
  3. "\n ⇢ " +
  4. " Type : " +
  5. requestEntity.getRestType().name() +
  6. "\n ⇢ " +
  7. " Url : " +
  8. requestEntity.getUrl() +
  9. "\n ⇢ " +
  10. " Params : " +
  11. requestEntity.getParamMap());
  12. }

Ok至此,让我们感受一下完整的Log输出吧:

  1. ╔════════════════════════════════════════════════════════════════════════════════════════
  2. Thread: main
  3. ╟────────────────────────────────────────────────────────────────────────────────────────
  4. OperatorMap$1.onNext (OperatorMap.java:54)
  5. UseCase$2.call (UseCase.java:32)
  6. UseCase$2.call (UseCase.java:36)
  7. UltraParserFactory.outputs (UltraParserFactory.java:24)
  8. ╟────────────────────────────────────────────────────────────────────────────────────────
  9. Request entity !!!!
  10. Type : POST
  11. Url : im/msg/feedback.json
  12. Params : {oaId=10004, fbType=2, fbTime=1459254194640}
  13. ╚════════════════════════════════════════════════════════════════════════════════════════
  14. --> POST http://172.30.2.10:7010/folio/im/msg/feedback.json HTTP/1.1
  15. Content-Type: application/x-www-form-urlencoded
  16. Content-Length: 40
  17. User-Agent: LINK/2.2.1;Meizu m2 note; Android 5.1;appid=868013027927205
  18. Lianjia-App-Id: lianjia-im
  19. Lianjia-access-token: VTZUYw4/VGIGOgpsDz0PbFU1BG1WYgE8BT1WNgs5ATA=
  20. Host: 172.30.2.10:7010
  21. Connection: Keep-Alive
  22. Accept-Encoding: gzip
  23. --> END POST
  24. <-- 200 OK http://172.30.2.10:7010/folio/im/msg/feedback.json (43ms)
  25. Server: Apache-Coyote/1.1
  26. Pragma: no-cache
  27. Cache-Control: no-cache, no-store, max-age=0
  28. Expires: Thu, 01 Jan 1970 00:00:00 GMT
  29. Content-Type: application/json;charset=UTF-8
  30. Content-Language: en-US
  31. Transfer-Encoding: chunked
  32. Date: Tue, 29 Mar 2016 12:23:14 GMT
  33. OkHttp-Sent-Millis: 1459254194669
  34. OkHttp-Received-Millis: 1459254194706
  35. <-- END HTTP
  36. ╔════════════════════════════════════════════════════════════════════════════════════════
  37. Thread: ParallelThread#12
  38. ╟────────────────────────────────────────────────────────────────────────────────────────
  39. OkHttpCall.execute (OkHttpCall.java:177)
  40. OkHttpCall.parseResponse (OkHttpCall.java:213)
  41. GsonResponseBodyConverter.convert (GsonResponseBodyConverter.java:25)
  42. GsonResponseBodyConverter.convert (GsonResponseBodyConverter.java:37)
  43. ╟────────────────────────────────────────────────────────────────────────────────────────
  44. {
  45. "errorno": 0,
  46. "cost": 33
  47. }
  48. ╚════════════════════════════════════════════════════════════════════════════════════════

未完待续

就如我之前所说那样UltraParser并不完美,所以我没有把它做成SDK,更没有上传到Maven仓库,但考虑到它所涉及的代码并不多,所以我把它作为普通类添加到代码库中,目的就是可以随时完善,或者添加新特性,因为目前它只支持标准的GetPost请求。它虽然在商机中表现良好,但是不见得在其他项目中能够表现良好,所以,如果你有任何建议或者意见,请与我交流沟通,Thanks :)

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注