読者です 読者をやめる 読者になる 読者になる

Apache HttpClientのソースをScalaで書き換えてみる

社内でやっているScala勉強会で、テストがあるJavaの実装をScalaで書き換えてみようということになり、Apache HttpClientのソースをScalaで書き換えることになりました。
作業の流れは以下のとおりです。

  • 対象とするソースを全員でひとつ選ぶ
  • そのソースのファイルを削除する(コンパイルエラーになる)
  • 各々がScalaで同じクラスを実装し、テストを通す
  • ソースレビュー

今回書き換える対象のソースはorg.apache.http.impl.client.CloseableHttpResponseProxyです。

CloseableHttpResponseProxy

これが実際のJavaの実装です。

/*
 * ====================================================================
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 *
 */

package org.apache.http.impl.client;

import java.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.annotation.NotThreadSafe;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.util.EntityUtils;

/**
 * @since 4.3
 */
@NotThreadSafe
class CloseableHttpResponseProxy implements InvocationHandler {

    private final HttpResponse original;

    CloseableHttpResponseProxy(final HttpResponse original) {
        super();
        this.original = original;
    }

    public void close() throws IOException {
        final HttpEntity entity = this.original.getEntity();
        EntityUtils.consume(entity);
    }

    public Object invoke(
            final Object proxy, final Method method, final Object[] args) throws Throwable {
        final String mname = method.getName();
        if (mname.equals("close")) {
            close();
            return null;
        } else {
            try {
                return method.invoke(original, args);
            } catch (final InvocationTargetException ex) {
                final Throwable cause = ex.getCause();
                if (cause != null) {
                    throw cause;
                } else {
                    throw ex;
                }
            }
        }
    }

    public static CloseableHttpResponse newProxy(final HttpResponse original) {
        return (CloseableHttpResponse) Proxy.newProxyInstance(
                CloseableHttpResponseProxy.class.getClassLoader(),
                new Class<?>[] { CloseableHttpResponse.class },
                new CloseableHttpResponseProxy(original));
    }

}

で、これが私が書き換えたScalaのソースです。

package org.apache.http.impl.client

import java.io.IOException
import java.lang.reflect.{InvocationTargetException, InvocationHandler, Method, Proxy}
import org.apache.http.HttpResponse
import org.apache.http.util.EntityUtils
import org.apache.http.client.methods.CloseableHttpResponse
import org.apache.http.annotation.NotThreadSafe

@NotThreadSafe
class CloseableHttpResponseProxy(val original: HttpResponse) extends InvocationHandler {

  @throws(classOf[IOException])
  def close = EntityUtils.consume(original.getEntity)

  @throws(classOf[Throwable])
  override def invoke(proxy: Object, method: Method, args: Array[Object]): Object = {
    method.getName match {
      case "close" => {
        close
        return null
      }
      case _ => {
        try {
          method.invoke(original, args :_*)
        } catch {
          case e: InvocationTargetException => {
            e.getCause match {
              case cause: Throwable => throw cause
              case _ => throw e
            }
          }
        }
      }
    }
  }
}
object CloseableHttpResponseProxy {
  def newProxy(original: HttpResponse): CloseableHttpResponse = {
    Proxy.newProxyInstance(
      classOf[CloseableHttpResponseProxy].getClassLoader,
      Array[Class[_]](classOf[CloseableHttpResponse]),
      new CloseableHttpResponseProxy(original)).asInstanceOf[CloseableHttpResponse]
  }
}

これがScala的に完璧かはわからないですが、今回勉強になったことをひとつずつ書いていきます。

@throws

Scalaではすべての例外をメソッドの外の投げることができる。
明示的に記載する場合は@throwsアノテーションを使う。

Java
public void close() throws IOException {/** ... */}
Scala
@throws(classOf[IOException])
def close = EntityUtils.consume(original.getEntity)

classOf[T]

JavaのCloseableHttpResponse.classをScalaで書く場合、classOf[T]を使う。

Java
CloseableHttpResponse.class
Scala
classOf[CloseableHttpResponse]

: _*

Method#invokeのところで、Array[Object]を渡しているが、args :_*としないと以下の様な警告がでる。

I assume that the elements of this array should be passed as individual arguments to the vararg.
Therefore I follow the array with a `: _*', to mark it as a vararg argument.
If that's not what you want, compile this file with option -Xno-varargs-conversion.

Javaの可変長引数をとるメソッドにArrayを渡す場合、: _*を付けないと警告となる。
Scalaの可変長引数をとるメソッドに、変数で値を渡す場合には同様の対応をしなければいけないが、Scalaの場合はコンパイルエラーとなる。

Stringの可変長引数を取り、出力するメソッド

scala> def foo(args: String*) {args.foreach(println)}
foo: (args: String*)Unit

scala> foo("foo", "bar", "boo")
foo
bar
boo

これに対して変数で値を渡そうとすると、そのままでは下記のようにエラーになる。

scala> val list = List("foo", "bar", "boo")
list: List[String] = List(foo, bar, boo)

scala> foo(list)
<console>:10: error: type mismatch;
 found   : List[String]
 required: String
              foo(list)
                  ^

なので、: _*をつける。

scala> foo(list: _*)
foo
bar
boo

ちょっとめんどい。
 

Scalaのバージョンによる違い?

error: cannot find symbol class CloseableHttpResponseProxy

最初、pomで定義しているscala-libraryのバージョンの指定が、2.7.2でやっていて、このクラスの呼び出し元でこんなエラーが出ててよくわかんなかったんですが、バージョンを2.10.0にしたら解決できました。

error: unreachable code

e.getCause のmatchのところで、nullのところを_(プレースホルダー)にすると到達不能コードと言われ、コンパイルエラーとなりました。
詳しく調べてないですが、これも2.10.0にすることで解決できました。
 
 

まとめ

こんな感じで、結構短いコードでしたが、いろいろハマったり勉強になったりすることがあるなぁと思いました。
JavaエンジニアがScalaで開発できるようになることを目的とした勉強会なので、JavaScalaに書き直すという単純な作業ですが、とても有意義だと感じました。
時間の都合上、ソースレビューまでは至らなかったので、次回はそこも含めてやれればと思います。