0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-03-12

以下の記事の続きです。

目的

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

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

GOAL

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

POSTメソッド

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

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

EmployeeResource.java
    @Context
    UriInfo uriInfo;

    @POST
    @Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
    public Response addEmployee(Employee employee) {
        EmployeeRepository.getInstance().insert(employee.getId(), employee.getFirstName());

        UriBuilder builder = uriInfo.getAbsolutePathBuilder();
        builder.path(String.valueOf(employee.getId()));
        return Response.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})
    public Response updateEmployee(Employee employee) {
        EmployeeRepository.getInstance().update(employee.getId(), employee.getFirstName());
        // 新規作成した場合はcreatedを返す必要があるが、このサンプルではエラーとするため、常にokを返す
        return Response.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})
    public Response deleteEmployee(@PathParam("id") int id) {
        EmployeeRepository.getInstance().delete(id);
        // Entityの状態を返す場合はokを返す。
        // 受け付けたが処理が終わっていない場合は(キューに乗っただけなど)acceptedを返す
        // このサンプルでは削除が完了して該当コンテントがなくなったことだけ返す
        return Response.noContent().build();
    }

2.動作確認

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

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

例外ハンドリング

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

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

NotFoundExceptionHandler.java
@Provider
public class NotFoundExceptionHandler implements ExceptionMapper<EmployeeNotFoundException> {

    public Response toResponse(EmployeeNotFoundException ex) {
        return Response.status(Response.Status.NOT_FOUND).build();
    }
}

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

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

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

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

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

DuplicateExceptionHandler.java
@Provider
public class DuplicateExceptionHandler implements ExceptionMapper<DuplicateIdException> {

    public Response toResponse(DuplicateIdException ex) {
        return Response.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
@XmlRootElement
public class Employee {
...

これだけです。

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})
    public void getEmployee(int id) {
        String urlPath = String.format("/employees/%d", id);
        Employee employee = target(urlPath).request().get(Employee.class);
        Employee expect = 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})
    public void getAll(String mediaType) {
        final Response response = target("/employees/all").request().accept(mediaType).get();
        assertThat(response.getHeaderString("Content-Type"))
                .isEqualTo(mediaType);
        ...
    }

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

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

EmployeeResourceTest.java
    @ParameterizedTest
    @MethodSource("getParamProvider")
    public void getEmployee(int id, String mediaType) {
        String urlPath = String.format("/employees/%d", id);
        final Response response = target(urlPath).request().accept(mediaType).get();
        assertThat(response.getHeaderString("Content-Type"))
                .isEqualTo(mediaType);

        Employee employee = response.readEntity(Employee.class);
        Employee expect = EmployeeRepository.getInstance().select(id);
        assertThat(employee).isEqualToComparingFieldByField(expect);
    }

    static Stream<Arguments> getParamProvider() {
        return Stream.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")
    public void addEmployee(int id, String bodyRaw, String mediaType) {

        final Response response = target("/employees").request()
                .post(Entity.entity(bodyRaw, mediaType));
        assertThat(response.getStatus()).isEqualTo(201);
        assertThat(response.getHeaderString("Location"))
                .isEqualTo("http://localhost:9998/employees/" + id);
    }

    static Stream<Arguments> postRawProvider() {
        final String json = "{\"firstName\":\"Honeycomb\",\"id\":11}";
        final String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
                "<employee><firstName>KitKat</firstName><id>19</id></employee>";
        return Stream.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")
    public void updateEmployee(int id, String bodyRaw, String mediaType) {
        final Response response = target("/employees").request()
                .put(Entity.entity(bodyRaw, mediaType));
        assertThat(response.getStatus()).isEqualTo(200);

        Employee employee = target("/employees/"+id).request().get(Employee.class);
        Employee expected = EmployeeRepository.getInstance().select(id);
        assertThat(employee).isEqualToComparingFieldByField(expected);
    }

    static Stream<Arguments> putRawProvider() {
        final String json = "{\"firstName\":\"Frozen yogurt\",\"id\":8}";
        final String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
                "<employee><firstName>Cup Cake</firstName><id>3</id></employee>";
        return Stream.of(
                Arguments.of(8, json, MediaType.APPLICATION_JSON),
                Arguments.of(3, xml, MediaType.APPLICATION_XML)
        );
    }

    @Test
    public void deleteEmployee() {
        final Response response = target("/employees/9")
                .request().delete();
        assertThat(response.getStatus()).isEqualTo(204);
    }

3. 例外系

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

(1)コンフィグを追加

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

EmployeeResourceTest.java
    @Override
    protected Application configure() {
        return new ResourceConfig(EmployeeResource.class)
                // 以下を追加
                .register(DuplicateExceptionHandler.class)
                .register(NameNotFoundExceptionHandler.class)
                .register(NotFoundExceptionHandler.class);
    }

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

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

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

EmployeeResourceTest.java
    @Test
    public void exception_selectEmployee(){
        final Response response = target("/employees/1").request().get();
        assertThat(response.getStatus()).isEqualTo(404);
    }

    @Test
    public void exception_searchEmployee(){
        final Response response = target("/employees/search?name=android").request().get();
        assertThat(response.getStatus()).isEqualTo(404);
    }

    @ParameterizedTest
    @MethodSource("putRawProvider")
    public void exception_addEmployee(int id, String bodyRaw, String mediaType) {
        final Response response = target("/employees").request()
                .post(Entity.entity(bodyRaw, mediaType));
        assertThat(response.getStatus()).isEqualTo(409);
    }

    @ParameterizedTest
    @MethodSource("putExceptionProvider")
    public void exception_updateEmployee(int id, String bodyRaw, String mediaType) {
        final Response response = target("/employees").request()
                .put(Entity.entity(bodyRaw, mediaType));
        assertThat(response.getStatus()).isEqualTo(404);
    }
    static Stream<Arguments> putExceptionProvider() {
        final String json = "{\"firstName\":\"Lollipop\",\"id\":21}";
        final String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
                "<employee><firstName>Jelly Bean</firstName><id>17</id></employee>";
        return Stream.of(
                Arguments.of(21, json, MediaType.APPLICATION_JSON),
                Arguments.of(3, xml, MediaType.APPLICATION_XML)
        );
    }

    @Test
    public void exception_deleteEmployee(){
        final Response response = 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

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

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?