6.访问REST API

6.1 问题

应用程序需要通过HTTP访问RESTful API,实现与远程主机上Web服务的交互。

注意:
REST(Representational State Transfer,表述性状态转移)是一种常见的Web服务器架构风格。RESTful API通常用标准的HTTP动作构建,以创建对远程资源的请求。返回的响应通常是标准文档格式,例如XML、JSON或逗号分隔符(Comma-Separated Value,CSV)。

6.2 解决方案

(API Level 9)
Google建议使用HTTPURLConnection访问Android中的网络资源。可以在AsyncTask中使用Java HttpURLConnection类。不考虑本节中给出的目标版本,这个类在API Level 1时就已经是Android框架的一部分,但在Android2.3发布之前,只推荐使用该方法进行网络的I/O操作。主要的原因就是刚开始是这个类的实现有许多的bug,所以选择使用HttpClient会更加稳妥。后来,Android团队对HttpURLConnection的性能和稳定性做了很大的提高,现在其已成为一种推荐使用的方式。

注意:
HttpClient来自于Apache HttpComponents库,该库已合并到Android框架中。如果你的应用程序仍然支持Gingerbread之前的平台,这就是你可以考虑采用的备选方法。

HttpURLConnection是轻量级的,在新版本的Android中响应速度更快并且内置很多增强功能。它的API更加开发,所以也更加普及,可以实现任何类型的HTTP传输。缺点是开发人员需要编写更多的代码。

6.3 实现机制

让我们看一下如何使用HttpURLConnection发送HTTP请求。网络请求是较长的阻塞性操作,因此不能在应用程序的主线程上执行它们。这个规则非常重要,如果尝试这样做,Android将通过NetworkOnMainThread异常使应用程序崩溃。
因此,必须在诸如AsyncTask这样的任务中封装网络调用,以确保在后台线程中完成此工作。我们首先在以下第一段代码中定义RestTask的实现,并在第二段代码中定义一个辅助类。
使用了HttpConnection的RestTask

public class RestTask extends AsyncTask {
    private static final String TAG = "RestTask";

    public interface ResponseCallback {
        public void onRequestSuccess(String response);

        public void onRequestError(Exception error);
    }

    public interface ProgressCallback {
        public void onProgressUpdate(int progress);
    }
    
    private HttpURLConnection mConnection;
    private String mFormBody;
    private File mUploadFile;
    private String mUploadFileName;

    // Activity 回调,使用WeakReferences来避免阻塞操作导致链接对象保留在内存中
    private WeakReference mResponseCallback;
    private WeakReference mProgressCallback;

    public RestTask(HttpURLConnection connection) {
        mConnection = connection;
    }

    public void setFormBody(List formData) {
        if (formData == null) {
            mFormBody = null;
            return;
        }
        
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < formData.size(); i++) {
            NameValuePair item = formData.get(i);
            sb.append( URLEncoder.encode(item.getName()) );
            sb.append("=");
            sb.append( URLEncoder.encode(item.getValue()) );
            if (i != (formData.size() - 1)) {
                sb.append("&");
            }
        }

        mFormBody = sb.toString();
    }

    public void setUploadFile(File file, String fileName) {
        mUploadFile = file;
        mUploadFileName = fileName;
    }

    public void setResponseCallback(ResponseCallback callback) {
        mResponseCallback = new WeakReference(callback);
    }

    public void setProgressCallback(ProgressCallback callback) {
        mProgressCallback = new WeakReference(callback);
    }

    private void writeMultipart(String boundary, String charset, OutputStream output, boolean writeContent) throws IOException {
        BufferedWriter writer = null;
        try {
            writer = new BufferedWriter(new OutputStreamWriter(output, Charset.forName(charset)), 8192);
            // 发送表单数据组件
            if (mFormBody != null) {
                writer.write("--" + boundary);
                writer.write("\r\n");
                writer.write("Content-Disposition: form-data; name=\"parameters\"");
                writer.write("\r\n");
                writer.write("Content-Type: text/plain; charset=" + charset);
                writer.write("\r\n");
                writer.write("\r\n");
                if (writeContent) {
                    writer.write(mFormBody);
                }
                writer.write("\r\n");
                writer.flush();
            }
            // 发送二进制文件
            writer.write("--" + boundary);
            writer.write("\r\n");
            writer.write("Content-Disposition: form-data; name=\"" + mUploadFileName
                            + "\"; filename=\"" + mUploadFile.getName() + "\"");
            writer.write("\r\n");
            writer.write("Content-Type: "
                            + URLConnection.guessContentTypeFromName(mUploadFile
                                            .getName()));
            writer.write("\r\n");
            writer.write("Content-Transfer-Encoding: binary");
            writer.write("\r\n");
            writer.write("\r\n");
            writer.flush();
            if (writeContent) {
                InputStream input = null;
                try {
                    input = new FileInputStream(mUploadFile);
                    byte[] buffer = new byte[1024];
                    for (int length = 0; (length = input.read(buffer)) > 0;) {
                        output.write(buffer, 0, length);
                    }
                    // 不要关闭OutputStream
                    output.flush();
                } catch (IOException e) {
                    Log.w(TAG, e);
                } finally {
                    if (input != null) {
                        try {
                            input.close();
                        } catch (IOException e) {
                        }
                    }
                }
            }
            // 这个回车换行标志着二进制数据块的结束
            writer.write("\r\n");
            writer.flush();

            // multipart/form-data的结束
            writer.write("--" + boundary + "--");
            writer.write("\r\n");
            writer.flush();
        } finally {
            if (writer != null) {
                writer.close();
            }
        }
    }

    private void writeFormData(String charset, OutputStream output) throws IOException {
        try {
            output.write(mFormBody.getBytes(charset));
            output.flush();
        } finally {
            if (output != null) {
                output.close();
            }
        }
    }

    @Override
    protected Object doInBackground(Void... params) {
        //生成用来标识界限的随机字符串
        String boundary = Long.toHexString(System.currentTimeMillis());
        String charset = Charset.defaultCharset().displayName();
        
        try {
            // 如果可以的话,创建输出流
            if (mUploadFile != null) {
                //我们必须做一次复合请求
                mConnection.setRequestProperty("Content-Type",
                        "multipart/form-data; boundary=" + boundary);
                //计算extra元数据的大小
                ByteArrayOutputStream bos = new ByteArrayOutputStream();
                writeMultipart(boundary, charset, bos, false);
                byte[] extra = bos.toByteArray();
                int contentLength = extra.length;
                //将文件的大小加到length上
                contentLength += mUploadFile.length();
                //如果存在表单主体,把它也加到length上
                if (mFormBody != null) {
                    contentLength += mFormBody.length();
                }
                mConnection.setFixedLengthStreamingMode(contentLength);
            } else if (mFormBody != null) {
                //这种情况下,只是发送表单数据
                mConnection.setRequestProperty("Content-Type",
                                "application/x-www-form-urlencoded; charset=" + charset);
                mConnection.setFixedLengthStreamingMode(mFormBody.length());
            }
            
            //这是第一次调用URLConnection,它会真正执行网络IO操作
            // openConnection()执行的还是本地操作
            mConnection.connect();

            // 如果可以的话(对于POST),创建输出流
            if (mUploadFile != null) {
                OutputStream out = mConnection.getOutputStream();
                writeMultipart(boundary, charset, out, true);
            } else if (mFormBody != null) {
                OutputStream out = mConnection.getOutputStream();
                writeFormData(charset, out);
            }

            // 获取响应数据
            int status = mConnection.getResponseCode();
            if (status >= 300) {
                String message = mConnection.getResponseMessage();
                return new HttpResponseException(status, message);
            }

            InputStream in = mConnection.getInputStream();
            String encoding = mConnection.getContentEncoding();
            int contentLength = mConnection.getContentLength();
            if (encoding == null) {
                encoding = "UTF-8";
            }

            byte[] buffer = new byte[1024];

            int length = contentLength > 0 ? contentLength : 0;
            ByteArrayOutputStream out = new ByteArrayOutputStream(length);

            int downloadedBytes = 0;
            int read;
            while ((read = in.read(buffer)) != -1) {
                downloadedBytes += read;
                publishProgress((downloadedBytes * 100) / contentLength);
                out.write(buffer, 0, read);
            }

            return new String(out.toByteArray(), encoding);
        } catch (Exception e) {
            Log.w(TAG, e);
            return e;
        } finally {
            if (mConnection != null) {
                mConnection.disconnect();
            }
        }
    }

    @Override
    protected void onProgressUpdate(Integer... values) {
        // 更新进度UI
        if (mProgressCallback != null && mProgressCallback.get() != null) {
            mProgressCallback.get().onProgressUpdate(values[0]);
        }
    }

    @Override
    protected void onPostExecute(Object result) {
        if (mResponseCallback != null && mResponseCallback.get() != null) {
            if (result instanceof String) {
                mResponseCallback.get().onRequestSuccess((String) result);
            } else if (result instanceof Exception) {
                mResponseCallback.get().onRequestError((Exception) result);
            } else {
                mResponseCallback.get().onRequestError(new IOException("Unknown Error Contacting Host"));
            }
        }
    }
}

创建请求的工具类

public class RestUtil {
    
    public static RestTask obtainGetTask(String url)
            throws MalformedURLException, IOException {
        HttpURLConnection connection = (HttpURLConnection) (new URL(url))
                .openConnection();

        connection.setReadTimeout(10000);
        connection.setConnectTimeout(15000);
        connection.setDoInput(true);

        RestTask task = new RestTask(connection);
        return task;
    }

    public static RestTask obtainAuthenticatedGetTask(String url,
            String username, String password) throws MalformedURLException, IOException {
        HttpURLConnection connection = (HttpURLConnection) (new URL(url))
                .openConnection();

        connection.setReadTimeout(10000);
        connection.setConnectTimeout(15000);
        connection.setDoInput(true);
        
        attachBasicAuthentication(connection, username, password);
        
        RestTask task = new RestTask(connection);
        return task;
    }
    
    public static RestTask obtainFormPostTask(String url,
            List formData) throws MalformedURLException,
            IOException {
        HttpURLConnection connection = (HttpURLConnection) (new URL(url))
                .openConnection();

        connection.setReadTimeout(10000);
        connection.setConnectTimeout(15000);
        connection.setDoOutput(true);

        RestTask task = new RestTask(connection);
        task.setFormBody(formData);

        return task;
    }
    
    public static RestTask obtainAuthenticatedFormPostTask(String url,
            List formData, String username, String password) throws MalformedURLException,
            IOException {
        HttpURLConnection connection = (HttpURLConnection) (new URL(url))
                .openConnection();

        connection.setReadTimeout(10000);
        connection.setConnectTimeout(15000);
        connection.setDoOutput(true);
        
        attachBasicAuthentication(connection, username, password);

        RestTask task = new RestTask(connection);
        task.setFormBody(formData);

        return task;
    }

    public static RestTask obtainMultipartPostTask(String url,
            List formPart, File file, String fileName)
            throws MalformedURLException, IOException {
        HttpURLConnection connection = (HttpURLConnection) (new URL(url))
                .openConnection();

        connection.setReadTimeout(10000);
        connection.setConnectTimeout(15000);
        connection.setDoOutput(true);

        RestTask task = new RestTask(connection);
        task.setFormBody(formPart);
        task.setUploadFile(file, fileName);

        return task;
    }

    private static void attachBasicAuthentication(URLConnection connection, String username, String password) {
        //Add Basic Authentication Headers
        String userpassword = username + ":" + password;
        String encodedAuthorization = Base64.encodeToString(userpassword.getBytes(), Base64.NO_WRAP);
        connection.setRequestProperty("Authorization", "Basic "+
              encodedAuthorization);
    }
}

我们编写的这个RestTask可以处理GET、简单POST和复合POST请求,并根据添加到RestTask中的组件动态地定义请求的参数。
这个任务支持附加两个可选的回调:一个在请求完成时获得通知,另一个在下载反馈内容时更新所有可见的进度UI。
本例中,应用程序将使用RestUtil辅助类创建RestTask的一个实例。该类细化了HttpURLConnection的创建过程,它并不会进行任何的网络I/O操作,包括网络连接部分和与主机交互部分。辅助类创建了连接实例并且设置了超时值和HTTP请求方法。
注意:
默认情况下,所有URLConnection的请求方法会被设置为GET。调用setDoOutput()则会隐式地把方法设置为POST。如果需要设置为其他的HTTP动作,则需要使用setRequestMethod()。
POST情况下,如果有表单主题内容,这些值会直接设置到我们自定义的任务中,当任务执行时,就会读取它们。
当RestTask执行后,它会检查是否有关联的主题数据需要写入。如果我们已经关联了表单数据(以名-值对的方式)或者需要上传的文件,就会把它作为一个触发器来构造一个POST主体并发送。使用HttpURLConnection,我们需要处理连接的各种事宜,包括告诉服务器收到数据的数量。RestTask会花些时间计算即将发送多少数据,以及通过调用setFixedLengthStreamingMode()构造一个头字段来告诉服务器内容的大小。对于简单表单发送请求的情况,这种计算则很简单,我们只需要传入主体字符串的长度即可。
但是,复合POST包含的文件数据可能会复杂一些。复合POST在主体中有很多的附加数据用于指定POST中各个部分之间的分界线,而且这些数据也会算到我们所设置的长度中。要想完成这个目标,writeMultipart()在构造时可以传入一个本地OutputStream(本例中为一个ByteArrayOutputStream),然后将所有的附加数据写入这个OutputStream中,这样我们就可以衡量附加数据的大小了。当通过这种方式调用writeMultipart()时,会忽略真正的内容,例如文件和表单的数据,这些内容稍后可以通过调用它们各自的length()方法添加,而且我们也不想浪费时间将它们加载到内存中。
注意:
如果不知道要POST的内容有多大,HttpURLConnection还通过setChunkedStreamingMode()方法提供了块上传的机制。本例中,只需要上传即将发送的数据块的大小即可。

当任务已经向主机中写入了任何POST数据后,就可以读取响应内容了。如果初始请求是一个GET请求,由于并没有其他数据需要写入,任何可以直接忽略掉这一步。任何首先会检查响应代码的值以保证没有服务器端的错误,之后将响应的内容下载到一个StringBuilder中。下载时每次大概会读取4KB的数据块,同时将下载数据与响应内容总长度的百分比通知给进度回调处理程序。在所有的内容都下载完成后,任务会将所有结果响应数据以字符串的形式返回。

1.GET示例

在下面的示例中,我们利用了Google Custom Search REST API,此API的请求需要以下几个参数:

  • key : 唯一值,用于标识发出请求的应用程序。
  • cx : 标识符,标识i想要访问的自定义搜索引擎。
  • q : 一个字符串,代表想要执行的搜索查询。
    注意:
    关于这个API的更多信息可以访问http://developers.google.com/custom-search/。

在很多公共API中,GET请求是最简单也是最常见的请求。参数必须与请求一起编码到URL字符串中发送,所以不需要提供其他的数据。下面将创建GET请求,搜索“Android”(参见以下代码)。
执行API GET请求的Activity

public class SearchActivity extends Activity implements RestTask.ProgressCallback, RestTask.ResponseCallback {

    private static final String SEARCH_URI = "https://www.googleapis.com/customsearch/v1?key=%s&cx=%s&q=%s";
    private static final String SEARCH_KEY = "AIzaSyBbW-W1SHCK4eW0kK74VGMLJj_b-byNzkI";
    private static final String SEARCH_CX = "008212991319514020231:1mkouq8yagw";
    private static final String SEARCH_QUERY = "Android";
    
    private TextView mResult;
    private ProgressDialog mProgress;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ScrollView scrollView = new ScrollView(this);
        mResult = new TextView(this);
        scrollView.addView(mResult, new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
        setContentView(scrollView);
        
        //创建请求
        try{
            //简单 GET请求
            String url = String.format(SEARCH_URI, SEARCH_KEY, SEARCH_CX, SEARCH_QUERY);
            RestTask getTask = RestUtil.obtainGetTask(url);
            getTask.setResponseCallback(this);
            getTask.setProgressCallback(this);
    
            getTask.execute();
            
            //向用户显示进度
            mProgress = ProgressDialog.show(this, "Searching", "Waiting For Results...", true);
        } catch (Exception e) {
            mResult.setText(e.getMessage());
        }
    }
    
    @Override
    public void onProgressUpdate(int progress) {
        if (progress >= 0) {
            if (mProgress != null) {
                mProgress.dismiss();
                mProgress = null;
            }
            //更新用户的进度
            mResult.setText(String.format("Download Progress: %d%%", progress));
        }
    }
    
    @Override
    public void onRequestSuccess(String response) {
        //结束进度条
        if(mProgress != null) {
            mProgress.dismiss();
        }
        //处理返回的数据 (这里只是把结果显示出来)
        mResult.setText(response);
    }
    
    @Override
    public void onRequestError(Exception error) {
        //结束进度条
        if(mProgress != null) {
            mProgress.dismiss();
        }
        //处理返回的数据(这里只是把结果显示出来)
        mResult.setText("An Error Occurred: "+error.getMessage());
    }
}

在这个示例中,创建的这种GET请求需要所需连接的URL(在这里,就是发送到googleapis.com的GET请求)。URL保存为格式化字符串常量,Google API所需的参数则在运行时创建请求之前加上。
在Activity中创建了一个RestTask并执行,并把该Activity作为RestTask的回调。在任务完成后,会调用onRequestSuccess()或onRequestError(),在成功的情况下,会分析API返回的数据并处理。
我们还添加ProgressCallback到此Activity实现的接口列表中,这些Activity就可以得到下载进度的通知。然而,并不是所有的网络服务器都会为请求返回有效的内容长度(而是返回-1),这样会导致很难实现基于百分比的进度表示。这种情况下,我们的回调函数在下载完成前会一直显示一个持续的进度对话框。而对于进度值可以确定的情况下,该进度对话框会消失在屏幕上显示进度的百分比。
下载完成后,Activity会得到一个下载结果的JSON字符串回调。关于分析结构化XML和JSON数据的内容会在之后小节进行讨论,这里先简单在用户界面上显示原始的响应数据。

2.POST示例

很多时候,API要求在请求中提供所需的数据,可能是认证令牌,也有可能是搜索查询的内容。API会要求你通过HTTP POST发送请求,这样这些数据就会被编码到请求主体中,而不是编码到URL中。为了演示POST是如何工作的,我们将向httpbin.org发送请求,它是一个开发网络,用来读取和验证请求的内容并将它们返回(参见以下代码)。

public class SearchActivity extends Activity implements RestTask.ProgressCallback, RestTask.ResponseCallback {

    private static final String POST_URI = "http://httpbin.org/post";

    private TextView mResult;
    private ProgressDialog mProgress;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ScrollView scrollView = new ScrollView(this);
        mResult = new TextView(this);
        scrollView.addView(mResult, new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
        setContentView(scrollView);
        
        //创建请求
        try{
            //简单POST请求
            List parameters = new ArrayList();
            parameters.add(new BasicNameValuePair("title", "Android Recipes"));
            parameters.add(new BasicNameValuePair("summary", "Learn Android Quickly"));
            parameters.add(new BasicNameValuePair("authors", "Smith/Friesen"));
            RestTask postTask = RestUtil.obtainFormPostTask(POST_URI, parameters);            
            postTask.setResponseCallback(this);
            postTask.setProgressCallback(this);
            
            getTask.execute();
            
            //向用户显示进度
            mProgress = ProgressDialog.show(this, "Searching", "Waiting For Results...", true);
        } catch (Exception e) {
            mResult.setText(e.getMessage());
        }
    }
    
    @Override
    public void onProgressUpdate(int progress) {
        if (progress >= 0) {
            if (mProgress != null) {
                mProgress.dismiss();
                mProgress = null;
            }
            //更新用户的进度
            mResult.setText(String.format("Download Progress: %d%%", progress));
        }
    }
    
    @Override
    public void onRequestSuccess(String response) {
        //结束进度条
        if(mProgress != null) {
            mProgress.dismiss();
        }
        //处理返回的数据(这里只是把结果显示出来)
        mResult.setText(response);
    }
    
    @Override
    public void onRequestError(Exception error) {
        //结束进度条
        if(mProgress != null) {
            mProgress.dismiss();
        }
        //处理返回的数据(这里只是把结果显示出来)
        mResult.setText("An Error Occurred: "+error.getMessage());
    }
}

这是一个典型表单数据POST的示例,其中将表单字段作为名-值对传入。因此我们的RestTask已经设置为处理此任务,在此需要从RestUtil获得正确的任务,并且填入表单数据。和GET示例一样,之后会在后面的小节介绍如何分析结构化的XML和JSON反馈数据,目前只是将原始的反馈信息呈现到用户界面上。
这里需要注意的是,进度回调只涉及下载相关的响应,而没有涉及POST数据的上传,上传相关处理可能需要由开发人员来实现。

3.上传示例

以下代码演示了一个稍微复杂些的复合POST。在此同时上传原始二进制数据和一些名-值表单数据。
执行复合POST请求API的Activity

public class SearchActivity extends Activity implements RestTask.ProgressCallback, RestTask.ResponseCallback {

    private static final String SEARCH_URI = "https://www.googleapis.com/customsearch/v1?key=%s&cx=%s&q=%s";
    private static final String SEARCH_KEY = "AIzaSyBbW-W1SHCK4eW0kK74VGMLJj_b-byNzkI";
    private static final String SEARCH_CX = "008212991319514020231:1mkouq8yagw";
    private static final String SEARCH_QUERY = "Android";
    
    private static final String POST_URI = "http://httpbin.org/post";

    private TextView mResult;
    private ProgressDialog mProgress;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ScrollView scrollView = new ScrollView(this);
        mResult = new TextView(this);
        scrollView.addView(mResult, new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
        setContentView(scrollView);
        
        //Create the requests
        try{
            //Simple GET
            String url = String.format(SEARCH_URI, SEARCH_KEY, SEARCH_CX, SEARCH_QUERY);
            RestTask getTask = RestUtil.obtainGetTask(url);
            getTask.setResponseCallback(this);
            getTask.setProgressCallback(this);
            
            //Simple POST
            List parameters = new ArrayList();
            parameters.add(new BasicNameValuePair("title", "Android Recipes"));
            parameters.add(new BasicNameValuePair("summary", "Learn Android Quickly"));
            parameters.add(new BasicNameValuePair("authors", "Smith/Friesen"));
            RestTask postTask = RestUtil.obtainFormPostTask(POST_URI, parameters);            
            postTask.setResponseCallback(this);
            postTask.setProgressCallback(this);
            
            //File POST
            Bitmap image = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher);
            File imageFile = new File(getExternalCacheDir(), "myImage.png");
            FileOutputStream out = new FileOutputStream(imageFile);
            image.compress(CompressFormat.PNG, 0, out);
            out.flush();
            out.close();   
            List fileParameters = new ArrayList();
            fileParameters.add(new BasicNameValuePair("title", "Android Recipes"));
            fileParameters.add(new BasicNameValuePair("description", "Image File Upload"));
            RestTask uploadTask = RestUtil.obtainMultipartPostTask(POST_URI, fileParameters, imageFile, "avatarImage");
            uploadTask.setResponseCallback(this);
            uploadTask.setProgressCallback(this);
            
            getTask.execute();
            
            //Display progress to the user
            mProgress = ProgressDialog.show(this, "Searching", "Waiting For Results...", true);
        } catch (Exception e) {
            mResult.setText(e.getMessage());
        }
    }
    
    @Override
    public void onProgressUpdate(int progress) {
        if (progress >= 0) {
            if (mProgress != null) {
                mProgress.dismiss();
                mProgress = null;
            }
            //Update user of progress
            mResult.setText(String.format("Download Progress: %d%%", progress));
        }
    }
    
    @Override
    public void onRequestSuccess(String response) {
        //Clear progress indicator
        if(mProgress != null) {
            mProgress.dismiss();
        }
        //Process the response data (here we just display it)
        mResult.setText(response);
    }
    
    @Override
    public void onRequestError(Exception error) {
        //Clear progress indicator
        if(mProgress != null) {
            mProgress.dismiss();
        }
        //Process the response data (here we just display it)
        mResult.setText("An Error Occurred: "+error.getMessage());
    }
}

本例中,我们构造的POST请求包含两个不同的部分:表单数据部分(由名-值对组成)和文件部分。为了演示这个示例,我们使用应用程序的图标并将它快速地写入外部存储器的一个PNG文件(用于上传)中。
本例中,httpbin的JSON响应数据将对应于表单数据元素和Base4编码表示的PNG图像。

4.基本授权

向RestTask中添加基本授权的过程相当简单。有两种方式可以实现:在每个请求中直接添加或者全局使用名为Authenticator的类,首先我们看一下在单个请求中添加基本授权。以下代码修改了RestUtil的方法来关联适当格式的用户名和密码。
实现了基本授权功能的RestUtil

public class RestUtil {
    
    public static RestTask obtainGetTask(String url)
            throws MalformedURLException, IOException {
        HttpURLConnection connection = (HttpURLConnection) (new URL(url))
                .openConnection();

        connection.setReadTimeout(10000);
        connection.setConnectTimeout(15000);
        connection.setDoInput(true);

        RestTask task = new RestTask(connection);
        return task;
    }

    public static RestTask obtainAuthenticatedGetTask(String url,
            String username, String password) throws MalformedURLException, IOException {
        HttpURLConnection connection = (HttpURLConnection) (new URL(url))
                .openConnection();

        connection.setReadTimeout(10000);
        connection.setConnectTimeout(15000);
        connection.setDoInput(true);
        
        attachBasicAuthentication(connection, username, password);
        
        RestTask task = new RestTask(connection);
        return task;
    }
    
    public static RestTask obtainFormPostTask(String url,
            List formData) throws MalformedURLException,
            IOException {
        HttpURLConnection connection = (HttpURLConnection) (new URL(url))
                .openConnection();

        connection.setReadTimeout(10000);
        connection.setConnectTimeout(15000);
        connection.setDoOutput(true);

        RestTask task = new RestTask(connection);
        task.setFormBody(formData);

        return task;
    }
    
    public static RestTask obtainAuthenticatedFormPostTask(String url,
            List formData, String username, String password) throws MalformedURLException,
            IOException {
        HttpURLConnection connection = (HttpURLConnection) (new URL(url))
                .openConnection();

        connection.setReadTimeout(10000);
        connection.setConnectTimeout(15000);
        connection.setDoOutput(true);
        
        attachBasicAuthentication(connection, username, password);

        RestTask task = new RestTask(connection);
        task.setFormBody(formData);

        return task;
    }

    public static RestTask obtainMultipartPostTask(String url,
            List formPart, File file, String fileName)
            throws MalformedURLException, IOException {
        HttpURLConnection connection = (HttpURLConnection) (new URL(url))
                .openConnection();

        connection.setReadTimeout(10000);
        connection.setConnectTimeout(15000);
        connection.setDoOutput(true);

        RestTask task = new RestTask(connection);
        task.setFormBody(formPart);
        task.setUploadFile(file, fileName);

        return task;
    }

    private static void attachBasicAuthentication(URLConnection connection, String username, String password) {
        //添加基本授权头
        String userpassword = username + ":" + password;
        String encodedAuthorization = Base64.encodeToString(userpassword.getBytes(), Base64.NO_WRAP);
        connection.setRequestProperty("Authorization", "Basic "+
              encodedAuthorization);
    }
}

基本授权是作为头字段加入到一个HTTP请求中的,该头字段包括属性名称“Authorization”、“Basic”的值加上用户名和密码的Base64编码字符串。attachBasicAuthentication()辅助方法在URLConnection赋给RestTask之前会将上述属性名和编码字符串设置到URLConnection上。添加Base64.NO_WRAP标志以确保编码器没有新增额外的行,这会创建无效的值。
当不是所有请求都使用相同方式授权时,直接在请求中添加授权是一种非常好的方式。但是,有时候只需要设置一次认证信息即可让所有的请求使用它们。这时候就需要Authenticator类了。Authenticator允许为应用程序进程的所有的请求使用它们。这时候就需要Authenticator类了。Authenticator允许为应用程序进程的所有请求设置全局的用户名和密码认证信息。让我们看一下以下代码,它就实现了这个功能。
使用了Authenticator类的Activity

public class AuthActivity extends Activity implements ResponseCallback {

    private static final String URI = "http://httpbin.org/basic-auth/android/recipes";
    private static final String USERNAME = "android";
    private static final String PASSWORD = "recipes";
    
    private TextView mResult;
    private ProgressDialog mProgress;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mResult = new TextView(this);
        setContentView(mResult);
        
        Authenticator.setDefault(new Authenticator() {
            @Override
            protected PasswordAuthentication getPasswordAuthentication() {
                return new PasswordAuthentication(USERNAME, PASSWORD.toCharArray());
            }
        });
        
        try {
            RestTask task = RestUtil.obtainGetTask(URI);
            task.setResponseCallback(this);
            task.execute();
        } catch (Exception e) {
            mResult.setText(e.getMessage());
        }
        
//        try {
//            RestTask task = RestUtil.obtainAuthenticatedGetTask(URI, USERNAME, PASSWORD);
//            task.setResponseCallback(this);
//            task.execute();
//        } catch (Exception e) {
//            mResult.setText(e.getMessage());
//        }
    }
    
    @Override
    public void onRequestSuccess(String response) {
        if (mProgress != null) {
            mProgress.dismiss();
            mProgress = null;
        }
        mResult.setText(response);
    }

    @Override
    public void onRequestError(Exception error) {
        if (mProgress != null) {
            mProgress.dismiss();
            mProgress = null;
        }
        mResult.setText(error.getMessage());
    }
}

该例会再次连接httpbin,但本次连接的站点需要使用有效的认证信息。主机需要的用户名和密码是被编码到URL路径中的,如果没有提供相应的认证信息属性的话,主机的响应结果为UNAUTHORIZED。
只需要调用一次Authenticator.setDefault()方法并传入一个新的Authenticator实例,后续所有的请求在认证时就会使用上面提供的认证信息。所以我们在请求时创建
一个新的PasswordAuthentication实例以将正确的用户名和密码传入Authenticator类,这样进程中的所有的URLConnection实例都会使用它。注意,这个示例中,请求并没有关联认证信息,但执行请求后会得到一个已经认证的响应。

5.响应缓存

(API Level 13)
HttpURLConnection最后一个可以利用的平台改进方案是通过HttpResponseCache设置响应缓存。加快应用程序响应速度的方法就是将从远程主机获取的响应缓存起来。这样对与频繁使用的请求就可以直接从缓存中加载而不必每次都访问网络。在应用程序中安装和移除缓存只需要简单的几行代码。
这些安装和移除缓存的方法只需要调用一次,因此可以将它们放在Activty或每个请求范围外的应用级类中:

        //安装响应缓存
        try{
            File httpCacheDir = new File(context.getCacheDir(),"http");
            long httpCacheSize = 10*1024*1024;  //10MiB
            HttpResponseCache.install(httpCacheDir,httpCacheSize);
        }catch (IOException e){
            Log.e(TAG,"HTTP response cache installation failed:" + e);
        }
        //清空响应缓存
        HttpResponseCache cache = HttpResponseCache.getInstalled();
        if (cache != null){
            cache.flush();
        }

注意:
HttpResponseCache只能应用于HttpURLConnection的变体,它并不适用于ApacheHttpClient。

你可能感兴趣的:(6.访问REST API)