Quantcast
Channel: Postmanタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 470

IntelliJ+JerseyとPostmanでRESTfulAPIサンプル(POST/UPDATE/DELETE,例外処理,XML)

$
0
0

以下の記事の続きです。

目的

GET以外のAPI、例外ハンドリング、XMLでの応答が出来るようにします。
もうあんまりIntelliJ関係ないですけど。

各APIでセットしているステータスコードは、以下のサイトにある推奨のものにしています。

https://restfulapi.net/http-methods/

GOAL

  • POST/PUT(UPDATE)/DELETEが出来る
  • 例外ハンドリングが出来る
    • 500エラーではなくて404(Not Found)や309(Conflict)を返すようにする
  • XMLで応答が出来る

POSTメソッド

1.リソースクラスにAPIメソッドを追加

GETメソッドが@GETだったのだから、当然、POSTメソッドは@POSTを付けます。

EmployeeResource.java
@ContextUriInfouriInfo;@POST@Consumes({MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML})publicResponseaddEmployee(Employeeemployee){EmployeeRepository.getInstance().insert(employee.getId(),employee.getFirstName());UriBuilderbuilder=uriInfo.getAbsolutePathBuilder();builder.path(String.valueOf(employee.getId()));returnResponse.created(builder.build()).build();}

@Contextで、実行環境に関する情報(コンテキスト)を取得できます。
ここでは、(参考にしたページのサンプルのままなのですが)ヘッダーに、追加したデータのアクセスURLを返してあげています。そのため、UriInfoが必要なので使っているようです。

2.動作確認

ここからはPostmanで確認していきます。

  • メソッドタイプを[POST]にする
  • [BODY]タブを選ぶ
  • [raw]を選ぶ
  • ドロップダウンから、[JSON]を選ぶ
  • 下記の文字列を設定する
    • {"firstName":"Honeycomb","id":11}
postman_post_json.png

実行してみてください。

postman_created.png

Statusが201 Createdになっていれば成功です。
また、[Headers]タブをクリックすると、レスポンスヘッダーが見られます。この中に、Locationという項目があり、Urlが入っているはずです。

post_header_location.png

Urlをコピペしてブラウザに貼り付ければ、新しいデータにアクセスできることが分かります。

3.余談

お急ぎの方は飛ばしてドウゾ。

Locationを返している部分ですが、参考にしたサイトは、String.formatを使って次のようにしていました。

String.format("%s/%s",uriInfo.getAbsolutePath().toString()

UnitTestは、これでlocalhost:xxxx/employees/11みたいに返ってくるのですが、ところが、これをPostmanやターミナルからcurlコマンドで実行すると、localhost:xxxx/empoloyees//11みたいに最後のパスセパレータ(/)がなぜか重複してしまい、当然そのままURLをコピペしたのではアクセス出来ません。

URLEncodeがらみと思って調査&試しましたが、どうやら違うようでした。
色々試行錯誤して、結局、上記の通り、UriBuilderを使うことで解決しました。

というお話でした(笑)

あとは、これまではブラウザアクセスしかしていなくて、「POSTするにはどうするんだ?クライアントアプリ作らなきゃならんの?」と思っていました。絶対ツールがあるはずだと思って探し、Postmanを知ったのは実はこの時でした(笑)

PUT(UPDATE)メソッド

1.リソースクラスにAPIメソッドを追加

GETメソッドが~~略

EmployeeResource.java
@PUT@Consumes({MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML})publicResponseupdateEmployee(Employeeemployee){EmployeeRepository.getInstance().update(employee.getId(),employee.getFirstName());// 新規作成した場合はcreatedを返す必要があるが、このサンプルではエラーとするため、常にokを返すreturnResponse.ok().build();}

2.動作確認

  • Postmanで、メソッドタイプを[PUT]を選んで、jsonを入力
    • {"firstName":"Frozen yogurt","id":8}

Statusは200になればOKです。
続いて/allなどを叩いてみれば、該当のデータが変更されているのがわかります。

DELETEメソッド

1.リソースクラスにAPIメソッドを追加

GETメソッドが~~略

EmployeeResource.java
@DELETE@Path("/{id}")@Consumes({MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML})publicResponsedeleteEmployee(@PathParam("id")intid){EmployeeRepository.getInstance().delete(id);// Entityの状態を返す場合はokを返す。// 受け付けたが処理が終わっていない場合は(キューに乗っただけなど)acceptedを返す// このサンプルでは削除が完了して該当コンテントがなくなったことだけ返すreturnResponse.noContent().build();}

2.動作確認

  • Postmanで、メソッドタイプを[DELETE]を選んで、パスを入力
postman_delete.png

Statusは204になればOKです。
続けて/allを叩いてみると、id=3が無くなっているはずです。

例外ハンドリング

さて、例外クラスをたくさん作ってきましたが、全く使われていません。いや、使われてはいるのですが、例外が起こると、500(Internal Server Error)が起きてしまいます。これだと、サーバー側でクラッシュしてしまっているかのようです。そうではなくて、単にデータが無いだけなので、404(Not Found)を返すようにしたいです。

これには、ExceptionMapperというのを使います。
次のようなクラスを作ります。

NotFoundExceptionHandler.java
@ProviderpublicclassNotFoundExceptionHandlerimplementsExceptionMapper<EmployeeNotFoundException>{publicResponsetoResponse(EmployeeNotFoundExceptionex){returnResponse.status(Response.Status.NOT_FOUND).build();}}

これは、EmployeeNotFoundExceptionが投げられたのを検知したら、ステータスコード404を返す、というハンドラーになります。

これで実行し、存在しないパスにアクセスしてみてください。
例えば、/employee/1ですね。

404が表示されましたか?以前は、500エラーが返っていたはずです。

これを、例外クラスごとに量産します。
(ああ、kotlinなら同じファイルに全部書けるのに・・・ファイルが無駄に増えなくていいのに・・・)

EmployeeNameNotFoundExceptionの場合も、404エラーでいいでしょう。
DuplicateIdExceptionの時は、409(conflict)を返しましょう。

DuplicateExceptionHandler.java
@ProviderpublicclassDuplicateExceptionHandlerimplementsExceptionMapper<DuplicateIdException>{publicResponsetoResponse(DuplicateIdExceptionex){returnResponse.status(Response.Status.CONFLICT).build();}}

存在するidに対して、POSTをしてみてください。

Statusが409になっていれば正常です。

conflict.png

XMLでの応答

1. どうでもいい愚痴

お急ぎの方は2へドウゾ(笑)

これがむちゃくちゃハマりました。
たぶん、JDKを8以下で実行してきていたら、ハマらなかったことでしょう。
大人の事情で11を使わないといけないので、そこでハマってしまっていました。

Jersey公式のサンプルや、他の解説ページをいくつも見ても、「XMLで応答を返すには、モデルクラス(Entity)に、@XmlRootElementって書いてAcceptapplication/xmlを指定すればいいんだよ」という情報しかなくて、でもそれだけだと、「500 Internal Server Error」になってしまい、何時間もさまよいました(T_T)

最終的に、こちらのページにたどり着いて、ようやく原因が分かりました。

JDKのバージョン別に何が変わったのか、そういえば調べていたのに、全く気づきませんでした(トホホ)
資料に書いたら忘れちゃうの、ダメですねえ。。。

ということで、JAXB関連の依存関係を追加し、モデルクラスに@XmlRootElementを付けるだけです。

2. 依存関係の追加

pom.xml<dependencies>下に次のように追加します。

pom.xml
<!-- https://mvnrepository.com/artifact/javax.xml.bind/jaxb-api --><dependency><groupId>javax.xml.bind</groupId><artifactId>jaxb-api</artifactId><version>2.3.1</version></dependency><!-- https://mvnrepository.com/artifact/javax.activation/activation --><dependency><groupId>javax.activation</groupId><artifactId>activation</artifactId><version>1.1.1</version></dependency><!-- https://mvnrepository.com/artifact/org.glassfish.jaxb/jaxb-runtime --><dependency><groupId>org.glassfish.jaxb</groupId><artifactId>jaxb-runtime</artifactId><version>2.3.2</version></dependency>

3. XMLエレメントを示すアノテーションの追加

Employeeクラスにアノテーションを付けます。

Employee.java
@XmlRootElementpublicclassEmployee{...

これだけです。

4. 動作確認

PostmanでAcceptタイプを指定して試してみましょう。

(1)application/jsonを指定

postman-accept-json.png

(2)application/xmlを指定

postman-accept-xml.png

なお、ブラウザ(Chrome)だと、xmlがデフォルトで返ってくるようですが、PostmanはAcceptタイプを指定しない場合はjsonで返ってくるようです。

他のメソッドについても試してみてください。

JUnitテスト

ここまでのJUnitテストも書いておきます。
それと、せっかくJUnit5を使っているので、GETのテストをパラメタライズテストにしてみようと思います。
さらに、xmlも受け取れるようになったので、そのテストを追加します。

1. パラメタライズテスト

パラメタライズテストとは、パラメータの配列を渡して、パラメータ違いのテストを繰り返し行える機能のことを言います。
書き方は簡単。

  • @Testから@ParameterizedTestにアノテーションを変える
  • @ValueSourceでパラメータの配列を渡す

簡単ですね。もう少し色々複雑なこともできるのですが、とりあえずこれができれば十分かと思います。

EmployeeResourceTest.java
@ParameterizedTest@ValueSource(ints={3,4,5,8,9})publicvoidgetEmployee(intid){StringurlPath=String.format("/employees/%d",id);Employeeemployee=target(urlPath).request().get(Employee.class);Employeeexpect=EmployeeRepository.getInstance().select(id);assertThat(employee).isEqualToComparingFieldByField(expect);}

これで、引数id{3,4,5,8,9}という値を順番に使って繰り返しテストをしてくれます。
実行すると、こんな結果表示がされます。
リストが見えない場合は、チェックアイコンをクリックしてみてください。
parametrize_test.png

繰り返しのテストを書くのが楽になりました。

2. XMLのテスト

(1)GET系

GET系は、Accept別にちゃんと通過するかテストを追加します。これもまた、パラメタライズテストで出来ます。

EmployeeResourceTest.java
@ParameterizedTest@ValueSource(strings={MediaType.APPLICATION_JSON,MediaType.APPLICATION_XML})publicvoidgetAll(StringmediaType){finalResponseresponse=target("/employees/all").request().accept(mediaType).get();assertThat(response.getHeaderString("Content-Type")).isEqualTo(mediaType);...}

そうしたら、getEmployeeのテストも、jsonの時とxmlの時で分けたいですね。
すると、複数のテストパラメーターが必要になってきます。

こういう時には、MethodSourceを使ってパラメータを作って渡します。

EmployeeResourceTest.java
@ParameterizedTest@MethodSource("getParamProvider")publicvoidgetEmployee(intid,StringmediaType){StringurlPath=String.format("/employees/%d",id);finalResponseresponse=target(urlPath).request().accept(mediaType).get();assertThat(response.getHeaderString("Content-Type")).isEqualTo(mediaType);Employeeemployee=response.readEntity(Employee.class);Employeeexpect=EmployeeRepository.getInstance().select(id);assertThat(employee).isEqualToComparingFieldByField(expect);}staticStream<Arguments>getParamProvider(){returnStream.of(Arguments.of(3,MediaType.APPLICATION_JSON),Arguments.of(4,MediaType.APPLICATION_JSON),Arguments.of(5,MediaType.APPLICATION_JSON),Arguments.of(8,MediaType.APPLICATION_JSON),Arguments.of(9,MediaType.APPLICATION_JSON),Arguments.of(3,MediaType.APPLICATION_XML),Arguments.of(4,MediaType.APPLICATION_XML),Arguments.of(5,MediaType.APPLICATION_XML),Arguments.of(8,MediaType.APPLICATION_XML),Arguments.of(9,MediaType.APPLICATION_XML));}

parameterized_matrix.png

まあこんなに必要はないですが、MethodSourceのサンプルということで。

(2)POST

POSTのテストも、jsonをPOSTした場合とxmlをPOSTした場合のテストが必要ですが、これもパラメタライズで出来てしまいます。
ただ、気をつけないといけないのは、セッションが切れていないようなので、staticなEmployeeRepositoryには追加されたデータが残ります。重複追加はエラーになる仕様にしているので、同じ物を追加はできないということに注意して、プロバイダーを作る必要があります。

EmployeeResourceTest.java
@ParameterizedTest@MethodSource("postRawProvider")publicvoidaddEmployee(intid,StringbodyRaw,StringmediaType){finalResponseresponse=target("/employees").request().post(Entity.entity(bodyRaw,mediaType));assertThat(response.getStatus()).isEqualTo(201);assertThat(response.getHeaderString("Location")).isEqualTo("http://localhost:9998/employees/"+id);}staticStream<Arguments>postRawProvider(){finalStringjson="{\"firstName\":\"Honeycomb\",\"id\":11}";finalStringxml="<?xml version=\"1.0\" encoding=\"UTF-8\"?>"+"<employee><firstName>KitKat</firstName><id>19</id></employee>";returnStream.of(Arguments.of(11,json,MediaType.APPLICATION_JSON),Arguments.of(19,xml,MediaType.APPLICATION_XML));}

追加するid,jsonまたはxmlの文字列,MediaTypeを引数で渡しています。

3. PUT/DELETEのテスト

PUT, DELETEのテストを追加します。PUTはjsonとxmlの両方をテストします。

該当箇所は全部でこうなります。

EmployeeResourceTest.java
@ParameterizedTest@MethodSource("putRawProvider")publicvoidupdateEmployee(intid,StringbodyRaw,StringmediaType){finalResponseresponse=target("/employees").request().put(Entity.entity(bodyRaw,mediaType));assertThat(response.getStatus()).isEqualTo(200);Employeeemployee=target("/employees/"+id).request().get(Employee.class);Employeeexpected=EmployeeRepository.getInstance().select(id);assertThat(employee).isEqualToComparingFieldByField(expected);}staticStream<Arguments>putRawProvider(){finalStringjson="{\"firstName\":\"Frozen yogurt\",\"id\":8}";finalStringxml="<?xml version=\"1.0\" encoding=\"UTF-8\"?>"+"<employee><firstName>Cup Cake</firstName><id>3</id></employee>";returnStream.of(Arguments.of(8,json,MediaType.APPLICATION_JSON),Arguments.of(3,xml,MediaType.APPLICATION_XML));}@TestpublicvoiddeleteEmployee(){finalResponseresponse=target("/employees/9").request().delete();assertThat(response.getStatus()).isEqualTo(204);}

3. 例外系

例外をハンドリング出来るようになったので、これもテストしましょう。

(1)コンフィグを追加

何もしないと、ExceptionMapperはテスト中には実行されません。
次のようにテストのコンフィグに追加します。

EmployeeResourceTest.java
@OverrideprotectedApplicationconfigure(){returnnewResourceConfig(EmployeeResource.class)// 以下を追加.register(DuplicateExceptionHandler.class).register(NameNotFoundExceptionHandler.class).register(NotFoundExceptionHandler.class);}

(2)例外系テストメソッドの作成

  • selectに存在しないidを指定した時
  • searchに存在しない文字列を指定した時
  • postに既存のidを指定した時
  • putに存在しないidを指定した時
  • deleteに存在しないidを指定した時

これくらいでしょうか。
該当テストコードは以下のようになります。

EmployeeResourceTest.java
@Testpublicvoidexception_selectEmployee(){finalResponseresponse=target("/employees/1").request().get();assertThat(response.getStatus()).isEqualTo(404);}@Testpublicvoidexception_searchEmployee(){finalResponseresponse=target("/employees/search?name=android").request().get();assertThat(response.getStatus()).isEqualTo(404);}@ParameterizedTest@MethodSource("putRawProvider")publicvoidexception_addEmployee(intid,StringbodyRaw,StringmediaType){finalResponseresponse=target("/employees").request().post(Entity.entity(bodyRaw,mediaType));assertThat(response.getStatus()).isEqualTo(409);}@ParameterizedTest@MethodSource("putExceptionProvider")publicvoidexception_updateEmployee(intid,StringbodyRaw,StringmediaType){finalResponseresponse=target("/employees").request().put(Entity.entity(bodyRaw,mediaType));assertThat(response.getStatus()).isEqualTo(404);}staticStream<Arguments>putExceptionProvider(){finalStringjson="{\"firstName\":\"Lollipop\",\"id\":21}";finalStringxml="<?xml version=\"1.0\" encoding=\"UTF-8\"?>"+"<employee><firstName>Jelly Bean</firstName><id>17</id></employee>";returnStream.of(Arguments.of(21,json,MediaType.APPLICATION_JSON),Arguments.of(3,xml,MediaType.APPLICATION_XML));}@Testpublicvoidexception_deleteEmployee(){finalResponseresponse=target("/employees/1").request().get();assertThat(response.getStatus()).isEqualTo(404);}

putRawProvider(PUTテスト用)をちょうどいいので使い回しています。
ただし使っているのはexception_addEmployee(POSTの例外テスト)です。

感想

なかなかハマりどころの多い箇所でした。わかってしまえば単純なことだったりするのですが・・・

ここまでのコードを、Githubにアップしました。
https://github.com/le-kamba/JerseySample

次はAPI部分をモジュール分割してみます。
こんな形が理想ですね。

jerseysample
  |- pom.xml
  |- src/main/java/package/xxx/sampleapp
  |- serverapi
  |    |- pom.xml
  |    |- src/main/java/package/yyy/server
  |- repository
       |- pom.xml
       |- src/main/java/package/zzz/repository

依存関係の書き方や実行方法の実験を兼ねているため、リポジトリ層も分けてみます。

参考


Viewing all articles
Browse latest Browse all 470

Trending Articles