1. 程式人生 > >Volley StringRequest和JSONObjectRequest使用幾個細節

Volley StringRequest和JSONObjectRequest使用幾個細節

一、Volley StringRequest

        下面是百度api提供的免費的介面‘天氣查詢-檢視可用城市列表’,在百度apistore中有提供(天氣查詢)。

看下官方提供的程式碼:

String httpUrl = "http://apis.baidu.com/apistore/weatherservice/citylist";
String httpArg = "cityname=%E6%9C%9D%E9%98%B3";
String jsonResult = request(httpUrl, httpArg);
System.out.println(jsonResult);

/**
 * @param urlAll
 *            :請求介面
 * @param httpArg
 *            :引數
 * @return 返回結果
 */
public static String request(String httpUrl, String httpArg) {
    BufferedReader reader = null;
    String result = null;
    StringBuffer sbf = new StringBuffer();
    httpUrl = httpUrl + "?" + httpArg;

    try {
        URL url = new URL(httpUrl);
        HttpURLConnection connection = (HttpURLConnection) url
                .openConnection();
        connection.setRequestMethod("GET");
        // 填入apikey到HTTP header
        connection.setRequestProperty("apikey",  "您自己的apikey");
        connection.connect();
        InputStream is = connection.getInputStream();
        reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
        String strRead = null;
        while ((strRead = reader.readLine()) != null) {
            sbf.append(strRead);
            sbf.append("\r\n");
        }
        reader.close();
        result = sbf.toString();
    } catch (Exception e) {
        e.printStackTrace();
    }
    return result;
}

現在用Volley 的StringRequest實現它的介面。需要關注的幾個地方:

URL:

httpUrl = httpUrl + "?" + httpArg;//http://apis.baidu.com/apistore/weatherservice/citylist?cityname=%E6%9C%9D%E9%98%B3

Request Method:

connection.setRequestMethod("GET");

Header : 

connection.setRequestProperty("apikey",  "您自己的apikey");

引數帶在url後面,如果有多個引數用‘&’分割

如果是拼接引數需要注意最前面還有一個‘?’

GET請求

帶了http header引數,對應connection.setRequestProperty("","");

下面使用Volley的StringRequest的兩種方式來實現上面的請求。

第一種:StringRequest Method.GET 

把引數帶在url後面,使用StringRequest的不帶參構造。Header引數重寫getHeaders方法,對應 connection.setRequestProperty("apikey",  "您自己的apikey");

 private String url = "http://apis.baidu.com/apistore/weatherservice/citylist";
 private String params = "cityname=朝陽";
 private void stringRequestWithGet(){
        url = url + "?" + params;
        StringRequest stringRequest = new StringRequest(Request.Method.GET, url, new Response.Listener<String>() {
            @Override
            public void onResponse(String response) {
                try {
                    //使用JSONObject給response轉換編碼
                    JSONObject jsonObject = new JSONObject(response);
                    responseText.setText(jsonObject.toString());
                } catch (JSONException e) {
                    e.printStackTrace();
                }
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                responseText.setText(error.getMessage());
            }
        }){
            @Override
            public Map<String, String> getHeaders() throws AuthFailureError {
                Map<String,String> map = new HashMap<>();
                map.put("apikey","f71e5f1e08cd5a7e42a7e9aa70d22458");
                return map;
            }
        };
        mQueue.add(stringRequest);
    }

第二種:StringRequest Method.POST

Method:改為POST

Params:引數不帶在Url末尾,而是重寫StringRequest的getParams()。

注意:我以這種方式請求百度apistore中 ‘天氣查詢-檢視可用城市列表’介面,沒有成功。但是訪問其他提供商的API是成功的。可能是這個介面不支援Post請求,或者我少傳了什麼引數吧。

private void stringRequestWithPost(){
        StringRequest stringRequest = new StringRequest(Request.Method.POST, url, new Response.Listener<String>() {
            @Override
            public void onResponse(String response) {
                try {
                    //使用JSONObject給response轉換編碼
                    JSONObject jsonObject = new JSONObject(response);
                    responseText.setText(jsonObject.toString());
                } catch (JSONException e) {
                    e.printStackTrace();
                }
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                responseText.setText(error.getMessage());
            }
        }){
            @Override
            protected Map<String, String> getParams() throws AuthFailureError {
                Map<String,String> map = new HashMap<>();
                map.put("cityname","朝陽");
                return map;
            }

            @Override
            public Map<String, String> getHeaders() throws AuthFailureError {
                Map<String,String> map = new HashMap<>();
                map.put("apikey","f71e5f1e08cd5a7e42a7e9aa70d22458");
                return map;
            }
        };
        mQueue.add(stringRequest);
    }


二、Volley JsonObjectRequest

下面介紹使用JsonObjectRequest,以及使用JsonObjectRequest會遇到的一些坑。

由於百度apistore中我以post請求一直沒成功過,不知道是什麼問題,下面我換了一家API提供商 showapi 的 'QQ音樂十八大排行榜’介面來進行GET請求和POST請求。

先說Method.POST,遇到的坑會多一點。

第一種:JsonObjectRequest  Method.POST

錯誤寫法

 /**
     * JsonOnjectRequest Post 錯誤方式
     * 這裡使用了和StringRequest一樣的方式傳參 重寫getParams()方法。
     */
    private void jsonObjectRequestError1(){

        String url = "http://route.showapi.com/213-3";
        final Map<String,String> params = DummyData.getDummyData();

        JsonObjectRequest jsonObjectRequest = new JsonObjectRequest(Request.Method.POST,url,null, new Response.Listener<JSONObject>() {
            @Override
            public void onResponse(JSONObject response) {
                responseText.setText(response.toString());
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                responseText.setText(error.getMessage());
            }
        }){
            @Override
            protected Map<String, String> getParams() throws AuthFailureError {
                return params;
            }
        };
        mQueue.add(jsonObjectRequest);
    }

JsonObjectRequest構造中的引數傳null,和StringRequest一樣去重寫getParams()方法來傳參。貌似是沒問題的?

使用這段程式碼拿不到伺服器返回的資料,反饋的結果是沒有傳參給伺服器。那就看看引數最終是在哪裡提交給伺服器的。

Debug HurlStack.java中的addBodyIfExists()這個方法,發現request.getBody()==null,也就是沒有接收到引數。

StringRequest可以重寫getParams來傳參是因為Request中的這個方法:

Request.java

    public byte[] getBody() throws AuthFailureError {
        Map<String, String> params = getParams();
        if (params != null && params.size() > 0) {
            return encodeParameters(params, getParamsEncoding());
        }
        return null;
    }
重寫的getParams的結果會傳到這裡。

JsonObjectRequest不能重寫getParams來傳參是因為JsonRequest中的這個方法:

JsonRequest.java

    @Override
    public byte[] getBody() {
        try {
            return mRequestBody == null ? null : mRequestBody.getBytes(PROTOCOL_CHARSET);
        } catch (UnsupportedEncodingException uee) {
            VolleyLog.wtf("Unsupported Encoding while trying to get the bytes of %s using %s",
                    mRequestBody, PROTOCOL_CHARSET);
            return null;
        }
    }

重寫了Request.getBody(),這段程式碼說明 mRequestBody這個變數就是它的引數。這個變數就存在與JsonRequest的建構函式中。

說明,JsonObjectRequest的傳參必須通過構造來傳,重寫getParams()是無法傳遞的。

下面按正常思路,來踩第二個坑。給JsonObjectRequest的構造傳遞引數。又會出現什麼問題?

    /**
     * JsonOnjectRequest Post 可能會出錯的方式
     *
     * 這裡的思路是呼叫JsonObjectRequest帶參構造,傳參進入不就得了?
     * 但是這裡還是請求不到資料。
     * 原因是使用JsonObjectRequest 如果伺服器接受的引數型別是 http://....?key1=value1&key2=value2,
     * 而不是json串{key1 : value1,key2 : value2...},引數就會傳不進去。
     * 可以在 HurlStack.java 的addBodyIfExists方法中看到你傳入的引數資訊。
     */
    private void jsonObjectRequestError2(){

        String url = "http://route.showapi.com/213-3";
        Map<String,String> map = DummyData.getDummyData();
        JSONObject params = new JSONObject(map);

        JsonObjectRequest jsonObjectRequest = new JsonObjectRequest(Request.Method.POST,url,params, new Response.Listener<JSONObject>() {
            @Override
            public void onResponse(JSONObject response) {
                responseText.setText(response.toString());
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                responseText.setText(error.getMessage());
            }
        });
        mQueue.add(jsonObjectRequest);
    }

仍然獲取不到資料,原因看上面的註釋。反饋的結果是沒有傳參給伺服器。那就看看引數最終是在哪裡提交給伺服器的。

Debug HurlStack.java中的addBodyIfExists()這個方法。


引數傳遞是正確的,只不過傳遞的引數格式是一個json串。這時考慮是不是伺服器不接受這樣的傳參,那就換一種格式傳遞引數:

http://apis.baidu.com/apistore/weatherservice/citylist?key1=value1&key2=value2...

下面以這種思路把程式碼進行改造。

假設伺服器不接受json串的引數,而是接受key=value這樣的引數,這樣我們就傳遞這種格式的引數給JsonObjectRequest。然後在試試能否成功。但是JsonObjectRequest的構造中,引數必須以JSONObject來定義,所以只能單獨定義一個類繼承JSONObject,引數以String的格式傳入。

按正常思路,來踩第三個坑。定義一個類繼承JsonObjectRequest,傳入一個String引數,格式是 "key1=value1&key2=value2..."。

這裡不貼程式碼了,這個類除了把構造的引數JSONObject改為String,其他完全一致。當然,結果還是獲取不到資料。

原因很簡單,看JsonRequest中的變數 PROTOCOL_CONTENT_TYPE。

    /** Content type for request. */
    private static final String PROTOCOL_CONTENT_TYPE =
    String.format("application/json; charset=%s", PROTOCOL_CHARSET);
請求的是一個json串,但是這裡自定義的Request傳入的引數不是一個json串。所以還需要在自定義的類中重寫一個方法:
    @Override
    public String getBodyContentType() {
        return "application/x-www-form-urlencoded; charset=" + getParamsEncoding();
    }

這段程式碼從Request.java中Copy。

自定義的Request

public class MyJsonObjectRequest extends JsonRequest<JSONObject> {

    String stringRequest;

    /**
     * 這裡的method必須是Method.POST,也就是必須帶引數。
     * 如果不想帶引數,可以用JsonObjectRequest,給它構造引數傳null。GET方式請求。
     * @param stringRequest 格式應該是 "key1=value1&key2=value2"
     */

    public MyJsonObjectRequest(String url, String stringRequest,
                             Response.Listener<JSONObject> listener, Response.ErrorListener errorListener) {
        super(Method.POST, url,stringRequest , listener, errorListener);
        this.stringRequest = stringRequest;
    }

    @Override
    public String getBodyContentType() {
        return "application/x-www-form-urlencoded; charset=" + getParamsEncoding();
    }

    @Override
    protected Response<JSONObject> parseNetworkResponse(NetworkResponse response) {
        try {
            String jsonString = new String(response.data,
                    HttpHeaderParser.parseCharset(response.headers, PROTOCOL_CHARSET));
            return Response.success(new JSONObject(jsonString),
                    HttpHeaderParser.parseCacheHeaders(response));
        } catch (UnsupportedEncodingException e) {
            return Response.error(new ParseError(e));
        } catch (JSONException je) {
            return Response.error(new ParseError(je));
        }
    }

請求方法:
private void jsonObjectRequestPostSuccess1(){
        String url = "http://route.showapi.com/213-3";
        Map<String,String> map = DummyData.getDummyData();
        String params = appendParameter(url,map);

        MyJsonObjectRequest jsonObjectRequest = new MyJsonObjectRequest(url,params, new Response.Listener<JSONObject>() {
            @Override
            public void onResponse(JSONObject response) {
                responseText.setText(response.toString());
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                responseText.setText(error.getMessage());
            }
        });
        mQueue.add(jsonObjectRequest);
    }



    private String appendParameter(String url,Map<String,String> params){
        Uri uri = Uri.parse(url);
        Uri.Builder builder = uri.buildUpon();
        for(Map.Entry<String,String> entry:params.entrySet()){
            builder.appendQueryParameter(entry.getKey(),entry.getValue());
        }
        return builder.build().getQuery();
    }


當然,也並非一定要自定義一個JsonObjectRequest不可,如果瞭解Request的繼承關係和引數的傳遞流程,直接使用JsonObjectRequest也是可以的。

    /**
     * 不想自定義,非得用JsonObjectRequestPost不可,只要你不嫌麻煩。
     */
    private void jsonObjectRequestPostSuccess2(){
        String url = "http://route.showapi.com/213-3";
        Map<String,String> params = DummyData.getDummyData();
        final String mRequestBody = appendParameter(url,params);

        JsonObjectRequest jsonObjectRequest = new JsonObjectRequest(Request.Method.POST,url,null, new Response.Listener<JSONObject>() {
            @Override
            public void onResponse(JSONObject response) {
                responseText.setText(response.toString());
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                responseText.setText(error.getMessage());
            }
        }){
            @Override
            public String getBodyContentType() {
                return "application/x-www-form-urlencoded; charset=" + getParamsEncoding();
            }

            @Override
            public byte[] getBody() {
                try {
                    return mRequestBody == null ? null : mRequestBody.getBytes(PROTOCOL_CHARSET);
                } catch (UnsupportedEncodingException uee) {
                    VolleyLog.wtf("Unsupported Encoding while trying to get the bytes of %s using %s",
                            mRequestBody, PROTOCOL_CHARSET);
                    return null;
                }
            }
        };
        mQueue.add(jsonObjectRequest);
    }
    
    private String appendParameter(String url,Map<String,String> params){
        Uri uri = Uri.parse(url);
        Uri.Builder builder = uri.buildUpon();
        for(Map.Entry<String,String> entry:params.entrySet()){
            builder.appendQueryParameter(entry.getKey(),entry.getValue());
        }
        return builder.build().getQuery();
    }

總結:

使用JsonObjectRequest 請求伺服器,它的引數是以 json 串 的格式傳輸給伺服器的,考慮伺服器是否支援這種格式,可以在JsonRequest的PROTOCOL_CONTENT_TYPE中看到它傳遞引數的型別。如果伺服器支援的是 http:...?key1=value1&key2=value2這種格式,也就是對應Request.java中的PROTOCOL_CONTENT_TYPE變數型別。就需要做一些改動,有兩種方式來實現。第一種方式是定義一個類繼承JsonObjectRequest,第二種直接使用JsonObjectRequest。原理是相同的,重點是重寫匿名內部類的getBodyContentType這個方法。不能重寫getParams這個方法類傳參。

使用StringRequest 如果是帶引數,或者Http Header,或者其他,可以重寫它匿名內部類方法,如getParams(),getHeader()等等。


有必要關注HurlStack.java這個類。

原始碼