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()
を呼び出す。ここではこのStreamingOutput
のwrite()
を実装することによりストリーミング処理を実現している。
なお、最初のコード例では全体を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
情報を扱えるようになる。
[田中 靖也]