先扯两句
做个项目是真占时间啊,不知不觉已经三周没有发新的博客了(这个接口不错吧),总算是有个周末,趁休息,将最近遇到的一些问题再次贴出来,算是完善一下工程。
闲言少叙,老规矩还是先上我的Git库,然后开始正文。
MyBaseApplication (https://github.com/BanShouWeng/MyBaseApplication)
正文
“实践是检验真理的唯一标准”,这句话的唯一两字是否正确我们先放下不表,不过至少在这段做项目的时间中,对于之前自己所写的内容还是有了全新的认识的,上一次发的内容,主要是总结了一下漏洞,希望看到的大家看到后能够避免走上相同的弯路,而今天这篇呢,看到标题大家应该也知道了,还是一些阶段总结的内容,不过这次下手的只是Retrofit,但想必误入这些弯路的绝不止我一个,那么就进入今天的内容吧。
Retrofit 上传JSON
具体还是从需求说起,当然,还是网络访问的部分,那就是我参与的项目需要给后台传输JSON数据,之前也查到了一些方法,那就是在Header中添加设置Content-Type为application/json,看了我前面的博客《一个Android工程的从零开始》阶段总结与修改1-base的应该会知道,其中最后一部分就是阐述的如何进行Retrofit header的动态添加封装,这里自然也就方便了许多,只需要在初始化中加下属这部分即可。
headerParams.put("Content-Type", "application/json");
这么简单的操作还不是分分钟结束战斗啊,于是试了第一个接口,get请求,果然没有问题。可当尝试POST请求的时候,却是给我返回来了服务器端的异常信息,我还愤愤的找服务器端算账,可当看到我的请求数据,真是恨不得找个地缝钻进去。
老子已经进行了设置,为毛上传的信息还是key-value形式的!!!
别问我为什么get可以,本来get就不管上传什么形式的,统统url,自然不可能出问题。
为了告诉后台我确实上传的是JSON形式的参数,还将传递的参数拦截了下来,给他们看:
拦截方法如下:
private void initBaseData(boolean isFromLogin) {
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.connectTimeout(5, TimeUnit.SECONDS);
builder.addInterceptor(new Interceptor() {
@Override
public okhttp3.Response intercept(Chain chain) throws IOException {
Request request = chain.request();
LogUtil.info("responseString", "request====" + action);
LogUtil.info("responseString", "request====" + request.headers().toString());
LogUtil.info("responseString", "request====" + request.toString());
okhttp3.Response proceed = chain.proceed(request);
LogUtil.info("zzz", "proceed====" + proceed.headers().toString());
return proceed;
}
});
Retrofit.Builder builder1 = new Retrofit.Builder()
.client(builder.build())
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create());
builder1.baseUrl(baseUrl);
retrofit = builder1.build();
}
从中也可以看出我们的Retrofit只是对于OkHttp的一种封装,具体到一些功能性的内容,还是得从OkHttp下手,这点我也很无奈啊。
不过需要说明的一点是,大家可以看到,这里我们只输出的Header对应的信息,却没有对Body下手,这个真不是我不想,而是真心无能为例,body下只有一个toString方法,输出的还是对应body的Id,所以大家如果想查看自己的传输的body的信息,暂时我能提供的建议,就只有通过断点调试了。
从proceed.headers().toString()对应的输出语句中确实可以看到如下日志:
proceed====Date: Sat, 26 Aug 2017 08:02:59 GMT
Content-Type: application/json; charset=utf-8
可是日志显示出来花来,服务器端Spring的JSON接受都没过就报空指针的事实还是摆在那里。
苦逼的查了一番资料后,才发现一种解决方法,竟然需要我自己把JSON串写好,通过RequestBody传才可以,那真是一万只草泥马在心头奔驰而过啊!
用于传输的Service自然也要做出对应调整:
public interface RetrofitPostJsonService {
@POST("{action}")
Observable<ResponseBody> postResult(@Path("action") String action, @HeaderMap Map<String, String> headerParams, @Body RequestBody requestBody);
}
需要说明的一点是,大家如果与我之前发的POST传输的Service对比一下就会发现,这里要比之前少了“@FormUrlEncoded”的配置,那是因为这个配置信息本就是将URL格式化为为key-value格式,既然我们设置了要传输JSON格式,再用这个参数,自然是要报错的:
Caused by: java.lang.IllegalArgumentException: @Body parameters cannot be used with form or multi-part encoding. (parameter #3)
这部分搞定了之后,该进行的自然就是如何封装到POST方法中了,对于这部分呢,我这里总共分为了一下两种情况:
- 普通key-Value形式的封装
- 复杂JSON的封装
普通key-Value形式的封装###
把这一条放在最前面不仅仅是因为它的名字中有普通两个字,显得比较好处理,而是因为它真的要比另一种情况要好处理得多。
public <T extends BaseBean> void post(final String action, final Class<T> clazz, boolean showDialog, final ResultCallBack callBack) {
if (showDialog) {
showLoadDialog();
}
initBaseData(false);
if (jsonService == null) {
jsonService = retrofit.create(RetrofitPostJsonService.class);
}
if (params == null) {
params = new HashMap<>();
}
RequestBody requestBody =
RequestBody.create(MediaType.parse("application/json; charset=utf-8"),
String.valueOf(new JSONObject(params)));
jsonService.postResult(action, headerParams, requestBody)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<ResponseBody>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
}
@Override
public void onNext(@NonNull ResponseBody responseBody) {
hideLoadDialog();
try {
String responseString = responseBody.string();
LogUtil.info("responseString", action + "********** responseString post " + responseString);
callBack.success(action, new Gson().fromJson(responseString, clazz));
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void onError(@NonNull Throwable e) {
callBack.error(action, e);
}
@Override
public void onComplete() {
params = null;
}
});
}
可以看得出来,这段接受的方法,实际上与之前封装的POST方法真的却别不大。
1、由于需要上传JSON,且又是普通的Key-Value转换而成的JSON,于是我们就可以实现最最简单的方法实现,那就是直接将原本的之前封装中用到的Map转换成JSON就好。
String.valueOf(new JSONObject(params))
最外一层的目的是将得到的JSON转换成String类型,用于上传。
2、前面说过了,Service已经被改变了,所以这里我们也需要将传输的参数做出对应修改,那就是创建一个RequestBody对象,在配合上第一条生成的JSON,就是如下的代码:
RequestBody requestBody =
RequestBody.create(MediaType.parse("application/json; charset=utf-8"),
String.valueOf(new JSONObject(params)));
3、就是参数的传递,这个也是最简单的了,那就是将我们的RequestBody 对象传递给Service中的postResult方法即可:
jsonService.postResult(action, headerParams, requestBody)
如此以来,我们终于可以成功的传递JSON了。
复杂JSON封装###
上述方法已经能够实现我们大多数接口的传输数据的需求,可是在大多数,它也是有我们无法完成的部分,哪怕是我下面列举的简单又常见的JSON形式:
[
{
"name": "",
"myClass": "",
"grade": ""
},
{
"name": "",
"myClass": "",
"grade": ""
},
{
"name": "",
"myClass": "",
"grade": ""
},
{
"name": "",
"myClass": "",
"grade": ""
}
]
传输的就是四个学生的姓名、年级、以及班级,如果用之前的方法尝试的话,我的尝试的结果如下:
当然,除了直接创建JSON对象,我们还可以创建一个GSON获取json串,我就贱贱的尝试了一下Gson解析:
List<BaseBean> beanList = new ArrayList<>();
for (int i = 0; i < 4; i++) {
BaseBean baseBean = new BaseBean();
baseBean.setName("name" + i);
baseBean.setMyClass("class" + i);
baseBean.setGrade("grade" + i);
beanList.add(baseBean);
}
Log.i("adada", new Gson().toJson(beanList).toString());
很荣幸的发现,竟然能够转换成功:
[
{
"grade": "grade0",
"myClass": "class0",
"name": "name0"
},
{
"grade": "grade1",
"myClass": "class1",
"name": "name1"
},
{
"grade": "grade2",
"myClass": "class2",
"name": "name2"
},
{
"grade": "grade3",
"myClass": "class3",
"name": "name3"
}
]
正兴奋呢,需求又变了一个样子:
{
"student": [
{
"name": "",
"class": "",
"grade": ""
},
{
"name": "",
"class": "",
"grade": ""
},
{
"name": "",
"class": "",
"grade": ""
},
{
"name": "",
"class": "",
"grade": ""
}
],
"pass": [
{
"name": ""
},
{
"name": ""
}
]
}
刚刚看到的胜利曙光瞬间又阴云密布了,无奈之下,就只能再创建一个传输的bean:
class PostStudentBean{
private List<BaseBean> student;
private List<PassBean> pass;
public void setStudent(List<BaseBean> student){
this.student = student;
}
public List<BaseBean> getStudent(){
return student;
}
public void setPass(List<PassBean> pass){
this.pass= pass;
}
public List<PassBean> getPass(){
return pass;
}
}
class PassBean{
private String name;
public void setName(String name){
this.name = name;
}
public String getName(){
return name;
}
}
如此以来,就可以通过GSON将Bean转换为JSON串,用于传输即可。
当然,上面用一种比较倒霉的方法说明了两种我们在编程开发过程中可能会遇到的两种情况(个人认为如果需要Map配合Bean的不如就直接创建一个父级别的Bean好了),这部分操作暂时都是在创建参数的时候完成的,所以在这个第二种方法中,post传参要比上一中方法少一个使用param传递参数的过程,却需要通过创建Bean类,以及为Bean类赋值的步骤,同样,需要重载post方法,多添加一个(String json)的参数,然后将RequestBody 的创建改成如下形式即可:
RequestBody requestBody =
RequestBody.create(MediaType.parse("application/json; charset=utf-8"), json);
这样,我们的就实现了Retrofit的JSON传参,虽然看起来有些麻烦,但是……也确实麻烦,不过在有这方面需求的时候,这个封装也是必不可少的过程,所以希望这里可以帮助到大家。
尾址特殊字符转译问题
这个内容是在前面封装的时候所没有想到的,在开发过程中却狠狠的坑了我一回,除了含着泪解决,我们还有什么办法呢!
之前我们的传递的内容都很简单,比如我前面一直拿来举例的豆瓣电影查询,尾址只需要传递“top250”就算完成战斗,所以也就没有遇到过尾址特殊字符转译的问题,可是还是以豆瓣电影查询为例,完整请求URL为“https://api.douban.com/v2/movie/top250”,可是豆瓣API明显不只是电影一项:
[豆瓣书籍所搜] (https://api.douban.com/v2/book/search?q=python&fields=id,title)
[豆瓣音乐搜索] (https://api.douban.com/v2/music/search)
等等等等,有兴趣大家可以去豆瓣Api V2看一看还有多少,这么多的内容,我们在封装网络访问框架的时候,自然不会只使用“https://api.douban.com/v2/movie/”作为baseUrl,而通过观察,显而易见,我们所使用的baseUrl将会是“https://api.douban.com/v2/”,而后在根据具体需求设置尾址“movie/top250”、“book/search”、或者“music/search”,而当如此使用的时候,我们今天遇到的问题也就来了,那就是尾址特殊字符转译,说具体点,就是“/”被Retrofit进行了转译,变成了“%2F”,当然,除此之外还会有其他的特殊字符会被转译,具体请参见网址URL中特殊字符转义编码。查询了一些资料,有一些解决方法是通过“//”,将被转译的字符再强行转译回来,原理嘛,就好像传说中的负负得正一样。
只不过很不幸的是,我尝试了这个方法之后,在上述监听到的URL却编程了如下的样子“https://api.douban.com/v2/movie%2F%2Ftop250”(点了一下,豆瓣竟然能识别,不禁让我“内牛满面”),服务器果断给我返回了404,查了N多资料,真心没找到Retrofit应该怎么解决这个问题,而之前使用Volley的时候,又没遇到过这种情况。所以这里只能采用一个最无脑的解决方法,如果大家谁有更高端的解决方法,希望能不吝赐教:
首先,将post或者get传入的action根据action中最后一个“/”的坐标将其分为两部分,最后一个“/”后面的字符串被用做尾址,继续传递到后面Service对应的方法中,也就是下面代码中的“action1 ”,而其余的部分,也就是下面的“action2 ”则被用来当做参数传入Retrofit的初始化中,拼接到baseUrl中。
public <T extends BaseBean> void get(final String action, final Class<T> clazz, boolean showDialog, final ResultCallBack callBack) {
this.action = action;
if (showDialog) {
showLoadDialog();
}
String action1 = action.substring(action.lastIndexOf("/") + 1);
String action2 = action.substring(0, action.lastIndexOf("/") + 1);
initBaseData(action2, false);
if (getService == null) {
getService = retrofit.create(RetrofitGetService.class);
}
if (params == null) {
params = new HashMap<>();
}
getService.getResult(action1, headerParams, params)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Observer<ResponseBody>() {
@Override
public void onSubscribe(@NonNull Disposable d) {
}
@Override
public void onNext(@NonNull ResponseBody responseBody) {
hideLoadDialog();
try {
String responseString = responseBody.string();
LogUtil.info("responseString", action + "********** responseString get " + responseString);
callBack.success(action, new Gson().fromJson(responseString, clazz));
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void onError(@NonNull Throwable e) {
LogUtil.info("responseString", "responseString get " + e.toString());
callBack.error(action, e);
}
@Override
public void onComplete() {
params = null;
}
});
}
下面是动态拼接baseUrl的方法:
private void initBaseData(final String url, boolean isFromLogin) {
OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.connectTimeout(5, TimeUnit.SECONDS);
builder.addInterceptor(new Interceptor() {
@Override
public okhttp3.Response intercept(Chain chain) throws IOException {
Request request = chain.request();
LogUtil.info("zzz", "request====" + action);
LogUtil.info("zzz", "request====" + request.headers().toString());
LogUtil.info("zzz", "request====" + request.toString());
okhttp3.Response proceed = chain.proceed(request);
LogUtil.info("zzz", "proceed====" + proceed.headers().toString());
return proceed;
}
});
Retrofit.Builder builder1 = new Retrofit.Builder()
.client(builder.build())
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create());
builder1.baseUrl(baseUrl + url);
retrofit = builder1.build();
}
如此,就可以动态适配我们的尾址,同时也避免了转译带来的影响,虽然说方法简陋的可以,但是暂时拿来应急还是可以考虑的,如果后续发现更好的方法,我会继续更新到后续的博客上的。