@SmartDengg
2016-03-29T12:35:13.000000Z
字数 9562
阅读 1331
我最近一直在思考这样的一个问题:如何在保证质量的前提下,写出简短并且易读易维护的程序。我想每一个优秀的工程师都是乐于偷懒的。不想做重复冗余的工作,而且能编写出高质量的嗲吗。
随着OKhttp、Retrofit、RxJava等优秀类库的引入,我们的网络层放生了天翻地覆的变化,不仅变得更加健壮,而且使用更加方便,并且OkHttp已得到了官方的认可,4.4以上的底层调用已经采用了Okhttp。好吧,其实我这一次并不是来讲解这些类库的,我想简短的总结一下,我们遇到了哪些问题,甚至将会遇到哪些问题,以及如何用最低的成本搞定这些问题。
目前,我们是这样使用它的,首先定义一个service接口,然后在接口中使用注解标注请求参数的key,我在此冒昧借用了沛荣老师代码片段中的一小段,AddHouseSourceApi接口,代码如下:
public interface AddHouseSourceApi{@GET(UriUtil.URI_HOUSE_HOLDERS_SEARCH)LinkCall<RecommendHolderResponse> getHouseRecommendHolder(@Query("houseId") String houseId, @Query("delType") int delType,@Query("keyWord") String keyWord);}
然后在Activity或者Fragment中调用,我假设你已经熟悉了如何使用Retrofit:
AddHouseSourceApi service = ServiceGenerator.createService(AddHouseSourceApi.class);LinkCall<RecommendHolderResponse> call = service.getHouseRecommendHolder(mHouseId, mDelType, mKeyword);call.enqueue(new LinkCallbackAdapter<RecommendHolderResponse>() {@Override public void onResponse(RecommendHolderResponse entity, Response<?> response, Throwable throwable) {/*处理网络接口回调结果*/}});
调用非常简单,但是我相信,聪明的你,一定看出了一些问题。首先我们的这种类似于AddHouseSourceApi的接口会很多,而且也不能保证一个AddHouseSourceApi中只有一个函数,我们更不能保证,一个接口的请求参数只有少数的几个,当请求参数增长的时候,我们要写好多个@Query()或者@Field(),我们要把所有的硬编码都写在一个接口的函数中。首先,这看起来不是很美,其次,你要时刻保证清醒,千万不能写错一个参数值得的key,当然,人不是机器,难免会犯错,因此我抽空完善了Log打印效果,能够显而易见的看到,“请求行”,“请求头”,“请求体”,“状态行”,“响应头”和json格式的“响应体”。但是,由于在实际生产中,这些Service接口足够分散,即便他们在同一个package下,而且,有时候我们不但要维护Response还要维护请求参,倘若遇到一次接口变更或者迁移,首先要定位到正确的Service然后去修改恰当的@Query或者@Field(),比如添加一个参数andOne,那么AddHouseSourceApi将会变成这样:
@GET(UriUtil.URI_HOUSE_HOLDERS_SEARCH)LinkCall<RecommendHolderResponse> getHouseRecommendHolder(@Query("houseId") String houseId, @Query("delType") int delType,@Query("keyWord") String keyWord, @Query("andOne") String andOne);
其次我们还找到调用Service的Activity或者Fragment,添加一个请求参:
LinkCall<RecommendHolderResponse> call = service.getHouseRecommendHolder(mHouseId, mDelType, mKeyword, andOne);
这不仅破坏了应有的结构,而且也没有体现出封装性,再者说这本应属于Request的逻辑变化,为什么要修改Service?
当然,我们能够想到的问题,Square公司早已料到,因此他们为我们提供了另外两个注解@QueryMap和@FieldMap,以供使用,我们可以直接丢进去一个Map<String,String>,这里面持有我们的请求参数:
@GET(UriUtil.URI_HOUSE_HOLDERS_SEARCH)LinkCall<RecommendHolderResponse> getHouseRecommendHolder(@QueryMap Map params);
然后,调用方式也有会所变化:
Map<String,String> params = new HashMap<>();params.put("houseId",mHouseId);params.put("delType",mDelType);params.put("keyWord",mKeyword);LinkCall<RecommendHolderResponse> call = service.getHouseRecommendHolder(params);call.enqueue(new LinkCallbackAdapter<RecommendHolderResponse>() {@Override public void onResponse(RecommendHolderResponse entity, Response<?> response, Throwable throwable) {/*处理网络接口回调结果*/}});
这看起来,当需要添加或者修改请求参的时候,我们不再需要破坏原有的Service结构了。但是这样的书写,还是不够美观,因为它存在于Activity中,这会让它变得臃肿。而且一个Response居然对应了一个Map,这算什么?从编码规范来讲Response至少应该对应一个Request才对,这样写简直颠覆了我的编程思想。我想最难的应该就是CodeReview了吧,因为封装性的缺失,这无疑又增加了维护成本,当然很多人认为可以通过添加注释来缓解状况,但是回过头来仔细想想,这些注释出现在Activity或者Fragment中真的合适吗。
Ultrafit!这就是为什么我要创造它的原因,遇到问题,解决问题,才能打造一款高质量的应用和健壮的程序,当然,它也不够完美,因为他是基于注解的,这就意味着在运行时需要使用反射去读取注解,这在Android上可是一大禁忌,因为反射意味着在性能损耗,尤其是Android这种寸土寸金的地方。但又考虑到我们的编译时间已经够长了,所以我还是放弃了,在编译时通过读取注解从而生成辅助类的方案。
让我们先来看看如何使用它,商机目前新增网络网络请求都是这样的,从目前的情况来看,它简直表现良好。而Link这边只有系统消息反馈在用。
首先编写一个请求实体类FeedBackRequest:
@HttpPost(stringUrl = UriUtil.FeedbackUri) public class FeedBackRequest {/*** fbType 反馈类型: 2 已阅读 3 已转发* fbTime 时间戳:fbType=2时可指定时间批量阅读消息(此时oaId不能为空)毫秒为单位* oaId 系统号业务Id:501Link小信鸽 502链家研究院 503品牌活动 504房产指南* messageUrl 卡片链接: 阅读或者分享卡片时必须*/public static final int TYPE_READ = 2;public static final int TYPE_TRANSPOND = 3;@Retention(RetentionPolicy.SOURCE) @Target(value = { ElementType.FIELD, ElementType.PARAMETER })@IntDef(value = { TYPE_READ, TYPE_TRANSPOND }) private @interface FeedType {}@Argument(parameter = "fbType") private Integer feedType;@Argument(parameter = "fbTime") private String feedTime;@Argument(parameter = "oaId") private Integer id;@Argument(parameter = "messageUrl") private String messageUrl;public FeedBackRequest(@FeedType @NonNull int feedType, String feedTime, Integer id, String messageUrl) {this.feedType = feedType;this.feedTime = feedTime;this.id = id;this.messageUrl = messageUrl;}@Override public String toString() {return "FeedBackRequest{" +"feedType='" + feedType + '\'' +", feedTime='" + feedTime + '\'' +", id='" + id + '\'' +", messageUrl='" + messageUrl + '\'' +'}';}}
注解@HttpPost用来标注请求地址,说明是Post请求。当然也可以使用@HttpGet来标注一个Get请求。@Argument用来标注请求参数的key,它将会被解析,并添加到网络请求中,而最终的value,便是对应字段的值,如feedType最终所被赋的值,便是请求参数key所对应的value。如果这个字段为null,那么它将不会被解析。
那么再来看一看FeedbackService接口:
public class FeedbackService {@FormUrlEncoded @POST LinkCall<BaseResultInfo> requestFeedBack(@Url String url,@FieldMap Map<String, String> params);}
假设你已经知道了在retrofit中,post请求需要使用@FieldMap注解来进行标注。
最后来看看,我是如何使用哒:
FeedbackService service = ServiceGenerator.createService(FeedbackService.class)FeedBackRequest feedBackRequest =new FeedBackRequest(FeedBackRequest.TYPE_TRANSPOND, String.valueOf(System.currentTimeMillis()),eyeEntity.getFromUserId().bizType, mUrl);RequestEntity requestEntity = UltraParserFactory.createParser(feedBackRequest).parseRequestEntity();String url = requestEntity.getUrl();Map<String, String> paramMap = requestEntity.getParamMap();LinkCall<BaseResultInfo> call = service.requestFeedBack(url,params);call.enqueue(new LinkCallbackAdapter<BaseResultInfo>(){@Override public void onResponse(BaseResultInfo entity, Response<?> response, Throwable throwable) {/*处理网络接口回调结果*/}});
UltraParserFactory这个只有不到300行的万能类负责解析FeedBackRequest中所有被标记的注解,最终会返回一个RequestEntity,我们可以从中拿到Url和参数Map集合。接下来的事情就和Retrofit的一般用法一样了。至此,所有的Request都被封装到一个javabean中了,并且每一个Response对应一个Request的,不仅封装性能够得到体现,而且也降低了维护成本,并且请求实体类的出现让逻辑变得更加直观,也更透明。另外值得一提的是,如果多个存在参数服用的场景,比如分页加载等,可以抽取出来一个BaseRequest,让所有用到的子类去继承它,这样也可以少些很多参数,因为UltraParserFactory的解析支持父类继承,比如:
public class BaseRequest {@Argument(parameter = "key") private String appKey = Constants.APP_KEY;@Argument(parameter = "dtype") private String dtype = "json";}@HttpGet(stringUrl = Constants.MOVIE_TODAY_URL) public class MovieIdRequest extends BaseRequest {@Argument(parameter = "cityid") private int cityId;public MovieIdRequest(int cityId) {this.cityId = cityId;}}
key与dtype也能够被UltraParserFactory解析到。为此我们可以避免写重复的请求参数。
又回到了性能调优的问题上来,为了让反射带来的性能损耗降低最低,我取消Java的语言访问检查,也就是说,你可以使用private来修饰这些参数字段,而且,我认为这些请求实体类不应该,也不能够暴露细节,他应该是细节隐藏的,是不透明的。最后,我们退一步来讲联网请求本身属于耗时操作,反射的解析时间远不及远端接口的请求更耗时,所以,这在用户体验上并没有明显反应。
如果你熟悉RxJava,那么非常庆幸,我们又能将性能损耗降低一大截,我们甚至可以把注解的解析丢到工作线程中,这样它就影响不到UI线程了,我们View的刷新又会流畅很多,离掉帧又远了一步,这简直不能更棒了。
Observable.fromCallable(new Func0<RequestEntity>() {@Override public RequestEntity call() {FeedBackRequest feedBackRequest =new FeedBackRequest(FeedBackRequest.TYPE_TRANSPOND, String.valueOf(System.currentTimeMillis()),eyeEntity.getFromUserId().bizType, mUrl);RequestEntity requestEntity =UltraParserFactory.createParser(feedBackRequest).parseRequestEntity();UltraParserFactory.outputs(requestEntity);return requestEntity;}}).concatMap(new Func1<RequestEntity, Observable<BaseResultInfo>>() {@Override public Observable<BaseResultInfo> call(RequestEntity requestEntity) {return service.requestFeedBack(requestEntity.getUrl(), requestEntity.getParamMap());}}).subscribeOn(Schedulers.newThread()).observeOn(AndroidSchedulers.mainThread()).subscribe(new Subscriber<BaseResultInfo>() {@Override public void onCompleted(@Override public void onError(Throwable e@Override public void onNext(BaseResultInfo baseResultInfo/*处理网络接口回调结果*/}});
UltraParserFactory.outputs(requestEntity);只是一句打印而已:
public static void outputs(@NonNull RequestEntity requestEntity) {Logger.d("Request entity !!!!" +"\n ⇢ " +" Type : " +requestEntity.getRestType().name() +"\n ⇢ " +" Url : " +requestEntity.getUrl() +"\n ⇢ " +" Params : " +requestEntity.getParamMap());}
Ok至此,让我们感受一下完整的Log输出吧:
╔════════════════════════════════════════════════════════════════════════════════════════║ Thread: main╟────────────────────────────────────────────────────────────────────────────────────────║ OperatorMap$1.onNext (OperatorMap.java:54)║ UseCase$2.call (UseCase.java:32)║ UseCase$2.call (UseCase.java:36)║ UltraParserFactory.outputs (UltraParserFactory.java:24)╟────────────────────────────────────────────────────────────────────────────────────────║ Request entity !!!!║ ⇢ Type : POST║ ⇢ Url : im/msg/feedback.json║ ⇢ Params : {oaId=10004, fbType=2, fbTime=1459254194640}╚════════════════════════════════════════════════════════════════════════════════════════--> POST http://172.30.2.10:7010/folio/im/msg/feedback.json HTTP/1.1Content-Type: application/x-www-form-urlencodedContent-Length: 40User-Agent: LINK/2.2.1;Meizu m2 note; Android 5.1;appid=868013027927205Lianjia-App-Id: lianjia-imLianjia-access-token: VTZUYw4/VGIGOgpsDz0PbFU1BG1WYgE8BT1WNgs5ATA=Host: 172.30.2.10:7010Connection: Keep-AliveAccept-Encoding: gzip--> END POST<-- 200 OK http://172.30.2.10:7010/folio/im/msg/feedback.json (43ms)Server: Apache-Coyote/1.1Pragma: no-cacheCache-Control: no-cache, no-store, max-age=0Expires: Thu, 01 Jan 1970 00:00:00 GMTContent-Type: application/json;charset=UTF-8Content-Language: en-USTransfer-Encoding: chunkedDate: Tue, 29 Mar 2016 12:23:14 GMTOkHttp-Sent-Millis: 1459254194669OkHttp-Received-Millis: 1459254194706<-- END HTTP╔════════════════════════════════════════════════════════════════════════════════════════║ Thread: ParallelThread#12╟────────────────────────────────────────────────────────────────────────────────────────║ OkHttpCall.execute (OkHttpCall.java:177)║ OkHttpCall.parseResponse (OkHttpCall.java:213)║ GsonResponseBodyConverter.convert (GsonResponseBodyConverter.java:25)║ GsonResponseBodyConverter.convert (GsonResponseBodyConverter.java:37)╟────────────────────────────────────────────────────────────────────────────────────────║ {║ "errorno": 0,║ "cost": 33║ }╚════════════════════════════════════════════════════════════════════════════════════════
就如我之前所说那样UltraParser并不完美,所以我没有把它做成SDK,更没有上传到Maven仓库,但考虑到它所涉及的代码并不多,所以我把它作为普通类添加到代码库中,目的就是可以随时完善,或者添加新特性,因为目前它只支持标准的Get和Post请求。它虽然在商机中表现良好,但是不见得在其他项目中能够表现良好,所以,如果你有任何建议或者意见,请与我交流沟通,Thanks :)