1概要

このチュートリアルでは、JAX-RS標準に準拠したフレームワークとしてhttp://cxf.apache.org/[Apache CXF]を紹介します。これは、REST(Representational State Transfer)アーキテクチャパターンに対するJavaエコシステムのサポートを定義しています。

具体的には、RESTful Webサービスを構築して公開する方法、およびサービスを検証するための単体テストを記述する方法について順を追って説明します。

これはApache CXFに関するシリーズの3番目です。

最初の1つ

は、JAX-WSに完全に準拠した実装としてのCXFの使用法に焦点を当てています。リンク:/apache-cxf-with-spring[2番目の記事]には、CXFとSpringの併用方法に関するガイドがあります。


2 Mavenの依存関係

最初に必要な依存関係は

org.apache.cxf:cxf – frontend-

です。

<dependency>
    <groupId>org.apache.cxf</groupId>
    <artifactId>cxf-rt-frontend-jaxrs</artifactId>
    <version>3.1.7</version>
</dependency>

このチュートリアルでは、サーブレットコンテナを使用する代わりに、CXFを使用してWebサービスを公開するための

Server

エンドポイントを作成します。したがって、Maven POMファイルに次の依存関係を含める必要があります。

<dependency>
    <groupId>org.apache.cxf</groupId>
    <artifactId>cxf-rt-transports-http-jetty</artifactId>
    <version>3.1.7</version>
</dependency>

最後に、単体テストを容易にするためにHttpClientライブラリを追加しましょう。

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.2</version>
</dependency>


Here

you

cxf-rt-frontend-jaxrs

依存関係の最新バージョンを見つけることができます。

https://search.maven.org/classic/#search%7Cga%7C1%7Cg%3A%22org.apache.cxf%22%20AND%20a%3A%22cxf-rt-transports-も参照してください。


org.apache.cxfの最新バージョンは、http-jetty%22[このリンク]に含まれています。cxf-rt-transports-http-jetty

アーティファクト。最後に、最新バージョンの

httpclient

がhttps://search.maven.org/classic/#search%7Cga%7C1%7Cg%3A%22org.apache.httpcomponents%22%20AND%20a%3A%22httpclient%22にあります。[ここに]。


3リソースクラスと要求マッピング

簡単な例を実行しましょう。次の2つのリソース

Course



Student

を使用してREST APIを設定します。

私たちは単純に始めて、進むにつれてより複雑な例に向かって進みます。


3.1. リソース

これは

Student

リソースクラスの定義です。

@XmlRootElement(name = "Student")
public class Student {
    private int id;
    private String name;

   //standard getters and setters
   //standard equals and hashCode implementations

}

このクラスのインスタンスはXMLにマーシャリングする必要があることをJAXBに伝えるために

@ XmlRootElement

アノテーションを使用していることに注意してください。

次に、

Course

リソースクラスの定義が来ます。

@XmlRootElement(name = "Course")
public class Course {
    private int id;
    private String name;
    private List<Student> students = new ArrayList<>();

    private Student findById(int id) {
        for (Student student : students) {
            if (student.getId() == id) {
                return student;
            }
        }
        return null;
    }

   //standard getters and setters
   //standard equals and hasCode implementations

}

最後に、ルートリソースであり、Webサービスリソースへのエントリポイントとして機能する

CourseRepository

を実装しましょう。

@Path("course")
@Produces("text/xml")
public class CourseRepository {
    private Map<Integer, Course> courses = new HashMap<>();

   //request handling methods

    private Course findById(int id) {
        for (Map.Entry<Integer, Course> course : courses.entrySet()) {
            if (course.getKey() == id) {
                return course.getValue();
            }
        }
        return null;
    }
}


@ Path

アノテーションの付いたマッピングに注目してください。ここでは、

CourseRepository

がルートリソースなので、

course

で始まるすべてのURLを処理するようにマッピングされています。


@ Produces

アノテーションの値は、クライアントに送信する前に、このクラス内のメソッドから返されたオブジェクトをXMLドキュメントに変換するようサーバーに指示するために使用されます。他のバインディングメカニズムは指定されていないため、ここではデフォルトとしてJAXBを使用します。


3.2. 簡単なデータ設定

これは単純な実装例なので、本格的な永続的なソリューションではなく、インメモリデータを使用しています。

それを念頭に置いて、システムにデータを取り込むための簡単なセットアップロジックを実装しましょう。

{
    Student student1 = new Student();
    Student student2 = new Student();
    student1.setId(1);
    student1.setName("Student A");
    student2.setId(2);
    student2.setName("Student B");

    List<Student> course1Students = new ArrayList<>();
    course1Students.add(student1);
    course1Students.add(student2);

    Course course1 = new Course();
    Course course2 = new Course();
    course1.setId(1);
    course1.setName("REST with Spring");
    course1.setStudents(course1Students);
    course2.setId(2);
    course2.setName("Learn Spring Security");

    courses.put(1, course1);
    courses.put(2, course2);
}

このクラス内のHTTP要求を処理するメソッドについては、次のサブセクションで説明します。


3.3. API – マッピングメソッドのリクエスト

それでは、実際のREST APIの実装に進みましょう。

リソースPOJOに

@ Path

アノテーションを使用して、APIオペレーションの追加を開始します。

API操作がPOJO自体ではなくコントローラーで定義される典型的なSpringプロジェクトでのアプローチとは大きく異なる点を理解することが重要です。


Course

クラス内で定義されたマッピングメソッドから始めましょう:

@GET
@Path("{studentId}")
public Student getStudent(@PathParam("studentId")int studentId) {
    return findById(studentId);
}

簡単に言うと、メソッドは

GET

リクエストを処理するときに呼び出されます。

HTTPリクエストから

studentId

pathパラメータをマッピングする簡単な構文に注目しました。

それから

findById

ヘルパーメソッドを使用して対応する

Student

インスタンスを返します。

次のメソッドは、受信した

Student

オブジェクトを

students

リストに追加することによって、

@ POST

アノテーションで示される

POST

要求を処理します。

@POST
@Path("")
public Response createStudent(Student student) {
    for (Student element : students) {
        if (element.getId() == student.getId() {
            return Response.status(Response.Status.CONFLICT).build();
        }
    }
    students.add(student);
    return Response.ok(student).build();
}

これは、作成操作が成功した場合は

200 OK

応答を、送信された

id

を持つオブジェクトがすでに存在する場合は

409 Conflict

を返します。

また、その値は空の文字列なので、

@ Path

アノテーションをスキップすることもできます。

最後のメソッドは

DELETE

リクエストを処理します。

id

が受信したパスパラメータである

students

リストから要素を削除し、

OK

(200)ステータスの応答を返します。指定された

id

に関連付けられた要素がない場合(削除するものがないことを意味します)、このメソッドは

Not Found

(404)ステータスの応答を返します。

@DELETE
@Path("{studentId}")
public Response deleteStudent(@PathParam("studentId") int studentId) {
    Student student = findById(studentId);
    if (student == null) {
        return Response.status(Response.Status.NOT__FOUND).build();
    }
    students.remove(student);
    return Response.ok().build();
}


CourseRepository

クラスのマッピングメソッドのリクエストに移りましょう。

次の

getCourse

メソッドは、キーが

GET

要求の受け取った

courseId

pathパラメータである

courses

マップ内のエントリの値である

Course

オブジェクトを返します。内部的に、このメソッドは

findById

ヘルパーメソッドにパスパラメータをディスパッチしてその役割を果たします。

@GET
@Path("courses/{courseId}")
public Course getCourse(@PathParam("courseId") int courseId) {
    return findById(courseId);
}

次のメソッドは、受信した

PUT

要求の本文がエントリ値で、

courseId

パラメータが関連キーである

courses

マップの既存のエントリを更新します。

@PUT
@Path("courses/{courseId}")
public Response updateCourse(@PathParam("courseId") int courseId, Course course) {
    Course existingCourse = findById(courseId);
    if (existingCourse == null) {
        return Response.status(Response.Status.NOT__FOUND).build();
    }
    if (existingCourse.equals(course)) {
        return Response.notModified().build();
    }
    courses.put(courseId, course);
    return Response.ok().build();
}

この

updateCourse

メソッドは、更新が成功した場合は

OK

(200)ステータスの応答を返し、何も変更せず、既存のオブジェクトとアップロードされたオブジェクトが同じフィールド値を持つ場合は

Not Modified

(304)応答を返します。指定された

id

を持つ

Course

インスタンスが

courses

マップで見つからない場合、メソッドは

Not Found

(404)ステータスを持つ応答を返します。

このルートリソースクラスの3番目のメソッドは、HTTP要求を直接処理しません。代わりに、リクエストは

Course

クラスに委譲され、そこでリクエストはマッチングメソッドによって処理されます。

@Path("courses/{courseId}/students")
public Course pathToStudent(@PathParam("courseId") int courseId) {
    return findById(courseId);
}

直前に委譲されたリクエストを処理する

Course

クラス内のメソッドを示しました。


4

サーバー

エンドポイント

このセクションでは、前のセクションで説明したリソースを持つRESTful Webサービスを公開するために使用されるCXFサーバーの構築に焦点を当てます。最初のステップは、

JAXRSServerFactoryBean

オブジェクトをインスタンス化し、ルートリソースクラスを設定することです。

JAXRSServerFactoryBean factoryBean = new JAXRSServerFactoryBean();
factoryBean.setResourceClasses(CourseRepository.class);

次に、リソースプロバイダをファクトリBeanに設定して、ルートリソースクラスのライフサイクルを管理する必要があります。すべてのリクエストに同じリソースインスタンスを返すデフォルトのシングルトンリソースプロバイダを使用します。

factoryBean.setResourceProvider(
  new SingletonResourceProvider(new CourseRepository()));

Webサービスが公開されているURLを示すアドレスも設定します。

factoryBean.setAddress("http://localhost:8080/");

これで、

factoryBean

を使用して、着信接続の待機を開始する新しい

server

を作成できます。

Server server = factoryBean.create();

このセクションの上記のコードはすべて

main

メソッドで囲む必要があります。

public class RestfulServer {
    public static void main(String args[]) throws Exception {
       //code snippets shown above
    }
}

このmainメソッドの呼び出しは、セクション6に示されています。


5テストケース

このセクションでは、以前に作成したWebサービスを検証するために使用されたテストケースについて説明します。これらのテストは、最も一般的に使用される4つのメソッド、つまり

GET



POST



PUT

、および

DELETE

のHTTP要求に応答した後でサービスのリソース状態を検証します。


5.1. 準備

まず、

RestfulTest

という名前の2つの静的フィールドがテストクラス内で宣言されています。

private static String BASE__URL = "http://localhost:8080/baeldung/courses/";
private static CloseableHttpClient client;

テストを実行する前に、

client

オブジェクトを作成します。これは、サーバーと通信して後で破棄するために使用されます。

@BeforeClass
public static void createClient() {
    client = HttpClients.createDefault();
}

@AfterClass
public static void closeClient() throws IOException {
    client.close();
}


client

インスタンスはテストケースで使用する準備が整いました。


5.2.

GET

リクエスト

テストクラスでは、Webサービスを実行しているサーバーに

GET

要求を送信するための2つのメソッドを定義します。

最初の方法は、リソース内の

id

を指定して

Course

インスタンスを取得することです。

private Course getCourse(int courseOrder) throws IOException {
    URL url = new URL(BASE__URL + courseOrder);
    InputStream input = url.openStream();
    Course course
      = JAXB.unmarshal(new InputStreamReader(input), Course.class);
    return course;
}

2つ目は、リソースのコースと学生の

__id


sを指定して、

Student__インスタンスを取得することです。

private Student getStudent(int courseOrder, int studentOrder)
  throws IOException {
    URL url = new URL(BASE__URL + courseOrder + "/students/" + studentOrder);
    InputStream input = url.openStream();
    Student student
      = JAXB.unmarshal(new InputStreamReader(input), Student.class);
    return student;
}

これらのメソッドはHTTP

GET

要求をサービスリソースに送信し、次に対応するクラスのインスタンスへのXML応答を非整列化します。両方とも

POST



PUT

、および

DELETE

要求を実行した後にサービスリソースの状態を検証するために使用されます。


5.3.

POST

リクエスト

このサブセクションでは、

POST

リクエストに関する2つのテストケースを取り上げ、アップロードされた

Student

インスタンスが競合を引き起こす場合とそれが正常に作成された場合のWebサービスの動作を説明します。

最初のテストでは、クラスパス上の

conflict

student.xml

ファイルから非整列化された

Student__オブジェクトを使用します。内容は次のとおりです。

<Student>
    <id>2</id>
    <name>Student B</name>
</Student>

これは、そのコンテンツが

POST

リクエストボディに変換される方法です。

HttpPost httpPost = new HttpPost(BASE__URL + "1/students");
InputStream resourceStream = this.getClass().getClassLoader()
  .getResourceAsStream("conflict__student.xml");
httpPost.setEntity(new InputStreamEntity(resourceStream));


Content-Type

ヘッダーは、リクエストのコンテンツタイプがXMLであることをサーバーに伝えるために設定されています。

httpPost.setHeader("Content-Type", "text/xml");

アップロードされた

Student

オブジェクトは最初の

Course

インスタンスに既に存在しているため、作成が失敗して

Conflict

(409)ステータスの応答が返されることが予想されます。次のコードスニペットは期待を検証します。

HttpResponse response = client.execute(httpPost);
assertEquals(409, response.getStatusLine().getStatusCode());

次のテストでは、同じくクラスパスにある

created

student.xml__という名前のファイルからHTTPリクエストの本文を抽出します。ファイルの内容は次のとおりです。

<Student>
    <id>3</id>
    <name>Student C</name>
</Student>

前のテストケースと同様に、リクエストを作成して実行し、新しいインスタンスが正常に作成されたことを確認します。

HttpPost httpPost = new HttpPost(BASE__URL + "2/students");
InputStream resourceStream = this.getClass().getClassLoader()
  .getResourceAsStream("created__student.xml");
httpPost.setEntity(new InputStreamEntity(resourceStream));
httpPost.setHeader("Content-Type", "text/xml");

HttpResponse response = client.execute(httpPost);
assertEquals(200, response.getStatusLine().getStatusCode());

Webサービスリソースの新しい状態を確認することがあります。

Student student = getStudent(2, 3);
assertEquals(3, student.getId());
assertEquals("Student C", student.getName());

これは、新しい

Student

オブジェクトに対する要求に対するXML応答が次のようになることです。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Student>
    <id>3</id>
    <name>Student C</name>
</Student>


5.4.

PUT

リクエスト

更新しようとしている

Course

オブジェクトが存在しない無効な更新リクエストから始めましょう。これは、Webサービスリソース内に存在しない

Course

オブジェクトを置き換えるために使用されるインスタンスの内容です。

<Course>
    <id>3</id>
    <name>Apache CXF Support for RESTful</name>
</Course>

その内容はクラスパス上の

non

existent

course.xml

というファイルに格納されます。それは抽出され、それから以下のコードによって

PUT

リクエストの本体を生成するのに使用されます。

HttpPut httpPut = new HttpPut(BASE__URL + "3");
InputStream resourceStream = this.getClass().getClassLoader()
  .getResourceAsStream("non__existent__course.xml");
httpPut.setEntity(new InputStreamEntity(resourceStream));


Content-Type

ヘッダーは、リクエストのコンテンツタイプがXMLであることをサーバーに伝えるために設定されています。

httpPut.setHeader("Content-Type", "text/xml");

存在しないオブジェクトを更新するために意図的に無効なリクエストを送信したため、

Not Found

(404)レスポンスが受信されると予想されます。応答が検証されます。

HttpResponse response = client.execute(httpPut);
assertEquals(404, response.getStatusLine().getStatusCode());


PUT

リクエストの2番目のテストケースでは、同じフィールド値を持つ

Course

オブジェクトを送信します。この場合は何も変更されていないため、

Not Modified

(304)ステータスの応答が返されることが予想されます。全体のプロセスは説明されています:

HttpPut httpPut = new HttpPut(BASE__URL + "1");
InputStream resourceStream = this.getClass().getClassLoader()
  .getResourceAsStream("unchanged__course.xml");
httpPut.setEntity(new InputStreamEntity(resourceStream));
httpPut.setHeader("Content-Type", "text/xml");

HttpResponse response = client.execute(httpPut);
assertEquals(304, response.getStatusLine().getStatusCode());

ここで、

unchanged

course.xml__は更新に使用される情報を保持するクラスパス上のファイルです。その内容は次のとおりです。

<Course>
    <id>1</id>
    <name>REST with Spring</name>
</Course>

最後の

PUT

要求のデモでは、有効な更新を実行します。

以下は、Webサービスリソース内の

Course

インスタンスを更新するために使用される内容を持つ

changed

course.xml__ファイルの内容です。

<Course>
    <id>2</id>
    <name>Apache CXF Support for RESTful</name>
</Course>

これがリクエストの構築方法と実行方法です。

HttpPut httpPut = new HttpPut(BASE__URL + "2");
InputStream resourceStream = this.getClass().getClassLoader()
  .getResourceAsStream("changed__course.xml");
httpPut.setEntity(new InputStreamEntity(resourceStream));
httpPut.setHeader("Content-Type", "text/xml");

サーバーへの

PUT

リクエストを検証し、アップロードが成功したことを検証しましょう。

HttpResponse response = client.execute(httpPut);
assertEquals(200, response.getStatusLine().getStatusCode());

Webサービスリソースの新しい状態を確認しましょう。

Course course = getCourse(2);
assertEquals(2, course.getId());
assertEquals("Apache CXF Support for RESTful", course.getName());

次のコードスニペットは、以前にアップロードされた

Course

オブジェクトに対するGETリクエストが送信されたときのXMLレスポンスの内容を示しています。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Course>
    <id>2</id>
    <name>Apache CXF Support for RESTful</name>
</Course>


5.5.

DELETE

リクエスト

まず、存在しない

Student

インスタンスを削除してみましょう。操作は失敗し、

Not Found

(404)ステータスの対応する応答が予想されます。

HttpDelete httpDelete = new HttpDelete(BASE__URL + "1/students/3");
HttpResponse response = client.execute(httpDelete);
assertEquals(404, response.getStatusLine().getStatusCode());


DELETE

リクエストの2番目のテストケースでは、リクエストを作成、実行、検証します。

HttpDelete httpDelete = new HttpDelete(BASE__URL + "1/students/1");
HttpResponse response = client.execute(httpDelete);
assertEquals(200, response.getStatusLine().getStatusCode());

次のコードスニペットを使用して、Webサービスリソースの新しい状態を確認します。

Course course = getCourse(1);
assertEquals(1, course.getStudents().size());
assertEquals(2, course.getStudents().get(0).getId());
assertEquals("Student B", course.getStudents().get(0).getName());

次に、Webサービスリソースの最初の

Course

オブジェクトに対する要求の後に受信されたXML応答をリストします。

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Course>
    <id>1</id>
    <name>REST with Spring</name>
    <students>
        <id>2</id>
        <name>Student B</name>
    </students>
</Course>

最初の

Student

が正常に削除されたことは明らかです。


6. テスト実行

セクション4では、

RestfulServer

クラスの

main

メソッドで

Server

インスタンスを作成および破棄する方法について説明しました。

サーバーを稼働させる最後のステップは、その

main

メソッドを呼び出すことです。それを実現するために、Exec MavenプラグインがMaven POMファイルに含まれて構成されています。

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>exec-maven-plugin</artifactId>
    <version>1.5.0<version>
    <configuration>
        <mainClass>
          com.baeldung.cxf.jaxrs.implementation.RestfulServer
        </mainClass>
    </configuration>
</plugin>

このプラグインの最新バージョンはhttps://search.maven.org/classic/#search%7Cga%7C1%7Cg%3A%22org.codehaus.mojo%22%20AND%20a%3A%22exec-mavenで見つけることができます。 -plugin%22[このリンク]。

このチュートリアルに示されている成果物のコンパイルおよびパッケージ化の過程で、Maven Surefireプラグインは

Test

で始まるまたは終わる名前を持つクラスで囲まれたすべてのテストを自動的に実行します。

この場合、プラグインはこれらのテストを除外するように設定されるべきです。

<plugin>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>2.19.1</version>
    <configuration>
    <excludes>
        <exclude>** ** /ServiceTest</exclude>
    </excludes>
    </configuration>
</plugin>

上記の設定では、

ServiceTest

はテストクラスの名前なので除外されます。含まれるテストがサーバーの接続準備が整う前にMaven Surefireプラグインによって実行されない限り、そのクラスに任意の名前を選択できます。

Maven Surefireプラグインの最新バージョンについては、https://search.maven.org/classic/#search%7Cga%7C1%7Ca%3A%22maven-surefire-plugin%22[here]を確認してください。

これで、

exec:java

という目標を実行してRESTful Webサービスサーバーを起動し、IDEを使用して上記のテストを実行できます。同様に、あなたはコマンドを実行することによってテストを開始することができます


7. 結論

このチュートリアルでは、JAX-RS実装としてのApache CXFの使い方を説明しました。このフレームワークを使用して、RESTful Webサービスのリソースを定義し、サービスを公開するためのサーバーを作成する方法を示しました。

これらすべての例とコードスニペットの実装はhttps://github.com/eugenp/tutorials/tree/master/apache-cxf/cxf-jaxrs-implementation[GitHubプロジェクト]にあります。