1. 概要

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

具体的には、RESTful Webサービスを構築および公開する方法と、サービスを検証するための単体テストを作成する方法を段階的に説明します。

これは、ApacheCXFに関するシリーズの3番目です。 最初のものは、JAX-WSに完全に準拠した実装としてのCXFの使用に焦点を当てています。 2番目の記事は、SpringでCXFを使用する方法に関するガイドを提供します。

2. Mavenの依存関係

最初に必要な依存関係は org.apache.cxf:cxf-rt-frontend- jaxrs。 このアーティファクトは、JAX-RSAPIとCXF実装を提供します。

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

このチュートリアルでは、サーブレットコンテナを使用する代わりに、CXFを使用してサーバーエンドポイントを作成し、Webサービスを公開します。 したがって、次の依存関係をMavenPOMファイルに含める必要があります。

<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>

ここには、cxf-rt-frontend-jaxrs依存関係の最新バージョンがあります。 org.apache.cxf:cxf-rt-transports-http-jetty アーティファクトの最新バージョンについては、このリンクを参照することもできます。 最後に、httpclientの最新バージョンはここにあります。

3. リソースクラスとリクエストマッピング

簡単な例の実装を始めましょう。 2つのリソースを使用してRESTAPIをセットアップしますコース学生。

単純なものから始めて、より複雑な例に移ります。

3.1. リソース

Studentリソースクラスの定義は次のとおりです。

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

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

}

@XmlRootElement アノテーションを使用して、このクラスのインスタンスをXMLにマーシャリングする必要があることをJAXBに通知していることに注意してください。

次に、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
    
}

最後に、 CourseRepository を実装しましょう。これはルートリソースであり、Webサービスリソースへのエントリポイントとして機能します。

@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 –リクエストマッピングメソッド

それでは、実際のRESTAPIの実装に移りましょう。

@Path アノテーションを使用して、リソースPOJOでAPI操作の追加を開始します。

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

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

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

簡単に言うと、このメソッドは GET リクエストを処理するときに呼び出され、@GETアノテーションで示されます。

HTTPリクエストからstudentIdパスパラメータをマッピングする簡単な構文に注目してください。

次に、 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を持つオブジェクトがすでに存在する場合は409Conflictが返されます。

また、値が空の文字列であるため、@Pathアノテーションをスキップできることにも注意してください。

最後のメソッドは、DELETEリクエストを処理します。 id が受信パスパラメータである学生リストから要素を削除し、 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メソッドは、受信したcourseIdパスをキーとするcoursesマップのエントリの値であるCourseオブジェクトを返します。 GETリクエストのパラメータ。 内部的には、このメソッドはパスパラメーターをfindByIdヘルパーメソッドにディスパッチしてそのジョブを実行します。

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

次のメソッドは、 courses マップの既存のエントリを更新します。ここで、受信した PUT リクエストの本文はエントリ値であり、courseIdパラメータは関連付けられたキーです。 :

@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)応答を返します。既存のオブジェクトとアップロードされたオブジェクトのフィールド値は同じです。 指定されたidCourseインスタンスがcoursesマップで見つからない場合、メソッドは Not Found ( 404)ステータス。

このルートリソースクラスの3番目のメソッドは、HTTPリクエストを直接処理しません。 代わりに、リクエストを Course クラスに委任します。このクラスでは、一致するメソッドによってリクエストが処理されます。

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

委任されたリクエストを直前に処理するCourseクラス内のメソッドを示しました。

4. サーバーエンドポイント

このセクションでは、前のセクションでリソースが示されているRESTfulWebサービスを公開するために使用される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 = 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 、およびのHTTP要求に応答した後、サービスのリソース状態を検証します。 DELETE

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リクエスト

テストクラスでは、GETリクエストをWebサービスを実行しているサーバーに送信する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 を指定して、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メソッドを呼び出すことです。 これを実現するために、ExecMavenプラグインがMavenPOMファイルに含まれて構成されています。

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

このプラグインの最新バージョンは、このリンクから見つけることができます。

このチュートリアルで説明されているアーティファクトをコンパイルおよびパッケージ化するプロセスで、Maven Surefireプラグインは、Testで始まるまたは終わる名前を持つクラスに含まれるすべてのテストを自動的に実行します。 この場合、プラグインはこれらのテストを除外するように構成する必要があります。

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

上記の構成では、 ServiceTest はテストクラスの名前であるため、除外されます。 サーバーが接続の準備ができる前に、そこに含まれるテストがMaven Surefireプラグインによって実行されない場合は、そのクラスに任意の名前を選択できます。

Maven Surefireプラグインの最新バージョンについては、こちらを確認してください。

これで、 exec:java ゴールを実行して、RESTful Webサービスサーバーを起動し、IDEを使用して上記のテストを実行できます。 同様に、ターミナルでコマンド mvn -Dtest =ServiceTesttestを実行してテストを開始できます。

7. 結論

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

これらすべての例とコードスニペットの実装は、GitHubプロジェクトにあります。