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

エンタープライズギークス (Enterprise Geeks)

企業システムの企画・開発に携わる技術者集団のブログです。開発言語やフレームワークなどアプリケーション開発に関する各種情報を発信しています。ウルシステムズのエンジニア有志が運営しています。

JAX-RSを利用して大量データを効率的に配信する方法

今回の記事では、JAX-RSを利用して大量データを効率的に配信する方法を紹介する。

JAX-RSはRESTアーキテクチャーに基づくWEBサービスの機能を提供するAPIであり、参照実装の1つとしてOracle社が提供するJerseyがある。

このJerseyを利用して、データベース等への問合せ結果を返すREST APIを素直に書くと、以下のようになる。

@Path("/persons")
public class Persons {
  @GET
  @Produces(MediaType.APPLICATION_JSON)
  public List<Person> getAllPersons() {
    List<Person> persons = new ArrayList<>();
    /* データベースへの問合せ処理は省略 */
    while (resultSet.next()) { 
      Person person = new Person();
      // resultSetからPerson情報を取り出す
      String name = resultSet.getString("name");
      person.setName(name);
      persons.add(person);
    }
    // レスポンスを返す
    return persons;
  }
}

上記のコードで使っているJAX-RSアノテーションについて簡単に説明しておこう。

クラス宣言に指定する@PathはリソースのURIを定義するもので、上記の場合は

http://[IPアドレス]:[ポート番号]/persons

へのリクエストとなる。

@GETはHTTP GETメソッドに応答することを表し、@ProducesではレスポンスのコンテンツのMIMEタイプがJSONであることを指定している。

このコードでは、読み込んだPerson情報をメモリー上にすべて展開して処理している。対象のデータが少なければ問題にならないが、大量データだった場合にはOutOfMemoryErrorが発生してしまう可能性がある。

この問題の回避策の一つとして、OutputStreamを利用する方法がある。

StreamingOutputを利用したストリーミング

レスポンスとしてOutputStreamを返すには、JAX-RSが用意しているStreamingOutputを利用すればよい。
コードは次のようになる。

@Path("/persons")
public class Persons {
  @GET
  @Produces(MediaType.TEXT_PLAIN)
  public Response getAllPersons() {
    StreamingOutput stream = new StreamingOutput() {
      @Override
      public void write(OutputStream out)
          throws IOException, WebApplicationException {
        BufferedWriter writer
          = new BufferedWriter(new OutputStreamWriter(out, "UTF-8"));
        try {
          /* データベースへの問合せ処理は省略 */

          /* JAXBの詳細に関しては省略 */
          jaxbContext
            = JAXBContextFactory.createContext(new Class[]{Person.class},
                                               null);
          Marshaller marshaller = jaxbContext.createMarshaller();
          // JSON形式への変換設定
          marshaller.setProperty(MarshallerProperties.MEDIA_TYPE,
                                 MediaType.APPLICATION_JSON);
          // JSON形式変換時に改行を行わない設定
          marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT,
                                 false);
          
          Person person = new Person();
          // データベースへPersonに関する情報を問合せしResultSetを取得
          while (resultSet.next()) { 
            // resultSetからPerson情報を取り出す
            String name = resultSet.getString("name");
            person.setName(name);

            // JAXBを利用しPersonオブジェクトをJSON文字列へ変換する
            StringWriter stringWriter = new StringWriter();
            marshaller.marshal(person, stringWriter);
            // JSON形式のPerson情報を書き込む
            writer.write(stringWriter.toString());
            writer.newLine();
          }
          writer.flush();
        } finally {
          writer.close();
        }
      }
    };

    // レスポンスを返す
    return Response.ok(stream)
                   .header("Content-Disposition",
                           "attachment; filename=persons.json")
                   .build();
  }
}

StreamingOutputはレスポンスボディをストリーミング処理したい時に実装するコールバック用インターフェイスである。

JAX-RSではレスポンスボディを出力する際にコールバック関数のwrite()を呼び出す。ここではこのStreamingOutputwrite()を実装することによりストリーミング処理を実現している。

なお、最初のコード例では全体を1つのPerson情報リストで扱うためレスポンスのコンテンツのMIMEタイプをJSONとしていた。しかし、これではレスポンス全体を受取らないと扱えないため、変更後のコードではMIMEタイプをTEXT PLAINとし、1行に1つのPerson情報をJSON形式で出力するようにしている。

オブジェクトからJSON文字列への変換にはJAXBを利用している。変換処理を行うMarshallerの設定では変換形式としてJSON形式を指定し、変換後のJSON文字列がJSONフォーマットにより改行されない設定しておく必要がある。その他JAXBに関しての詳細は割愛する。

OutputStreamレスポンスを受け取るクライアント側

次に、この情報を受け取るクライアント側の仕組みを説明する。
クライアント側では、Jerseyが提供しているJerseyClient APIを使用する。

JSON文字列からオブジェクトへの変換もサーバ側と同様にJAXBを利用し行う。そのために、最初にUnmarshallerの設定を行っておく。

次に、JerseyClient APIを利用するが、ここでは先ほどのサーバ側リソースに対してHTTPメソッドのGETでアクセスする。その際にレスポンスを受け取るクラスをInputStream.classと指定し、InputStreamで受け取る。実はStringで受け取ることもできるが、それでは一度に全てのレスポンスを受取ることになるため、サーバ側と同様にクライアント側でOutOfMemoryErrorを起こす可能性が出てしまう。InputStreamで受け取ることで、メモリ使用量を抑止できる。

続いてBufferedReaderを利用して、行単位でJSON文字列を取得する。その後JAXBを利用することで、サーバ側とは逆にJSON文字列からPersonオブジェクトへデータをバインドできる。

コードは次のようになる。

public class ClientExample {
  public static void main(String[] args) {
    /* JAXBの詳細に関しては省略 */
    JAXBContext jaxbContext
      = JAXBContextFactory.createContext(new Class[]{Person.class}, 
                                         null);
    Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
    // JSON形式からの変換設定
    unmarshaller.setProperty(UnmarshallerProperties.MEDIA_TYPE, 
                             MediaType.APPLICATION_JSON);
    // サーバ側リソースに対してHTTPメソッドGETでアクセスする
    WebTarget webTarget 
      = ClientBuilder.newClient()
                     .target("http://IPアドレス:ポート番号/persons");
    InputStream in = webTarget.request().get(InputStream.class);
    // InputStreamからBufferedReaderを生成する部分に関しては
    // apache.commonsのIOUtilsを利用すると簡単に実装できる
    LineIterator it = IOUtils.lineIterator(in, "UTF-8");
    while (it.hasNext()) {
      String line = it.nextLine();
      // JAXBを利用しJSON文字列からPersonオブジェクトへ変換する
      StreamSource json = new StreamSource(new StringReader(line));
      JAXBElement<?> jaxbElement
        = unmarshaller.unmarshal(json, Person.class);
      Person person = (Person) jaxbElement.getValue();
      System.out.println(person.getName());
    }
  }
}

このように、渡されたデータをJSON文字列からオブジェクトにバインドすることで、クライアント側で簡単にPerson情報を扱えるようになる。

[田中 靖也]