RESTful Webサービスに対するApache CXFのサポート

RESTful WebサービスのApache CXFサポート

1. 概要

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

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

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

2. Mavenの依存関係

最初に必要な依存関係はorg.apache.cxf:cxf-rt-frontend-jaxrs. This artifact provides JAX-RS APIs as well as a CXF implementation:です


    org.apache.cxf
    cxf-rt-frontend-jaxrs
    3.1.7

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


    org.apache.cxf
    cxf-rt-transports-http-jetty
    3.1.7

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


    org.apache.httpcomponents
    httpclient
    4.5.2

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

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

簡単な例の実装を始めましょう。 2つのリソースCourseStudent.を使用して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 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 courses = new HashMap<>();

    // request handling methods

    private Course findById(int id) {
        for (Map.Entry 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 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の実装に移りましょう。

リソースPOJOで、@Pathアノテーションを使用して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を持つオブジェクトがすでに存在する場合は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パスパラメータをキーとするcoursesマップのエントリの値であるCourseオブジェクトを返します。リクエスト。 内部的には、このメソッドはパスパラメーターを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. Serverエンドポイント

このセクションでは、CXFサーバーの構築に焦点を当てます。CXFサーバーは、前のセクションで説明したリソースを持つRESTful Webサービスの公開に使用されます。 最初のステップは、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つのメソッド(GETPOSTPUT、およびDELETE)のHTTP要求に応答した後、サービスのリソース状態を検証します。

5.1. 準備

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

private static String BASE_URL = "http://localhost:8080/example/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つ目は、リソース内のコースと学生のidsを指定して、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;
}

これらのメソッドは、HTTPGET要求をサービスリソースに送信してから、対応するクラスのインスタンスへのXML応答を非整列化します。 どちらも、POSTPUT、およびDELETE要求の実行後にサービスリソースの状態を確認するために使用されます。

5.3. POSTリクエスト

このサブセクションでは、POSTリクエストの2つのテストケースを取り上げ、アップロードされたStudentインスタンスが競合を引き起こしたときと、正常に作成されたときのWebサービスの操作を示します。

最初のテストでは、次の内容のクラスパスにあるconflict_student.xmlファイルからマーシャリングされていないStudentオブジェクトを使用します。


    2
    Student B

これは、そのコンテンツが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リクエストの本文を抽出します。 ファイルの内容は次のとおりです。


    3
    Student C

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

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応答は次のようになります。



    3
    Student C

5.4. PUTリクエスト

更新されるCourseオブジェクトが存在しない、無効な更新要求から始めましょう。 Webサービスリソースに存在しないCourseオブジェクトを置き換えるために使用されるインスタンスのコンテンツは次のとおりです。


    3
    Apache CXF Support for RESTful

そのコンテンツは、クラスパス上の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は、更新に使用される情報を保持するクラスパス上のファイルです。 その内容は次のとおりです。


    1
    REST with Spring

PUTリクエストの最後のデモンストレーションでは、有効な更新を実行します。 以下は、WebサービスリソースのCourseインスタンスを更新するために使用されるchanged_course.xmlファイルのコンテンツです。


    2
    Apache CXF Support for RESTful

これはリクエストがどのように構築され実行されるかです:

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応答の内容を示しています。



    2
    Apache CXF Support for RESTful

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応答を一覧表示します。



    1
    REST with Spring
    
        2
        Student B
    

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

6. テスト実行

セクション4では、RestfulServerクラスのmainメソッドでServerインスタンスを作成および破棄する方法について説明しました。

サーバーを稼働させるための最後のステップは、そのmainメソッドを呼び出すことです。 それを実現するために、Maven POMファイルにExec Mavenプラグインが含まれて構成されています。


    org.codehaus.mojo
    exec-maven-plugin
    1.5.0
    
        
          com.example.cxf.jaxrs.implementation.RestfulServer
        
    

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

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


    maven-surefire-plugin
    2.19.1
    
    
        **/ServiceTest
    
    

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

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

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

7. 結論

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

これらすべての例とコードスニペットの実装は、the GitHub projectにあります。