@SmartDengg
2016-03-29T12:35:13.000000Z
字数 9562
阅读 1238
我最近一直在思考这样的一个问题:如何在保证质量的前提下,写出简短并且易读易维护的程序。我想每一个优秀的工程师都是乐于偷懒的。不想做重复冗余的工作,而且能编写出高质量的嗲吗。
随着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.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 40
User-Agent: LINK/2.2.1;Meizu m2 note; Android 5.1;appid=868013027927205
Lianjia-App-Id: lianjia-im
Lianjia-access-token: VTZUYw4/VGIGOgpsDz0PbFU1BG1WYgE8BT1WNgs5ATA=
Host: 172.30.2.10:7010
Connection: Keep-Alive
Accept-Encoding: gzip
--> END POST
<-- 200 OK http://172.30.2.10:7010/folio/im/msg/feedback.json (43ms)
Server: Apache-Coyote/1.1
Pragma: no-cache
Cache-Control: no-cache, no-store, max-age=0
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Content-Type: application/json;charset=UTF-8
Content-Language: en-US
Transfer-Encoding: chunked
Date: Tue, 29 Mar 2016 12:23:14 GMT
OkHttp-Sent-Millis: 1459254194669
OkHttp-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 :)