Android組込みのHttpComponent(HttpClient)の正しい使い方といくつかのtips

ブログ等に掲載されているHttpComponentのサンプルコードは、重要なところが端折られて紹介されている(というか間違っている事を知らずに書いている疑惑すらある)ことが多いので、正しいサンプルコードを書いておく。
まぁ、ここだけでなくApache HttpComponentsのドキュメントもちゃん読みましょう。あ、Androidのリファレンスにはロクに使い方が書いてないので、あんなゴミだけ読んでてもダメですよ。

要点

ポイントは2つ。

ResponseHandlerを使ってコードを書く

HttpResponseの内部リソースを自動で解放してくれるので、ミスがなくなり、コードも簡潔になる。ブログ等ではHttpResponseを使わないコードもよく掲載されているが、リソースの解放処理が記述されていないことが多いのであまりよろしくない。
なお、ResponseHandlerを使わずに自分でリソースを開放する場合のサンプルコードは、HttpComponents HttpClient ExamplesManual connection releaseにあるが、これを見ると煩雑なコードになっていることが判ると思う。

HttpClientを使い終わったら必ずgetConnectionManager().shutdown()する

これを忘れるとスレッドプールが開放されず、メモリリークの原因となる。

サンプルコード(基本形)

GET

ATND APIでイベントを検索して、レスポンスを文字列として返す例を掲載する。

// GET Requestを構築する。
// Uri.Builderを使うとURIエンコードも適切にやってくれる。
Uri.Builder builder = new Uri.Builder();
builder.scheme("http");
builder.encodedAuthority("api.atnd.org");
builder.path("/events/");
builder.appendQueryParameter("keyword", "terurou");
builder.appendQueryParameter("format", "xml");

HttpGet request = new HttpGet(builder.build().toString());

// HttpClientインタフェースではなくて、実クラスのDefaultHttpClientを使う。
// 実クラスでないとCookieが使えないなど不都合が多い。
DefaultHttpClient httpClient = new DefaultHttpClient();

try {
    String result = httpClient.execute(request, new ResponseHandler<String>() {
        @Override
        public String handleResponse(HttpResponse response)
                throws ClientProtocolException, IOException {
            
            // response.getStatusLine().getStatusCode()でレスポンスコードを判定する。
            // 正常に通信できた場合、HttpStatus.SC_OK(HTTP 200)となる。
            switch (response.getStatusLine().getStatusCode()) {
            case HttpStatus.SC_OK:
                // レスポンスデータを文字列として取得する。
                // byte[]として読み出したいときはEntityUtils.toByteArray()を使う。
                return EntityUtils.toString(response.getEntity(), "UTF-8");
            
            case HttpStatus.SC_NOT_FOUND:
                throw new RuntimeException("データないよ!"); //FIXME
            
            default:
                throw new RuntimeException("なんか通信エラーでた"); //FIXME
            }

        }
    });

    // logcatにレスポンスを表示
    Log.d("test", result);
} catch (ClientProtocolException e) {
    throw new RuntimeException(e); //FIXME
} catch (IOException e) {
    throw new RuntimeException(e); //FIXME
} finally {
    // ここではfinallyでshutdown()しているが、HttpClientを使い回す場合は、
    // 適切なところで行うこと。当然だがshutdown()したインスタンスは通信できなくなる。
    httpClient.getConnectionManager().shutdown();
}

この例ではResponseHandlerのようにStringを型パラメータとして指定して、httpClient.execute()の戻り値でStringを返している。戻り値が不要な場合は、ResponseHandlerと指定して、handleResponse()の処理の最後にreturn null;とすればよい。

また蛇足であるが、この例のようにレスポンスを文字列として取得するだけであれば、BasicResponseHandlerを使うともっとコード量を減らすことができる。ただし、小回りが利かないので(HTTPレスポンスコードをハンドリングしたい場合など)、個人的にはあまり使う機会がないかなぁと思う。


[追記]より確実でミスの少ないコードにするため、レスポンスを読み出す処理をEntityUtilsを使った処理に修正した。どうしてもInputStreamを直接扱いたい場合は、以下のように記述する。finallyでInputStreamをclose()したりと、いたって普通のコードだが、念のため。

// レスポンスデータを取得
InputStream content = response.getEntity().getContent();

// レスポンスデータを文字列に変換
BufferedReader reader = new BufferedReader(new InputStreamReader(content));
try {
    StringBuilder buf = new StringBuilder();
    String line;
    while ((line = reader.readLine()) != null) {
        buf.append(line);
    }
    return buf.toString();
} finally {
    // レスポンスデータ(InputStream)を閉じる
    content.close();
    reader.close();
}

POST
// POST Requestを構築する。
HttpPost request = new HttpPost("http://POSTを使う適当なWebAPI見つからんかった.com");

List<NameValuePair> params = new ArrayList<NameValuePair>();
params.add(new BasicNameValuePair("userId", "user-id"));
params.add(new BasicNameValuePair("password", "password"));
request.setEntity(new UrlEncodedFormEntity(params));

// 以下、GETと同じなので省略する。
// httpClient.execute(request,  new ResponseHandler<String>() { ... のように記述すれば良い。

tips

Cookie操作
// Cookieの登録
BasicClientCookie cookie = new BasicClientCookie("key", "value");
cookie.setDomain("domain");
cookie.setPath("/");
httpClient.getCookieStore().addCookie(cookie);

// Cookieの取得
List<Cookie> cookies = httpClient.getCookieStore().getCookies();

// Cookieの消去
httpClient.getCookieStore().clear();
ファイルアップロード

"Andoridでmultipart/form-dataのPOST"の続き - ryopeiの日記を参照。
apache-mime4j-0.6.jarとhttpmime-4.0.1.jarの2つのJAR追加すれば良い。HttpComponent Core自体のJAR(httpclientとかhttpcoreとか)を追加しろと解説してるブログも散見されるが、追加は不要である。

複数のスレッドから単一のHttpClientを使いまわす

ThreadSafeClientConnManagerを使えば良い。利用する場合は必ずHttpComponent本家ドキュメントを参照のこと。

// HttpClientの構築
SchemeRegistry schreg = new SchemeRegistry();
schreg.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
schreg.register(new Scheme("https", SSLSocketFactory.getSocketFactory(), 443));

HttpParams params = new BasicHttpParams();

ThreadSafeClientConnManager connManager = new ThreadSafeClientConnManager(params, schreg);
DefaultHttpClient httpClient = new DefaultHttpClient(connManager, params);
通信タイムアウト時間やらのパラメータ設定

HttpConnectionParamsとHttpProtocolParamsで設定できる。ここでは前述のThreadSafeClientConnManagerのサンプルにパラメータ設定を追加した例を示す。
これについても設定パラメータの詳細についてはHttpComponent本家ドキュメントを参照のこと。

SchemeRegistry schreg = new SchemeRegistry();
schreg.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
schreg.register(new Scheme("https", SSLSocketFactory.getSocketFactory(), 443));

HttpParams params = new BasicHttpParams();
HttpConnectionParams.setSocketBufferSize(params, 4096);   //ソケットバッファサイズ 4KB
HttpConnectionParams.setSoTimeout(params, 20000);         //ソケット通信タイムアウト20秒
HttpConnectionParams.setConnectionTimeout(params, 20000); //HTTP通信タイムアウト20秒
HttpProtocolParams.setContentCharset(params, "UTF-8");       //文字コードUTF-8と明示
HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1); //HTTP1.1

ThreadSafeClientConnManager connManager = new ThreadSafeClientConnManager(params, schreg);
DefaultHttpClient httpClient = new DefaultHttpClient(connManager, params);
gzip対応

面倒なことに標準では対応してないので、自分でgzipを展開する必要がある。

// リクエスト送信時にgzipを有効化する(HTTP Headerを付加)
HttpGet request = new HttpGet("http://なんか適当なURLをいれてね.net");
request.setHeader("Accept-Encoding", "gzip, deflate");

// gzipされているレスポンスはGZIPInputStreamに食わせる
httpClient.execute(request, new ResponseHandler<T>() {
    @Override
    public T handleResponse(HttpResponse response) throws ClientProtocolException, IOException {
        InputStream stream = null;
        if (isGZipHttpResponse(response)) {
            stream =  new GZIPInputStream(response.getEntity().getContent());
        } else {
            stream = response.getEntity().getContent();
        }

        // 以下、streamを読む処理が続く
    }
});

gzipが有効か判定する処理は以下のようなコードとなる。

private boolean isGZipHttpResponse(HttpResponse response) {
    Header header = response.getEntity().getContentEncoding();
    if (header == null) return false;
    
    String value = header.getValue();
    return (!TextUtils.isEmpty(value) && value.contains("gzip"));
}

ただ、毎回自分でgzipが有効化を判定するのは非常にだるいので、以下のようなgzipなレスポンスを透過的に扱うことができるラッパーを用意すると楽である。

// HttpEntityのラッパー
public class GZIPHttpEntity implements HttpEntity {
    private HttpEntity entity;

    public GZIPHttpEntity(HttpEntity entity) {
        this.entity = entity;
    }
    
    @Override
    public InputStream getContent() throws IOException, IllegalStateException {
        return new GZIPInputStream(entity.getContent());
    }

    // 以下、HttpEntityのメソッドを呼ぶだけのproxyコード
// // HttpResponseのラッパー
class GZIPHttpResponse implements HttpResponse {
    private HttpResponse response;

    public GZIPHttpResponse(HttpResponse response) {
        this.response = response;
    }
    
    @Override
    public HttpEntity getEntity() {
        return new GZIPHttpEntity(response.getEntity());
    }

    // 以下、HttpResponseのメソッドを呼ぶだけのproxyコード
// HttpClientのラッパー
public <T> T execute(HttpUriRequest request, final ResponseHandler<T> handler) {
    try {
        return httpClient.execute(request, new ResponseHandler<T>() {
            @Override
            public T handleResponse(HttpResponse response) throws ClientProtocolException, IOException {
                if (isGZipHttpResponse(response)) {
                    return handler.handleResponse(new GZIPHttpResponse(response));
                } else {
                    return handler.handleResponse(response);
                }
            }
        });
    } catch (ClientProtocolException e) {
        throw new HttpException(e);
    } catch (IOException e) {
        throw new HttpException(e);
    }
}

private boolean isGZipHttpResponse(HttpResponse response) {
    Header header = response.getEntity().getContentEncoding();
    if (header == null) return false;
    String value = header.getValue();
    return (!TextUtils.isEmpty(value) && value.contains("gzip"));
}
画像を読み込んでBitmapクラスとして取得する

tipsとして書くほどでもないかも。 BitmapFactory.decodeStream()を使うだけ。

HttpGet request = new HttpGet("http://適当な画像のURI");

Bitmap bitmap = httpClient.execute(request, new ResponseHandler<T>() {
    @Override
    public T handleResponse(HttpResponse response) throws ClientProtocolException, IOException {
        switch (response.getStatusLine().getStatusCode()) {
        case HttpStatus.SC_OK:
            return BitmapFactory.decodeStream(response.getEntity().getContent());
        case HttpStatus.SC_NOT_FOUND:
            throw new RuntimeException("データないよ!"); //FIXME
        default:
            throw new RuntimeException("なんか通信エラーでた"); //FIXME
        }
    }
});